4.4.3 返回引用
一个广受提倡的用于减少运行时间的技术就是返回引用,而不是返回值,因为返回值将需要高昂的拷贝开销。考虑4.2.2节的List::head函数,所有head函数的调用者-包括那些只是检查表头节点值的用户-都要承受把节点值拷贝出去的开销:
- List<Widget> l;
- //...
- cout << "first widget is " << l.head(); //拷贝了一个Widget.
为了避免这样使用的拷贝开销,我们可以修改head函数,让它返回一个指向内部存储表头值的拷贝的引用: - template<class T>
- class List {
- public:
- const T& head() const; //返回一个引用
- //...
- };
如果List类如上面一样定义,那么用户代码将不会拷贝一个Widget对象。我们还在声明head函数时,让它返回一个const引用,因为返回一个非const引用将允许用户在不使用cast转型去除const关键字的情况下就可以改变const List中存储的值: - void whoops(const List<Widget>&l) {
- widget newvalue;
- //...
- 1.head() = newvalue; //uh-oh!如果head函数返回一个非const
- //对象,这条语句是合法的,但不是一个好的
- //主意
- //...
- }
对于任何返回引用的程序库函数,程序库文档都应该给出关于引用存活期(指引用的有效时间)的说明。例如,我们如下给出head函数的文档:
head函数返回的引用将保持有效,直到引用指向的值从所给的List中删除。
返回引用有两个缺点。首先,它使用户的代码更加容易产生错误。回想一下4.2.2节的insert函数和tail函数:insert函数把一个节点插入到List的表头,tail函数返回除了头节点之外的表尾,现在让我们考虑下面代码:
- List<int> l;
- l.insert(0);
- const int& i = l.head();
- l.insert(1); //i仍然是有效的。
- ll = l.tail(); //i仍然是有效的。
- l = 1.tail(); //i现在变成无效了。
当i变成无效之后,任何试图使用引用i的操作都会是未定义的操作。但在返回一个T类型(即返回值),而不是返回T&类型(即返回引用)的head函数里,就不会出现上面这种未定义的操作。
返回引用的第二个缺点是限制了我们实现所给类的方式。例如,我们发现,如果以共享的方式实现类List,那么大多数用户的代码运行速度将会更快。特别地,让我们考虑List<int>x,它的底层实现如下图所示:
如果用户编写下面代码:
- List<int> y = x;
那么y将和x共享同一个节点:
如果用户在x中插入一个7,并在y中插入一个8,那么底层实现将会如下所示:
最后,如果用户要在y指向的链表尾部追加值9,那么x和y将不再共享同一个表尾;因此,我们把所有共享节点都拷贝出来生成一个新的链表,并把9附加到生成的新链表的尾部:
现在假设函数remove_all将会移除链表中所有的值,函数append将会在链表来尾追加一个给定的值。考虑下面的代码:
- List<int> x;
- x.insert(0);
- List<int> y = x;
- const int& i = y.head():
下面是我们上面代码得到的底层实现:
如果我们把1追加到y的末尾:
- y.append(1); //i仍然是有效的。
那么x和y将不再具有相同的表尾,所以y的List将是一个拷贝的List。(我们也可以直接在y原来的List追加一个节点,即让y保持原来的链表,而让x指向一个新的拷贝。但这个方法并不能解决问题,而只是把问题转移了-假设有一个指向x.head()函数的引用,那样问题将会出现。)现在的底层实现如下:
假设我们现在改变x:
- x.remove_all(); //i是否依然有效呢?
因为程序中并没有其他和x共享上面节点的List对象,所以remove_all操作将会删除x中所有的节点。从图形来看,我们将得到以下结果:
在调用了remove_all函数之后,引用i可能会是无效的。因此,基于上面List类的实现,我们根本不能保证(除非我们特别聪明),head函数返回的引用将会保持有效,直到此引用指向的值从所给的List(即我们调用head函数的List)中删除。
基于上面List的实现,我们对head函数唯一可以做的保证只能如下:
head函数返回的引用将保持有效,直到对任何List<T>执行了非const操作。
遗憾的是,有了上面这个保证,用户程序却会有意无意地操纵无效的引用。所以说,为了加强上面这个保证,来防止用户程序操纵无效的引用,我们往往需要改变List的实现,而这种改变通常都会降低List的效率。
返回引用可以提高效率,但它可能会使用户代码更加容易产生错误,并且限制类的实现方式。如果函数确实需要返回引用,那么程序库就必须给出关于每个引用存活期的文档。所以说,使用引用的过程必须是非常小心谨慎的,来确保每个引用都是有效的、正确的。