函数形参与引用的相关问题
一、引用形参
考虑下面不适宜复制实参的例子,该函数希望交换两个实参的值:
1 // incorrect version of swap: The arguments are not changed! 2 void swap(int v1, int v2) 3 { 4 int tmp = v2; 5 v2 = v1; // assigns new value to local copy of the argument 6 v1 = tmp; 7 } // local objects v1 and v2 no longer exist
这个例子期望改变实参本身的值。
但实际上执行 swap 时,只交换了其实参的局部副本,而传递给 swap 的实参并没有修改:
1 int main(void) 2 { 3 int i = 10; 4 int j = 20; 5 cout << "Before swap():\ti: " 6 << i << "\tj: " << j << endl; 7 swap(i, j); 8 cout << "After swap():\ti: " 9 << i << "\tj: " << j << endl; 10 return 0; 11 }
编译并执行程序,产生如下输出结果:
Before swap(): i: 10 j: 20
After swap(): i: 10 j: 20
为了使 swap 函数以期望的方式工作,交换实参的值,需要将形参定义为引用类型:
1 // ok: swap acts on references to its arguments 2 void swap(int &v1, int &v2) 3 { 4 int tmp = v2; 5 v2 = v1; 6 v1 = tmp; 7 }
与所有引用一样,引用形参直接关联到其所绑定的对象,而并非这些对象的副本。
定义引用时,必须用与该引用绑定的对象初始化该引用。
引用形参完全以相同的方式工作。每次调用函数,引用形参被创建并与相应实参关联。
此时,当调用 swap(i, j);
形参 v1 只是对象 i 的另一个名字,而 v2 则是对象 j 的另一个名字。
对 v1 的任何修改实际上也是对 i 的修改。对于 v2 同样如此。
重新编译使用 swap 的这个修订版本的 main 函数后,可以看到输出结果是正确的:
Before swap(): i: 10 j: 20
After swap(): i: 20 j: 10
注意:从 C 语言背景转到 C++ 的程序员习惯通过传递指针来实现对实参的访问。
而在 C++ 中,使用引用形参则更安全和更自然。
二、使用引用形参返回额外的信息
通过对上面 swap 这个例子的讨论,可以了解如何利用引用形参让函数修改实参的值。
然而,引用形参的另一种用法是向主调函数返回额外的结果。
函数只能返回单个值,但有些时候,函数有不止一个的内容需要返回。
例如,定义一个 find_val 函数,功能是在一个整型 vector 对象的元素中搜索某个特定值。
如果找到满足要求的元素,则返回指向该元素的迭代器;
否则返回一个迭代器,指向该 vector 对象的 end 操作返回的元素。
此外,如果该值出现了不止一次,我们还希望函数可以返回其出现的次数。
在这种情况下,返回的迭代器应该指向具有要寻找的值的第一个元素。
如何定义既返回一个迭代器又返回出现次数的函数?
我们可以定义一种包含一个迭代器和一个计数器的新类型。
然而更简便的解决方案是 —— 给 find_val 传递一个额外的引用实参,用于返回出现次数的统计结果:
1 // returns an iterator that refers to the first occurrence of value 2 // the reference parameter occurs contains a second return value 3 vector<int>::const_iterator find_val( 4 vector<int>::const_iterator beg, // first element 5 vector<int>::const_iterator end, // one past last element 6 int value, // the value we want 7 vector<int>::size_type &occurs) // number of times it occurs 8 { 9 // res_iter will hold first occurrence, if any 10 vector<int>::const_iterator res_iter = end; 11 occurs = 0; // set occurrence count parameter 12 for ( ; beg != end; ++beg) 13 if (*beg == value) { 14 // remember first occurrence of value 15 if (res_iter == end) 16 res_iter = beg; 17 ++occurs; // increment occurrence count 18 } 19 return res_iter; // count returned implicitly in occurs 20 }
调用 find_val 时,需传递 4 个实参:
一对标志 vector 对象中要搜索的元素范围的迭代器,
所查找的值,以及用于存储出现次数的 size_type 类型(第 3.2.3 节)对象。
假设 ivec 是 vector<int>, it 类型的对象,it 是一个适当类型的迭代器,
而 ctr 则是 size_type 类型的变量,则可如此调用该函数:
it = find_val(ivec.begin(), ivec.end(), 42, ctr);
调用后,ctr 的值将是 42 出现的次数,如果 42 在 ivec 中出现了,
则 it 将指向其第一次出现的位置;否则,it 的值为 ivec.end(),而 ctr 则为 0。
三、利用 const 引用避免复制
在向函数传递大型对象时,需要使用引用形参,这是引用形参适用的另一种情况。
虽然复制实参对于内置数据类型的对象或者规模较小的类类型对象来说没有什么问题,
但是对于大部分的类类型或者大型数组,它的效率太低;此外,某些类类型是无法复制的。
使用引用形参,函数可以直接访问实参对象,而无须复制它。
下面用一个比较两个 string 对象长度的函数作为例子。
这个函数需要访问每个 string 对象的 size,但不必修改这些对象。
由于 string 对象可能相当长,所以我们希望避免复制操作。
使用 const 引用就可避免复制:
1 // compare the length of two strings 2 bool isShorter(const string &s1, const string &s2) 3 { 4 return s1.size() < s2.size(); 5 }
其每一个形参都是 const string 类型的引用。因为形参是引用,所以不复制实参。
又因为形参是 const 引用,所以 isShorter 函数不能使用该引用来修改实参。
如果使用引用形参的唯一目的是避免复制实参,则应将形参定义为 const 引用。
四、更灵活的指向 const 的引用
如果函数具有普通的非 const 引用形参,则显然不能通过 const 对象进行调用。
毕竟,此时函数可以修改传递进来的对象,这样就违背了实参的 const 特性。
但比较容易忽略的是,调用这样的函数时,传递一个右值或具有需要转换的类型的对象同样是不允许的:
1 // function takes a non-const reference parameter 2 int incr(int &val) 3 { 4 return ++val; 5 } 6 int main() 7 { 8 short v1 = 0; 9 const int v2 = 42; 10 int v3 = incr(v1); // error: v1 is not an int 11 v3 = incr(v2); // error: v2 is const 12 v3 = incr(0); // error: literals are not lvalues 13 v3 = incr(v1 + v2); // error: addition doesn't yield an lvalue 14 int v4 = incr(v3); // ok: v3 is a non const object type int 15 }
问题的关键是非 const 引用形参只能与完全同类型的非 const 对象关联。
应该将不修改相应实参的形参定义为 const 引用。
如果将这样的形参定义为非 const 引用,则毫无必要地限制了该函数的使用。
例如,可编写下面的程序在一个 string 对象中查找一个指定的字符:
1 // returns index of first occurrence of c in s or s.size() if c isn't in s 2 // Note: s doesn't change, so it should be a reference to const 3 string::size_type find_char(string &s, char c) 4 { 5 string::size_type i = 0; 6 while (i != s.size() && s[i] != c) 7 ++i; // not found, look at next character 8 return i; 9 }
这个函数将其 string 类型的实参当作普通(非 const)的引用,尽管函数并没有修改这个形参的值。
这样的定义带来的问题是不能通过字符串字面值来调用这个函数:
if (find_char("Hello World", 'o')) // ...
虽然字符串字面值可以转换为 string 对象,但上述调用仍然会导致编译失败。
继续将这个问题延伸下去会发现,即使程序本身没有 const 对象,
而且只要使用 string 对象(而并非字符串字面值或产生 string 对象的表达式)
调用 find_char 函数,编译阶段的问题依然会出现。
例如,可能有另一个函数 is_sentence 调用 find_char 来判断一个 string 对象是否是句子:
1 bool is_sentence (const string &s) 2 { 3 // if there's a period and it's the last character in s 4 // then s is a sentence 5 return (find_char(s, '.') == s.size() - 1); 6 }
五、传递指向指针的引用
假设我们想编写一个与前面交换两个整数的 swap 类似的函数,实现两个指针的交换。
已知需用 * 定义指针,用 & 定义引用。
现在,问题在于如何将这两个操作符结合起来以获得指向指针的引用。这里给出一个例子:
1 // swap values of two pointers to int 2 void ptrswap(int *&v1, int *&v2) 3 { 4 int *tmp = v2; 5 v2 = v1; 6 v1 = tmp; 7 }
int *&v1 的定义应从右至左理解:v1 是一个引用,与指向 int 型对象的指针相关联。
也就是说,v1 只是传递进 ptrswap 函数的任意指针的别名。
重写之前的 main 函数,调用 ptrswap 交换分别指向值 10 和 20 的指针:
1 int main() 2 { 3 int i = 10; 4 int j = 20; 5 int *pi = &i; // pi points to i 6 int *pj = &j; // pj points to j 7 cout << "Before ptrswap():\t*pi: " 8 << *pi << "\t*pj: " << *pj << endl; 9 ptrswap(pi, pj); // now pi points to j; pj points to i 10 cout << "After ptrswap():\t*pi: " 11 << *pi << "\t*pj: " << *pj << endl; 12 return 0; 13 }
编译并执行后,该程序产生如下结果:
Before ptrswap(): *pi: 10 *pj: 20
After ptrswap(): *pi: 20 *pj: 10
即指针的值被交换了。在调用 ptrswap 时,pi 指向 i,而 pj 则指向 j。
在 ptrswap 函数中,指针被交换,使得调用 ptrswap 结束后,pi 指向了原来 pj 所指向的对象。
换句话说,现在 pi 指向 j,而 pj 则指向了 i。
如上代码,函数 is_sentence 中 find_char 的调用是一个编译错误。
传递进 is_sentence 的形参是指向 const string 对象的引用,
不能将这种类型的参数传递给 find_char,因为后者期待得到一个指向非 const string 对象的引用。
====================== 传说中的神奇的分割线 ========================
以下是两个具有总结性质问答题,会在面试中经常遇到:
举一个例子说明什么时候应该将形参定义为引用类型。再举一个例子说明什么时候不应该将形参定义为引用。
如果希望通过函数调用修改实参的值,就应该将形参定义为引用类型。
例如,用 swap 函数交换两个数的值。如果不将形参定义为指针类型,则需要实现直接修改实参的值,就只能使用形参定义为引用类型。
1 void swap(int &v1, int &v2) 2 { 3 int tmp = v2; 4 v2 = v1; 5 v1 = tmp; 6 }
除了类似上述例子的情况以外,为了通过一次函数调用来获得多个结果值的,也可以使用引用形参。
另外,在向函数传递大型对象时,为了避免复制实参以提高效率,以及使用无法复制的类类型
(其复制构造函数为 private 的类类型) 作为形参类型时,也应该讲形参定义为引用类型。
但这是使用形参的目的是为了避免复制实参,所以应该讲形参定义为 const 引用。
如果不需要通过函数调用修改实参的值,就不应该讲形参定义为引用类型。
例如,在下面的求绝对值的函数 abs 中,形参就不宜定义为引用类型。
1 int abs(int x) 2 { 3 return x > 0 ? x : -x; 4 }
何时应该将引用形参定义为 const 对象?如果在需要 const 引用时,将形参定义为普通引用,则会出现什么问题?
如果使用引用形参的唯一目的是避免复制实参,则应该将引用形参定义为 const 对象。
如果在需要 const 引用时, 将形参定义为普通引用,则会导致不能使用右值和 const 对象,
以及需要进行类型转换的对象来调用该函数,从而不必要地限制了该函数的使用。