引用
由于这篇文章并不是引用的教程,因此并不会对引用的语法及使用做很深的讲解。
在C++中将一个对象赋值给一个引用,就是给它起别名。
例如
1 | int a = 0; |
在这段代码中,a是一个int类型的对象,b是对象a的另一个名字。对b的任何操作和对a做同样操作没有任何区别。
但是b可以有相对a的更多的cv修饰符
C++为什么要引入引用
在1984年,C++引入了运算符重载,使得我们可以自定义运算符的行为。很不幸,这只是一个语法糖,例如一个二元运算符op,我们有如下语句:
1 | C = A op B; |
根据A的类型,以及是否重载了operator op,它会被转换为
1 | C = A.operator op(B); |
或者
1 | C = operator op(A, B); |
更为糟糕的是,在C++中,参数是以拷贝的方式传递的,这样我们就不得不将B(或者A和B)拷贝一份,因此较大的对象会有着较高的拷贝成本,以及参数传递过程中可能会出现对构造函数以及析构函数的调用,这大大降低了程序的运行效率,也使得程序的语义发生混乱,因此这里需要一种解决方案。
如果通过指针传递参数和返回值,那么我们确实可以避免对参数的调用,但会导致另一个问题:
1 | // 我们可能会写出这样的代码 |
那么在调用的时候呢?
1 | z = operator +(&x, &y); |
等价于
1 | z = &x + &y; |
但这与我们的直觉并不相符,这看上去是将x的地址和y的地址相加,而不是将x的值与y的值相加。
如果使
1 | z = x + y; |
等价于
1 | z = operator(&x, &y); |
看似很好,但并不是所有的参数都可以取地址的,例如参数是个整数字面量
1 | a = b + 1; |
这会被转换成
1 | a = operator +(&b, &1); |
很可惜,我们无法取得字面量1的地址。
我们需要一种新的机制使函数可以使用传值的语法传递一个对象等,并且不对其执行拷贝操作。引用便出现了。
语法
对于类型T,T的引用类型是T&(左值引用)或T&&(右值引用),这里先统一用T&表示两者。
类型
T可以是任意指针类型,也可以拥有任意个数的cv修饰符。
T&是区别与T的单独的类型。
引用是最顶层的类型,没有引用的引用(T& &),引用的指针(T & *),加在引用类型的cv修饰符会被忽略。
1 | typedef T int; |
初始化
引用类型可以直接使用对应类型T或T的派生类类型的对象直接初始化,此时会给目标对象起别名。
可以使用T类型的直接初始化、复制初始化、直接列表初始化、复制列表初始化初始化一个T类型的引用,就像没有引用修饰一样。 此时引用不再是指针的语法糖
C++11中加入了右值引用。区别于左值引用,右值引用在使用目标对象进行初始化时,目标对象必须是右值。相应的,左值引用在使用目标对象进行初始化时,目标对象必须是左值。
特别的,有且仅拥有const修饰的T类型的左值引用(即const T&)既可以使用左值对象进行初始化,也可以使用右值对象进行初始化。
注意这里不能有volatile修饰符,右值引用的const版本并没有什么特别的性质
引用可以延长临时对象的生命周期,当使用一个T类型的临时对象初始化const T&或T&&类型(后者的T可以有cv限定)的引用时,会直接延长该临时对象的生命周期。
函数重载
C++中形参可以时引用类型,可以避免对对象进行拷贝操作
看如下代码
1 | typedef int T; |
我们知道如下调用
1 | void test1() { |
会调用重载2
但是下面的代码呢?
1 | void test2(T&& t) { |
这会调用重载1,因为引用在初始化后在语法上与变量无异,作为一个表达式的值时会得到一个左值表达式。
decltype一个引用的名称会得到对应的引用类型。
和指针的区别
大多数情况下,引用的实现与指针相同,但是当不以同类型目标对象进行初始化且不作为形参时,引用类型T&的行为和对应类型T相同(但语义不同)
如果把指针比作软链接,那么引用就是对对象的硬链接。指针可以不指向一个对象,但引用一定指向一个对象(程序谬构除外)