在C++中引用是什么?

引用

由于这篇文章并不是引用的教程,因此并不会对引用的语法及使用做很深的讲解。

在C++中将一个对象赋值给一个引用,就是给它起别名。

例如

1
2
3
4
int a = 0;
int& b = a;
b = 1;
assert(a==1);

在这段代码中,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
2
3
4
5
6
7
8
9
10
// 我们可能会写出这样的代码
struct Vector_2 {
double x,y;
};
Vector_2 operator+(Vector_2* lhs, Vector_2* rhs) {
Vector_2 result;
result.x = lhs->x + rhs->x;
result.y = lhs->y + rhs->y;
return result;
}

那么在调用的时候呢?

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
2
3
4
5
typedef T int;
typedef T& Ref;
typedef Ref& RefRef; // 发生引用折叠,RefRef等同于T&
typedef Ref *RefPoint; // 编译错误
static_assert(std::is_same_v<const Ref, const T&>); // 断言失败,const Ref中的const被忽略

初始化

引用类型可以直接使用对应类型T或T的派生类类型的对象直接初始化,此时会给目标对象起别名。

可以使用T类型的直接初始化、复制初始化、直接列表初始化、复制列表初始化初始化一个T类型的引用,就像没有引用修饰一样。 此时引用不再是指针的语法糖

C++11中加入了右值引用。区别于左值引用,右值引用在使用目标对象进行初始化时,目标对象必须是右值。相应的,左值引用在使用目标对象进行初始化时,目标对象必须是左值。

特别的,有且仅拥有const修饰的T类型的左值引用(即const T&)既可以使用左值对象进行初始化,也可以使用右值对象进行初始化。

注意这里不能有volatile修饰符,右值引用的const版本并没有什么特别的性质

引用可以延长临时对象的生命周期,当使用一个T类型的临时对象初始化const T&或T&&类型(后者的T可以有cv限定)的引用时,会直接延长该临时对象的生命周期。

函数重载

C++中形参可以时引用类型,可以避免对对象进行拷贝操作

看如下代码

1
2
3
4
5
6
7
typedef int T;
void callRef(T&) { // 重载1
cout << "lref" << endl;
}
void callRef(T&&) { // 重载2
cout << "rref" << endl;
}

我们知道如下调用

1
2
3
4
void test1() {
T t;
callRef(static_cast<T&&>(t));
}

会调用重载2

但是下面的代码呢?

1
2
3
void test2(T&& t) {
callRef(t);
}

这会调用重载1,因为引用在初始化后在语法上与变量无异,作为一个表达式的值时会得到一个左值表达式。

decltype一个引用的名称会得到对应的引用类型。

和指针的区别

大多数情况下,引用的实现与指针相同,但是当不以同类型目标对象进行初始化且不作为形参时,引用类型T&的行为和对应类型T相同(但语义不同)

如果把指针比作软链接,那么引用就是对对象的硬链接。指针可以不指向一个对象,但引用一定指向一个对象(程序谬构除外)

文章目录
  1. 1. 引用
  2. 2. C++为什么要引入引用
  3. 3. 语法
    1. 3.1. 类型
    2. 3.2. 初始化
      1. 3.2.1. 函数重载
    3. 3.3. 和指针的区别
|