当前位置:嗨网首页>书籍在线阅读

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)。以前的引用(使用&声明的引用)现在称为左值引用。