C++特性之引用 (Boolan)
让客户满意是我们工作的目标,不断超越客户的期望值来自于我们对这个行业的热爱。我们立志把好的技术通过有效、简单的方式提供给客户,将通过不懈努力成为客户在信息化领域值得信任、有价值的长期合作伙伴,公司提供的服务项目有:域名注册、雅安服务器托管、营销软件、网站建设、蓬莱网站维护、网站推广。
本章内容:
1 引用的不同用例
1.1 引用变量
1.2 引用数据成员
1.3 引用参数
1.4 引用作为返回值
1.5 使用引用还是指针
1.6 右值引用
1 引用
在C++中,引用是变量的别名。所有对引用的修改都会改变被引用的变量的值。可将引用当作隐式指针,这个指针没有取变量地址和解除引用的麻烦。
也可以将引用当作原始变量的另一种名称。可以创建单独的引用变量,在类中使用引用数据成员,将引用作为函数和方法的参数,也可以让函数或者方法返回引用。
1.1 引用变量
引用变量在创建时必须初始化,如下:
int ival = 3; int &iRef = x;
赋值后,iRef就是ival的另一个名称。使用iRef就是使用ival的当前值。对iRef赋值会改变ival的值。
无法在类外面声明一个引用而不初始化它:
int &emptyRef; //编译出错
不能创建对未命名值(例如一个整数字面值)的引用,除非这个引用是一个const值。在下面的示例中,unnamedRef1将无法编译,因为这是一个针对常量的non-const引用。
这条语句意味着可以改变常量5的值,而这样做没有意义。由于unnamedRef2是一个const引用,因此可以运行,不能写成"unnamedRef2=7"。
int &unnamedRef1 = 5; //编译出错 const int &unnamedRef2 = 5; //正常运行
(1) 修改引用
引用总是引用初始化的那个变量:引用一旦创建,就无法修改。这一规则导致了许多令人迷惑的语法。如果在声明一个引用时用一个变量"赋值",那么这个引用就指向这个变量。
然而,如果在此后使用变量对引用赋值,被引用变量的值就变为赋值变量的值。引用不会更新为指向这个变量。示例代码如下:
int x = 3,y = 4; int &iRef = x; iRef = y;//Changes value of x to 4. Doesn't make iRef refer to y.
如果试图在赋值时取y的地址,以绕过这一限制:
int x = 3,y = 4; int &iRef = x; iRef = &y; //编译出错
上面的代码无法编译。y的地址是一个指针,但iRef声明为一个int的引用,而不是一个指针的引用。
如果将一个引用赋值给另一个引用时,只是修改了其指向的值,而不是修改所指向的引用变量。(在初始化引用之后无法改变引用所指的变量;而只能改变该变量的值。)
(2) 指针的引用和指向引用的指针
可以创建任何类型的引用,包括指针类型。下面给出一个指向int指针的引用例子:
int *pVal; int *&ptrRef = pVal; ptrRef = new int; *ptrRef = 5;
这一语法有一点奇怪:你可能不习惯看到*和&彼此相邻。然而,该语义上很简单:ptrRef是pVal的引用,pVal是一个指向int的指针。修改ptrRef会更改pVal。指针的引用很少见,但是在某些场合下很有用,在1.3节中会讨论这一内容。
注意:
(i)对引用取地址的结果与被引用变量取地址的结果是相同的。
(ii)无法声明引用的引用,或者指向引用的指针。
1.2 引用数据成员
类的数据成员可以引用,但是如果不是指向其他变量,引用就无法存在。因此,必须在构造函数初始化器(constructor initializer)中初始化引用数据成员,而不是在构造函数体
内。下面举例说明:
class MyClass { public: MyClass(int &iRef):m_ref(iRef) {} private: int &m_ref; };
1.3 引用参数
C++程序员通常不会单独使用引用变量或者引用数据成员。引用经常用作函数或者方法的参数。默认的参数传递机制是值传递:函数接收参数的副本。修改这些副本时,原始的参数
保持不变。引用允许指定另一种向函数传递参数的语义:按引用传递。当使用引用参数时,函数将引用作为参数。如果引用被修改,最初的参数变量也会修改。下面给出交换两个数的例子来说明:
void swap(int &first, int &second) { int temp = first; first = second; second = temp; }
可以采用下面的方式调用这个函数:
int x = 5, y = 6; swap(x, y);
当使用x和y做参数调用函数swap()时,first参数初始化为x的引用,second参数初始化为y的引用。当swap()修改first和second时,x和y实际上也被修改了。
就像无法使用常量初始化普通引用变量一样,不能将常量作为参数传递给按引用传递参数的函数:
swap(3, 4); //编译出错
(1) 指针转换为引用
某个函数或者方法需要一个引用做参数,而你拥有一个指向被传递值的指针,这是一种常见的困境。在此情况下,可以对指针解除引用(dereferencing),将指针"转换"为引用。
这一行为会给出指针所指的值,随后编译器用这个值初始化引用参数。例如,可以这样调用swap():
int x = 5, y = 6; int *px = &x; int *py = &y; swap(*px, *py);
(2) 按引用传递与按值传递
如果要修改参数,并修改传递给函数或者方法的变量,就需要使用按引用传递。然而,按引用传递的用途并不局限于此。按引用传递不需要将参数副本复制到函数,在有些情况下
会带来两面的好处:
(i)效率:复制较大的对象或者结构需要较长的时间。按引用传递只是把指向对象或者结构的指针传递给函数。
(ii)正确性:并非所有对象都允许按值传递,即使允许按值传递的对象,也可能不支持正确的深度复制(deep copying)。(如果需要深度复制,动态分配内存的对象必须提供自定
义复制构造函数。)
如果要利用好这些好处,但不想修改原始对象,可将参数标记为const,从而实现按常理引用传递参数。按引用传递的这些优点意味着,只有在参数是简单的内建类型(int或double
),且不需要修改参数的情况下才应该使用按值传递。其他情况下都应该按引用传递。
1.4 引用作为返回值
可以让函数或者方法返回一个引用,这样做的主要作用是提高效率。返回对象的引用不是返回整个对象可以避免不必要的复制。当然,只有涉及的对象在函数终止之后仍然存在的
情况才能使用这一技巧。(如果变量的作用域局限于函数或者方法,例如:堆栈中分配的变量,在函数结束时会被销毁。这个时候绝对不能返回这个变量的引用。)
返回引用的另一个原因是希望将返回值直接赋为左值(lvalue)(赋值语句在左边)。一些重载的运算符通常会返回引用。
1.5 使用引用还是指针
在C++中,引用有可能被认为是多余的:几乎所有使用引用可以完成的任务都可以用指针来代替完成。例如,可以这样编写swap()函数:
void swap(int *first, int *second) { int temp = *first; *first = *second; *second = temp; }
然而,这些代码不如使用引用版本那么清晰:引用可以使程序整洁并易于理解。此外,引用比指针安全:不可能存在无效的引用,也不需要显式地解除引用,因此不会遇到像指针
那样的解除引用问题。
大多数情况下,应该使用引用而不是指针。对象的引用甚至可以像指向对象的指针那样支持多态性。只有在需要改变所指地址时,才需要使用指针,因为无法改变引用所致的对像
。例如,动态分配内存时,应该将结果存储在指针而不是引用中。需要使用指针的第二种情况是可选参数。例如,指针参数可以定义为带默认值nullptr的可选参数,而引用参数不能这样定义。
还有一种方法可以判断使用指针还是引用作为参数和返回类型:考虑谁拥有内存。如果接受变量的代码负责释放相关对象的内存,必须使用指向对象的指针,最好是智能指针,这
是传递拥有权的推荐方式。如果接受变量的代码不需要释放内存,那么应该使用引用。
注意:如果不需要改变所指的地址,就应该使用引用而不是指针。
1.6 右值引用
在C++中,左值(lvalue)是可以获取其地址的一个量,例如一个有名称的变量。由于经常出现在赋值语句的左边,因此称其为左值。另一方面,所有不是左值的量都是右值(rvalue)
,例如常量值,临时对象或者临时值。通常右值位于赋值运算符的右边。
右值引用是一个对右值(rvalue)的引用。特别地,这是一个当右值是临时对象时使用的概念。右值引用的目的是提供在涉及临时对象时可以选用的特定方法。由于知道临时对象会
被销毁,通过右值引用,某些涉及复制大量值的操作可以通过简单的复制指向这些值的指针来实现。
函数可以将&&作为参数说明的一部分(例如 type&&name),来指定右值引用参数。通常,临时对象被当作const type&,但当函数重载使用了右值引用时,可以解析临时对象,
用于该重载。下面的示例说明了这一点。代码首先定义了两个incr()函数,一个接受左值引用;另一个接受右值引用:
// Increment value using lvalue reference parameter. void incr(int &value) { cout << "increment with lvalue reference" << endl; ++value; } // Increment value using rvalue reference parameter. void incr(int &&value) { cout << "increment with rvalue reference" << endl; ++value; }
可以使具有名称的变量作为参数调用incr()函数。于是a是一个具有名称的变量,因此调用接受左值引用的incr()函数。调用完incr()后,a的值将是11。
int a = 10, b = 20; incr(a);//调用incr(int &value);
还可以用表达式作为参数来调用inrc()函数。此时无法使用接受左值引用作为参数的incr()函数,因为表达式a+b的结果是临时的,这不是一个左值。在此情况下,会调用右值引用
版本。由于参数是一个临时值,当incr()函数调用结束后,会丢失这个增加的值。
incr(a + b); //将调用incr(int &&value);
字面量也可以作inrc()调用的参数,此时同样会调用右值引用版本,因为字面量不能作为左值。
incr(3); //将调用incr(int &&value);
如果删除接受左值引用的incr()函数,使用名称的变量调用incr(),例如:incr(b),此时会导致编译错误,因为右值引用参数(int &&value)永远不会与左值(b)绑定。如下所示可
以使用std::move()将左值转换为右值,强迫编译器调用incr()的右值版本。当incr()调用结束后,b的值为21。
incr(std::move(b)); //将调用incr(int &&value);
右值引用并不局限于函数的参数。可以声明右值引用类型的变量,并对其赋值,尽管这种用法并不常见。查下看如下代码:
int &i = 2;//invalid:reference to a constant int a = 2, b = 3; int &j = a + b;//invalid:reference to a temporary
使用右值引用后,下面的代码完全合法:
int &&i = 2; int a = 2, b = 3; int &&j = a + b;
前面示例中单独使用右值引用的情况很少见。