1.1.
值与对象
为了理解引用,我们需要首先搞清楚什么叫
左值
与
右值
。
简而言之,
左值
是一种
对象
,而不是
值
。其关键区别在于,是否明确在内存中有其可访问的位置。
即,其是否存在一个可访问的地址。如果有,那么它就是一个
对象
,也就是一个
左值
,否则,它就只是
一个
值
,即
右值
。
比如:你不可能对整数
10
取地址,因而这个表达式是一个
右值
。但是如果你定义了一个变量:
int a = 10;
变量 a
则代表一个 对象 ,即 左值 。如果我们再进一步,表达式 a + 1
则是一个右值表达式,因为你无法对这个表达式取地址。
任何可以取地址的表达式,背后都必然存在一个 对象 ,因而也必然属于 左值 。而如果我们把对象地址看作其 身份证 ( Identifier ),
那么我们也可以换句话说:任何有 身份证 的表达式,都属于 左值 ;否则,肯定属于 右值 。
1.2. 别名
引用 是 对象 的 别名 。
所谓 别名 ,是指你没有创建任何 新事物 ,而只是对 已存在事物 赋予了另外一个名字。比如:
using Int = int;
你并没有创建一个叫做 Int
的新类型,而只是对已存在类型 int
赋予了另外一个名字。再比如:
template <typename T>
using ArrayType = Array<T, 10>;
你并没有创建一个签名为 ArrayType<T>
的新模版,而只是对已存在模版 Array<T,N>
进行部分实例化后得到的模版,赋予了一个新名字。
因而,引用 作为 对象别名 ,并没有创建任何 新对象 (包括引用自身),而仅仅是给已存在对象赋予了一个新名字。
1.3. 空间
正是因为其 别名 语义, C++ 没有规定 引用 的尺寸(事实上,从 别名 语义的角度,它本身不需要内存,因而也就没有尺寸而言)。
因而,如果你试图通过 sizeof
去获取一个 引用 的大小,是不可能的。你只能得到它所引用的对象的大小(由于别名语义)。
struct Foo {
std::size_t a;
std::size_t b;
Foo foo;
Foo& ref = foo;
static_assert(sizeof(ref) == sizeof(Foo));
也正是由于其 别名语义 ,当你试图对一个引用取地址时,你得到的是对象的地址。比如,在上面的例子中,
&ref
与 &foo
得到的结果是一样的。
因而,当你定义一个指针时,指针自身就是一个 对象 (左值);它本身有自己明确的存储,并可以取自己的地址,可以通过 sizeof
获取自己的尺寸。
但是 引用 ,本身不是一个像指针那样的额外对象,而是一个对象的别名, 你对引用进行的任何操作,都是其所绑定对象的操作 。
在上面的例子中,ref
与 foo
没有任何差别,都是对象的一个名字而已。它们本身都代表一个对象,都是一个左值表达式。
因而,在不必要时,编译器完全不需要为引用分配任何内存。
但是,当你需要在一个数据结构中保存一个引用,或者需要传递一个引用时,你事实上是在存储或传递对象的 身份 (即地址)。
虽然这并不意味着 sizeof(T&)
就是引用的大小(从语义上,引用自身非对象,因而无大小,sizeof(T&) == sizeof(T)
),但对象的地址的确
需要对应的空间来存储。
struct Bar {
Foo& foo;
// still, reference keeps its semantics.
static_assert(sizeof(Bar::foo) == sizeof(Foo));
// but its storage size is identical to a pointer
static_assert(sizeof(Bar) == sizeof(void*));
// interesting!!!
static_assert(sizeof(Bar) < sizeof(Bar::foo));
一个引用必须初始化。这是因为其 对象别名 语义,因而没有 绑定 到任何对象的引用,从语义上就不成立。
由于必须通过初始化将引用绑定到某一个对象,因而从语义上,不存在 空引用 的概念。这样的语义,对于我们的接口设计,有着很好的帮助:
如果一个参数,从约束上就不可能是空,那么就不要使用指针,而使用引用。这不仅可以让被调用方避免不必要的空指针判断;更重要的是准确的约束表达。
不过,需要特别注意的是:虽然 空引用 从概念上是不存在的,但从事实上是可构造的。比如: T& ref = *(T*)nullptr
。
因而,在项目中,任何时候,需要从指针转为引用时,都需要确保指针的非空性。
另外,空引用 本身这个概念就是不符合语义的,因为引用只是一个对象的别名。上面的表达式,事实上站在对象角度同样可以构造: T obj = *(T*)nullptr
。
正如我们将指针所指向的对象赋值(或者初始化)给另一个对象一样,我们都必须确保指针的非空性。
像所有的左值一样,引用可以绑定到一个抽象类型,或者不完备类型(而右值是不可能的)。从这一点上,指针和引用具有相同的性质。因而,在传递参数时,决定
使用指针,还是引用,仅仅受是否允许为空的设计约束。
一个引用不可能从一个对象,绑定到 另外 一个对象。原因很简单,依然由于其 对象别名 语义。它本身就代表它所绑定的对象,重新绑定另外一个对象,从概念上不通。
而引用的 不可更换性 ,导致任何存在引用类型非静态成员的对象,都不可能直接实现 拷贝/移动赋值 函数。
因而,标准库中,需要存储数据的,比如 容器 , tuple
, pair
, optional
等等结构,都不允许
存储 引用 。
这就会导致,当一个对象需要选择是通过 指针 还是 引用 来作为数据成员时,除了 非空性 之外,相对于参数传递,还多了一个约束: 可修改性 。
而这两个约束并不必然是一致的,甚至可以是冲突的。
比如,一个类的设计约束是,它必须引用另外一个对象(非空性),但是随后可以修改为引用另外一个对象。这种情况下,
使用指针就是唯一的选择。但代价是,必须通过其它手段来保证 非空性 约束。
1.5. 左值
任何一个引用类型的 变量 ,都必然是其所绑定 对象 的 别名 ,因而都必然是 左值 。无论这个引用类型是 左值引用 ,
还是 右值引用 。关于这个话题,我们会在后续章节继续讨论。
引用是对象的别名,对于引用的一切操作都是对对象的操作;
引用自身从概念上没有大小(或者就是对象的大小);但引用在传递或需要存储时,其传递或存储的大小为地址的大小。
引用必须初始化;
引用不可能重新绑定;
将指针所指向的对象绑定到一个引用时,需要确保指针非空。
任何引用类型的变量,都是左值。