06-引用的属性和特别之处
8.2.3 引用的属性和特别之处
使用引用参数时,需要了解其一些特点。请看程序清单8.5。它使用两个函数来计算参数的立方,其中一个函数接受double类型的参数,另一个接受double引用。为了说明这一点,我们有意将计算立方的代码编写得比较奇怪。
程序清单8.5 cubes.cpp
// cubes.cpp -- regular and reference arguments
#include <iostream>
double cube(double a);
double refcube(double &ra);
int main ()
{
using namespace std;
double x = 3.0;
cout << cube(x);
cout << " = cube of " << x << endl;
cout << refcube(x);
cout << " = cube of " << x << endl;
return 0;
}
double cube(double a)
{
a *= a * a;
return a;
}
double refcube(double &ra)
{
ra *= ra * ra;
return ra;
}
下面是该程序的输出:
27 = cube of 3
27 = cube of 27
refcube()函数修改了main()中的x值,而cube()没有,这提醒我们为何通常按值传递。变量a位于cube()中,它被初始化为x的值,但修改a并不会影响x。但由于refcube()使用了引用参数,因此修改ra实际上就是修改x。如果程序员的意图是让函数使用传递给它的信息,而不对这些信息进行修改,同时又想使用引用,则应使用常量引用。例如,在这个例子中,应在函数原型和函数头中使用const:
double refcube(const double &ra);
如果这样做,当编译器发现代码修改了ra的值时,将生成错误消息。
顺便说一句,如果要编写类似于上述示例的函数(即使用基本数值类型),应采用按值传递的方式,而不要采用按引用传递的方式。当数据比较大(如结构和类)时,引用参数将很有用,您稍后便会明白这一点。
按值传递的函数,如程序清单8.5中的函数cube(),可使用多种类型的实参。例如,下面的调用都是合法的:
double z = cube(x + 2.0); // evaluate x + 2.0, pass value
z = cube(8.0); // pass the value 8.0
int k = 10;
z = cube(k); // convert value of k to double, pass value
double yo[3] = { 2.2, 3.3, 4.4};
z = cube (yo[2]); // pass the value 4.4
如果将与上面类似的参数传递给接受引用参数的函数,将会发现,传递引用的限制更严格。毕竟,如果ra是一个变量的别名,则实参应是该变量。下面的代码不合理,因为表达式x + 3.0并不是变量:
double z = refcube(x + 3.0); // should not compile
例如,不能将值赋给该表达式:
x + 3.0 = 5.0; // nonsensical
如果试图使用像refcube(x + 3.0)这样的函数调用,将发生什么情况呢?在现代的C++中,这是错误的,大多数编译器都将指出这一点;而有些较老的编译器将发出这样的警告:
Warning: Temporary used for parameter 'ra' in call to refcube(double &)
之所以做出这种比较温和的反应是由于早期的C++确实允许将表达式传递给引用变量。有些情况下,仍然是这样做的。这样做的结果如下:由于x + 3.0不是double类型的变量,因此程序将创建一个临时的无名变量,并将其初始化为表达式x + 3.0的值。然后,ra将成为该临时变量的引用。下面详细讨论这种临时变量,看看什么时候创建它们,什么时候不创建。
临时变量、引用参数和const
如果实参与引用参数不匹配,C++将生成临时变量。当前,仅当参数为const引用时,C++才允许这样做,但以前不是这样。下面来看看何种情况下,C++将生成临时变量,以及为何对const引用的限制是合理的。
首先,什么时候将创建临时变量呢?如果引用参数是const,则编译器将在下面两种情况下生成临时变量:
- 实参的类型正确,但不是左值;
- 实参的类型不正确,但可以转换为正确的类型。
左值是什么呢?左值参数是可被引用的数据对象,例如,变量、数组元素、结构成员、引用和解除引用的指针都是左值。非左值包括字面常量(用引号括起的字符串除外,它们由其地址表示)和包含多项的表达式。在C语言中,左值最初指的是可出现在赋值语句左边的实体,但这是引入关键字const之前的情况。现在,常规变量和const变量都可视为左值,因为可通过地址访问它们。但常规变量属于可修改的左值,而const变量属于不可修改的左值。
回到前面的示例。假设重新定义了refcube(),使其接受一个常量引用参数:
double refcube(const double &ra)
{
return ra * ra * ra;
}
现在考虑下面的代码:
double side = 3.0;
double * pd = &side;
double & rd = side;
long edge = 5L;
double lens[4] = { 2.0, 5.0, 10.0, 12.0};
double c1 = refcube(side); // ra is side
double c2 = refcube(lens[2]); // ra is lens[2]
double c3 = refcube(rd); // ra is rd is side
double c4 = refcube(*pd); // ra is *pd is side
double c5 = refcube(edge); // ra is temporary variable
double c6 = refcube(7.0); // ra is temporary variable
double c7 = refcube(side + 10.0); // ra is temporary variable
参数side、lens[2]、rd和*pd都是有名称的、double类型的数据对象,因此可以为其创建引用,而不需要临时变量(还记得吗,数组元素的行为与同类型的变量类似)。然而,edge虽然是变量,类型却不正确,double引用不能指向long。另一方面,参数7.0和side + 10.0的类型都正确,但没有名称,在这些情况下,编译器都将生成一个临时匿名变量,并让ra指向它。这些临时变量只在函数调用期间存在,此后编译器便可以随意将其删除。
那么为什么对于常量引用,这种行为是可行的,其他情况下却不行的呢?对于程序清单8.4中的函数swapr():
void swapr(int & a, int & b) // use references
{
int temp;
temp = a; // use a, b for values of variables
a = b;
b = temp;
}
如果在早期C++较宽松的规则下,执行下面的操作将发生什么情况呢?
long a = 3, b = 5;
swapr(a, b);
这里的类型不匹配,因此编译器将创建两个临时int变量,将它们初始化为3和5,然后交换临时变量的内容,而a和b保持不变。
简而言之,如果接受引用参数的函数的意图是修改作为参数传递的变量,则创建临时变量将阻止这种意图的实现。解决方法是,禁止创建临时变量,现在的C++标准正是这样做的(然而,在默认情况下,有些编译器仍将发出警告,而不是错误消息,因此如果看到了有关临时变量的警告,请不要忽略)。
现在来看refcube()函数。该函数的目的只是使用传递的值,而不是修改它们,因此临时变量不会造成任何不利的影响,反而会使函数在可处理的参数种类方面更通用。因此,如果声明将引用指定为const,C++将在必要时生成临时变量。实际上,对于形参为const引用的C++函数,如果实参不匹配,则其行为类似于按值传递,为确保原始数据不被修改,将使用临时变量来存储值。
注意: 如果函数调用的参数不是左值或与相应的const引用参数的类型不匹配,则C++将创建类型正确的匿名变量,将函数调用的参数的值传递给该匿名变量,并让参数来引用该变量。
应尽可能使用const
将引用参数声明为常量数据的引用的理由有三个:
- 使用const可以避免无意中修改数据的编程错误;
- 使用const使函数能够处理const和非const实参,否则将只能接受非const数据;
- 使用const引用使函数能够正确生成并使用临时变量。
因此,应尽可能将引用形参声明为const。
C++11新增了另一种引用——右值引用(rvalue reference)。这种引用可指向右值,是使用&&声明的:
double && rref = std::sqrt(36.00); // not allowed for double &
double j = 15.0;
double && jref = 2.0* j + 18.5; // not allowed for double &
std::cout << rref << '\n'; // display 6.0
std::cout << jref << '\n'; // display 48.5;
新增右值引用的主要目的是,让库设计人员能够提供有些操作的更有效实现。第18章将讨论如何使用右值引用来实现移动语义(move semantics)。以前的引用(使用&声明的引用)现在称为左值引用。