本节书摘来自异步社区出版社《C++覆辙录》一书中的第1章,第1.5节,作者: 【美】Stephen C. Dewhurst(史蒂芬 C. 杜赫斯特),更多章节内容可以访问云栖社区“异步社区”公众号查看。
1.55:对引用的认识误区
对于引用的使用,主要存在两个常见的问题。首先,它们经常和指针搞混。其次,它们未被充分利用。好多在C++工程里使用的指针实际上只是C阵营那些老顽固的杰作,该是引用翻身的时候了。
引用并非指针。引用只是其初始化物的别名。记好了,能对引用做的唯一操作就是初始化它。一旦初始化结束,引用就是其初始化物的另一种写法罢了(凡事皆有例外,请看常见错误44)。引用是没有地址的,甚至它们有可能不占任何存储:
int a = 12;
int &ra = a;
int *ip = &ra; // ip指涉到a的地址
a = 42; // ra的值现在也成42了```
由于这个原因(引用没有地址),声明引用的引用、指涉到引用的指针或引用的数组都是不合法的(尽管C++标准委员会已经在讨论至少在某些上下文环境里允许引用的引用)。
int &&rri = ra; // 错误!
int &*pri; // 错误!
int &ar[3]; // 错误!`
引用不可能带有常量性或挥发性,因为别名不能带有常量性或挥发性。尽管引用可以是某个带有常量性或挥发性的实体的引用。如果用关键字const或volatile来修饰引用,就会收到一个编译期错误:
int &const cri = a; // 错误!
const int &rci = a; // 没问题```
不过,比较诡异的是,如果把const或volatile饰词加在引用型别上面,并不会被C++语言判定为非法。编译器不会为此报错,而是简单地忽略这些饰词:
typedef int *PI;
typedef int &RI;
const PI p = 0; // p是常量指针
const RI r = a; // 没有常量引用,r就是个平凡的引用没有空引用,也没有型别为void的引用。
C *p = 0; // p是空指针
C &rC = *p; // 把引用绑定到空指针上,其结果未有定义
extern void &rv; // 试图声明型别为void的引用会引起编译期错误`
引用就是其不可变更的初始化物的别名,既然是别名,总得是“某个东西”的别名,这“某个东西”一定要实际存在才成。
不管怎样你都要记住,我可没说引用只能是简单变量名的别名。其实,任何能作为左值的(如果你不清楚什么是左值,请看常见错误6)复杂表达式都能作为引用的初始化物:
int &el = array[n-6][m-2];
el = el*n-3;
string &name = p->info[n].name;
if( name == "Joe" )
process( name );```
如果函数的返回值具有引用型别,这就意味着可以对该函数的返回值重新赋值。一个经常被津津乐道的典型例子是表示数组之抽象数据型别的索引函数(index function)16:
template
class Array {
public:
T &operator [](int i)
{ return a_[i]; }
const T &operator [](int i) const
{ return a_[i];}
// ...
private:
T a_[n];
};`
那个引用返回值能使对数组元素的赋值在语法上颇为自然了:
Arrayia;
ia[3] = ia[0];```
引用的另一个用途,就是可以让函数在其返回值之外多传递几个值:
Name *lookup( const string &id, Failure &reason );
// ...
string ident;
// ...
Failure reasonForFailure;
if( Name *n = lookup( ident, reasonForFailure ) ) {
// 查找成功则执行的例程
}
else {
// 如果查找失败,那么由reasonForFailure的值返回错误代号
}`
在对象身上实施目标型别为引用型别的强制型别转换操作的话,其效果与用非引用的相同型别进行的强制转换有着截然不同的效果:
char *cp = reinterpret_cast(a);
reinterpret_cast(a) = cp;```
在上述代码的第一行里,我们对一个int型变量实施了到指针型别强制型别转换(我们在这里使用了`reinterpret_cast`运算符,这好过使用形如“`(char *) a”`的旧式强制型别转换操作。要想知道这是出于何种考量,请看常见错误40)。这个操作的详细情况分解如下:一个`int`型变量的值被存储于一个副本中,并随即被按位当作指针型别来解释17。
而第二个强制型别转换操作则是完全另一番景象。转换成引用型别的强制型别转换操作的意义是把`int`型变量本身解释成指针型别,成为左值的是这个变量本身18,我们继而可以对它赋值。也许这个操作会引发一次核心转储(`dump core`,俗称“吐核”,也就是操作系统级的崩溃),不过那不是我们现在谈论的主题,再说,使用`reinterpret_cast`本身也就暗示着该操作没把可移植性纳入考量。和上述形式差不多的、没有转换成引用型别的强制型别转换操作的一次赋值尝试则会无可挽回地失败,因为这样的强制型别转换操作的结果是右值而不是左值19。
`
reinterpret_cast(a) = 0; // 错误! `
指涉到数组的引用保留了数组尺寸,而指针则不保留。
int ary[12];
int *pary = ary; // pary指涉到数组ary的第一个元素
int (&rary)[12] = ary; // rary是整个数组ary的引用
int ary23;
int (*pary2)[4] = ary2; // pary2指涉到数组ary2的第一个元素
int (&rary2)3 = ary2; // rary2是整个数组ary2的引用`
引用的这个性质有时在数组作为实参被传递给函数时有用(欲知详情,请看常见错误34)。
同样可以声明函数的引用:
int f( double );
int (* const pf)(double) = f; // pf是指涉到函数f()的常量指针
int (&rf)(double) = f; // rf是函数f()的引用```
指涉到函数的常量指针和函数的引用从编码实践角度来看,并无很大不同。除了一点,那就是指针可以显式地使用提领语法,而对引用是不能使用显式提领语法的,除非它被隐式转换成指涉到函数的指针20。
a = pf( 12.3 ); // 直接用函数指针名调用函数
a = (*pf)(12.3); // 使用提领语法也是可以的
a = rf( 12.3 ); // 通过引用调用函数
a = f( 12.3 ); // 直接调用函数本身
a = (*rf)(12.3); // 把引用(隐式)转换成指涉到函数的指针,再使用提领语法
a = (*f)(12.3); // 把函数本身(隐式)转换成指涉到函数的指针,再使用提领语法 `
请注意区别引用和指针。