[8] 引用
FAQs in section [8]:
- [8.1] 什么是引用?
- [8.2] 给引用赋值意味着什么?
- [8.3] 返回一个引用意味着什么?
- [8.4]
object.method1().method2()
是什么意思? - [8.5] 如何才能使一个引用指向另一个对象?
- [8.6] 何时该使用引用,何时该使用指针?
- [8.7] 什么是对象的句柄?它是指针吗?它是引用吗?它是指向指针的指针?它是什么?
8.1 什么是引用?
对象的别名(另一个名称)。
引用经常用于“按引用传递(pass-by-reference)”:
void swap(int& i, int& j)
{
int tmp = i;
i = j;
j = tmp;
}
int main()
{
int x, y;
_// ..._
swap(x,y);
}
此处的 i
和 j
分别是main中的 x
和 y
。换句话说,i
就是 x
—— 并非指向 x 的指针,也不是 x
的拷贝,而是 x
本身。对 i
的任何改变同样会影响 x
,反之亦然。
OK,这就是作为一个程序员所认知的引用。现在,给你一个不同的 角度,这可能会让你更糊涂,那就是引用是如何实现的。典型的情况下,对象 x
的引用 i
是 x
的机器地址。但是,当程序员写 i++
时,编译器产生增加 x
的代码。更详细的来说,编译器用来寻找 x
的地址位并没有被改变。C 程序员将此认为好像是 C 风格的按指针传递,只是句法不同 (1) 将 & 从调用者移到了被调用者处,(2)消除了*
s。换句话说,C 程序员会将 i
看作为宏 (*p)
,而 p 就是指向 x
的指针(例如,编译器自动地将潜在的指针解除引用;i++
被改变为 (*p)++
;i = 7
被自动地转变成 *p = 7
)。
很重要:请不要将引用看作为指向一个对象的奇异指针,即使引用经常是用汇编语言下的地址来实现的。引用就是对象。不是指向对象的指针,也不是对象的拷贝,就是对象。
8.2 给引用赋值,意味着什么?
改变引用的“指示物”(引用所指的对象)。
请记住: 引用就是它的指示物,所以当改变引用的值时,也会改变其指示物的值。以编译器编写者的行话来说,引用是一个“左值”(它可以出现在赋值运算符左边)。
8.3 返回一个引用,意味着什么?
意味着该函数调用可以出现在赋值运算符的左边。
最初这种能力看起来有些古怪。例如,没有人会认为表达式 f() = 7
有意义。然而,如果 a 是一个 Array 类,大多数人会认为 a[i] = 7
有意义,即使 a[i]
实际上是一个函数调用的伪装(它调用了 如下的 Array 类的 Array::operator[](int)
)。
class Array {
public:
int size() const;
float& operator[] (int index);
_// ..._
};
int main()
{
Array a;
for (int i = 0; i < a.size(); ++i)
a[i] = 7; // 这行调用了 Array::operator[](int)
}
8.4 object.method1().method2()
是什么意思?
连接这些方法的调用,因此被称为方法链
第一个被执行的是 object.method1()
。它返回对象,可能是对象的引用(如,method1()
可能以 return *this
结束),或可能是一些其他对象。我们姑且把返回的对象称为objectB
。然后objectB
成为method2()
的this
对象。
方法链最常用的地方是iostream
库。例如,cout << x << y
可以执行因为 cout << x
是一个返回cout
.的函数
虽然使用的较少,但仍然要熟练掌握的是在命名参数法(Named Parameter Idiom)中使用方法链。
8.5 如何能够使一个引用重新指向另一个对象?
不行。
你无法让引用与其指示物分离。
和指针不同,一旦引用和对象绑定,它无法再被重新指向其他对象。引用本身不是一个对象(它没有标识; 当试图获得引用的地址时,你将的到它的指示物的地址;记住:引用就是它的指示物 )。
从某种意义上来说,引用类似 int* const p
这样的const指针(并非如 const int* p
这样的指向常量的指针)。不管有多么类似,请不要混淆引用和指针;它们完全不同。
8.6 何时该使用引用, 何时该使用指针?
尽可能使用引用,不得已时使用指针。
当你不需要“重新指向(reseating)”时,引用一般优先于指针被选用。这通常意味着引用用于类的公有接口时更有用。引用出现的典型场合是对象的表面,而指针用于对象内部。
上述的例外情况是函数的参数或返回值需要一个“临界”的引用时。这时通常最好返回/获取一个指针,并使用 NULL 指针来完成这个特殊的使命。(引用应该总是对象的别名,而不是被解除引用的 NULL 指针)。
注意:由于在调用者的代码处,无法提供清晰的的引用语义,所以传统的 C 程序员有时并不喜欢引用。然而,当有了一些 C++ 经验后,你会很快认识到这是信息隐藏的一种形式,它是有益的而不是有害的。就如同,程序员应该针对要解决的问题写代码,而不是机器本身。
8.7 什么是对象的句柄?它是指针吗?它是引用吗?它是指向指针的指针?它是什么?
句柄术语一般用来指获取另一个对象的方法——一个广义的假指针。这个术语是(故意的)含糊不清的。
含糊不清在实际中的某些情况下是有用的。例如,在早期设计时,你可能不准备用句柄来表示。你可能不确定是否将一个简单的指针或者引用或者指向指针的指针或者指向引用的指针或者整型标识符放在一个数组或者字符串(或其它键)以便能够以哈希表(hash-table)(或其他数据结构)或数据库键或者一些其它的技巧来查询。如果你只知道你会需要一些唯一标识的东西来获取对象,那么这些东西就被称为句柄。
因此,如果你的最终目标是要让代码唯一的标识/查询一个Fred类的指定的对象的话,你需要传递一个Fred句柄这些代码。句柄可以是一个能被作为众所周知的查询表中的键(key)来使用的字符串(比如,在std::map<std::string,Fred>
或 std::map<std::string,Fred*>
中的键),或者它可以是一个作为数组中的索引的整数(比如,Fred* array = new Fred[maxNumFreds]
),或者它可以是一个简单的 Fred*,或者它可以是其它的一些东西。
初学者常常考虑指针,但实际上使用未初始化的指针有底层的风险。例如,如果Fred对象需要移动怎么办?当Fred对象可以被安全删除时我们如何获知?如果Fred对象需要(临时的)连续的从磁盘获得怎么办?等等。这些时候的大多数,我们增加一个间接层来管理位置。例如,句柄可以是Fred*,指向Fred的指针可以保证不会被移动。当Fred对象需要移动时,你只要更新指向Fred*的指针就可以了。或者让用一个整数作为句柄,然后在表或数组或其他地方查询Fred的对象(或者指向Fred对象的指针)。
重点是当我们不知道要做的事情的细节时,使用句柄。
使用句柄的另一个时机是想要将已经完成的东西含糊化的时候(有时用术语magic cookie也一样,就像这样,“软件传递一个magic cookie来唯一标识并定位适当的Fred对象”)。将已经完成的东西含糊化的原因是使得句柄的特殊细节或表示物改变时所产生的连锁反应最小化。举例来说,当将一个句柄从用来在表中查询的字符串变为在数组中查询的整数时,我们可不想更新大量的代码。
当句柄的细节或表示物改变时,维护工作更为简单(或者说阅读和书写代码更容易),因此常常将句柄封装到类中。这样的类常重载operator->
和 operator*
算符(既然句柄的效果象指针,那么它可能看起来也象指针)。