《C++覆辙录》——1.5:对引用的认识误区

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
   { return a_[i]; }
  const T &operator 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 ary2[3][4];
int (*pary2)[4] = ary2; // pary2指涉到数组ary2的第一个元素
int (&rary2)[3][4] = 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); // 把函数本身(隐式)转换成指涉到函数的指针,再使用提领语法 ```
请注意区别引用和指针。

时间: 2024-12-02 11:51:31

《C++覆辙录》——1.5:对引用的认识误区的相关文章

《C++覆辙录》——常见错误1:过分积极的注释

第1章 基础问题 C++覆辙录 说一个问题是基础的,并不就是说它不是严重的或不是普遍存在的.事实上,本章所讨论的基础问题的共同特点比起在以后章节讨论的技术复杂度而言,可能更侧重于使人警醒.这里讨论的问题,由于它们的基础性,在某种程度上可以说它们普遍存在于几乎所有的C++代码中. 常见错误1:过分积极的注释 很多注释都是画蛇添足,它们只会让源代码更难读,更难维护,并经常把维护工程师引入歧途.考虑下面的简单语句: a = b; // 将b赋值给a 这个注释难道比代码本身更能说明这个语句的意义吗?因而

《C++覆辙录》——导读

前言 C++覆辙录 本书之渊薮乃是近20年的小小挫折.大错特错.不眠之夜和在键盘的敲击中不觉而过的无数周末.里面收集了普遍的.严重的或有意思的C++常见错误,共计九十有九.其中的大多数,(实在惭愧地说)都是我个人曾经犯过的. 术语"gotcha"1有其云谲波诡的形成历史和汗牛充栋的不同定义.但在本书中,我们将它定义为C++范畴里既普遍存在又能加以防范的编码和设计问题.这些常见错误涵盖了从无关大局的语法困扰,到基础层面上的设计瑕疵,再到源自内心的离经叛道等诸方面. 大约10年前,我开始在

《C++覆辙录》——第2章 语法问题2.1:数组定义和值初始化的语法形式混淆

第2章 语法问题 C++覆辙录C++语言的语法和词法结构博大精深.此复杂性的一部分是从C语言那里继承而来的,另一部分则是为支撑某些特定的语言特性所要求的. 本章中我们将考察一组语法相关的头疼问题.其中有些属于常见的手误,但是错误的代码仍然能够通过编译,只不过会以出人意料的方式运行罢了.另外一些则是由于一段代码的语法结构及它们的运行期行为不再互为表里.其余的部分,我们主要研究语法层面的灵活余地带来的问题:明明是一字不差的代码,不同的软件工程师能从中得出大相径庭的结论来. 2.1:数组定义和值初始化

《OOD启思录》—本书中引用到的其他图书

本书中引用到的其他图书OOD启思录 本文仅用于学习和交流目的,不代表异步社区观点.非商业转载请注明作译者.出处,并保留本文的原始链接.

《C++覆辙录》——2.9:自反初始化

2.9:自反初始化 在以下的代码里,var的值变成了多少? int var = 12; { double var = var; // ... }``` 未有定义.C++语言中,某个名字在它的初始化对象被解析到之前就进入了其辖域的话,在初始化对象引用到这个名字时,它引用到的不是别的,正是这个刚刚被声明的对象.没有几个软件工程师会写出像上面这么莫名其妙的声明代码,但也许复制.粘贴的手法会让你陷入困境: int copy = 12; // 某深藏不露的变量// ...int y = (3x+2copy

《C++覆辙录》——1.2:幻数

2:幻数 幻数,用在这里时其含义是上下文里出现的裸字面常量(raw numeric literal),本来它们应该是具名常量(named constant)才对: class Portfolio { // ... Contract *contracts_[10]; char id_[10]; };``` 幻数带来的主要问题是它们没有(抽象)语义,它们只是个量罢了.一个"10"就是一个"10",你看不出它的意思是"合同的数量"或是"标识符

《C++覆辙录》——1.6:对常量(性)的认识误区

.6:对常量(性)的认识误区 在C++中的常量性概念是平凡的,但是这和我们对const先入为主的理解不太符合.首先我们要特别注意以const饰词修饰的变量声明和字面常量的区别: int i = 12; const int ci = 12;``` 字面常量12不是C++概念中的常量.它是个字面常量.字面常量没有地址,永远不可能改变其值.i是个对象,有自己的地址,其值可变.用const关键字修饰声明的ci也是个对象,有自己的地址,尽管在本例中其值不可变. 我们说i和`ci`可以作为左值使用,而字面常

《C++覆辙录》——1.8:未能区分可访问性和可见性

1.8:未能区分可访问性和可见性 C++语言压根儿没有实现什么数据隐藏,它实现了的是访问层级.在class中具有protected和private访问层级并非不可见,只是不能访问罢了.如同一切可见而不可及的事物一样(经理的形象跃入脑海),他们总是惹出各种麻烦. 最显而易见的问题就是即使是class的实现仅仅更改了一些貌似不可见的部分,也会带来必须重新编译代码的苦果.考虑一个简单的class,我们为其添加一个新的数据成员: class C { public: C( int val ) : a_(

《C++覆辙录》——1.9:使用糟糕的语言

1.9:使用糟糕的语言 当一个更大的世界入侵了C++社群原本悠然自得的乐土之时,它们带来了一些足堪天谴的语言和编码实践.本节乃是为了厘清返璞归真的C++语言所使用的正确适当.堪称典范之用语和行为. 用语 表1-1列出了最常见的用语错误,以及它们对应的正确形式. 表1-1 常见用语错误及其对应正确用语 没有什么所谓"纯虚基类".纯虚函数是有的,而包含有或是未能改写(override)此种函数的类,我们并不叫它"纯虚基类",而是叫它"抽象类". C+