引用是C++引入的新类型,是对一块内存空间起的一个别名,主要分为左值引用、常量左值引用和右值引用三种。C++语言标准规定,一个引用不是左值引用就是右值引用。其中,函数引用是一种特殊的左值常量引用;万能引用(universal reference)是一种特殊的引用类型,既可以表示左值引用,也可以表示右值引用,具体的引用类型最终会由编译器决定,判断依据是引用折叠(reference collasping)。
一、左值引用
一句话总结:左值引用是一级指针的语法糖。只有左值才能绑定到左值引用上。
int &a = 0; // a是int*的语法糖。
int *b = nullptr;
int *&b_ref = b; // b_ref是int**的语法糖。
大量的资料表示,编译器中的引用是以指针实现的。然而,左值引用必须要初始化后才能使用,否则会引发编译错误(这与指针不同,野指针或者空指针即便不初始化也可以通过编译),所以可以这样理解:如果代码通过了编译,那么引用的对象一定是可用的。然而,凡事有利必有弊,这样的特性也会导致问题。最典型的问题是:引用无法表示空值。例如,有些对象的成员并不是必需的,在复制文件时并不一定需要提供进度通知,应该由用户自行决定,而不是强制要求提供:
class file_copier
{
progress& _progress;
//...
public:
file_copier(progress &progress, /*....*/) : _progress(progress), /*...*/ {}
};
此时,为了构造file_copier
对象,必须提供一个非空的progress对象来进行进度通知,但是当用户不需要进度通知功能时怎么办呢?只好指定一个特殊的progress对象,表示空值;与其如此,为什么不使用指针呢:
class file_copier
{
progress* _progress;
//...
public:
file_copier(progress *progress, /*....*/) : _progress(progress), /*...*/ {}
};
int main(int argc, char *argv[])
{
file_copier fp(nullptr, /*...*/); // 使用空指针,表示用户不需要进度提示。
return 0;
}
所以,左值引用这颗糖是否甜,取决于实际情况。在这里也总结一下左值引用和指针的区别:
- 指针有自己独立的内存空间,而引用没有。
- sizeof(…)运算的结果不同:指针的大小平台相关,而引用则是被引用对象的大小。
- 指针可以被初始化为nullptr,而引用必须被初始化,且不为空。
- 指针可以改变指向,但是引用不能。
- 可以存在多级指针,但是不存在多级引用,也不存在引用的数组。
- 指针和引用的++运算含义不同:指针表示步进,而引用表示调用对象的operator++运算符。
- 只能使用指针进行动态内存分配,也无法对引用使用delete。
二、常量左值引用
左值和右值都可以绑定到常量左值引用上,这是因为常量左值引用可以保证部分右值的不可修改属性。一句话总结:常量左值引用是具有底层const的一级指针的语法糖,同时也可以绑定到右值上。
void test_bind_to_left_ref(const int &cref)
{
}
void test_bind_to_right_ref(const int &cref)
{
}
int main(int argc, char *argv[])
{
int a = 0;
const int &cref = a; // 常量左值引用绑定到左值上。
const int &cref = std::move(a); // 常量左值引用绑定到右值上。
// std::move(...)将参数转为右值。
test_bind_to_left_ref(a); // 常量左值引用绑定到左值上。
test_bind_to_right_ref(std::move(a)); // 常量左值引用绑定到右值上。
return 0;
}
函数引用是一种特殊的常量左值引用,它没有使用const
修饰,但同样具有常量语义:
double add(double x, double y) { return x + y; }
double sub(double x, double y) { return x - y; }
int main(int argc, char *argv[])
{
double(&func_ref)(double, double) = add; // 将函数引用绑定到函数上。
func_ref(10, 20); // add(10, 20);
// 然而,func_ref的值不能改变。例如:
func_ref = sub; // 编译错误,因为func_ref是常量左值引用。
return 0;
}
其实函数引用需要与函数类型声明、函数指针区别一下的,因为这三种类型都很常见:
double add(double x, double y) { return x + y; }
double sub(double x, double y) { return x - y; }
// 函数声明(c++11):
using func_decl = double(double, double);
func_decl mutiply; // 等价于:double mutiply(double x, double y);
double mutiply(double x, double y) { return x * y; } // 函数实现。
int main(int argc, char *argv[])
{
double(&func_ref)(double, double) = add; // 将函数引用绑定到函数上。
func_ref(10, 20); // add(10, 20);
func_ref = sub; // 编译错误,因为func_ref是常量左值引用。
double(*func_ptr)(double, double) = add; // 将函数指针指向add函数。
func_ptr(10, 20); // add(10, 20);
(*func_ptr)(10, 20); // add(10, 20);
func_ptr = sub; // 正确,函数指针可以改变指向。
return 0;
}
三、右值引用
右值引用只能绑定到右值上,主要目的是:
- 为了延长临时变量的生命周期,从而节约性能。
- 为了方便代码书写。
例如下面的例子:
#include <vector>
#include <initializer_list>
void func1(std::vector<int> &vec)
{
// ...
}
void func2(std::vector<int> vec)
{
// ...
}
void func3(const std::vector<int> &vec)
{
// ...
}
void func4(std::vector<int> &&vec)
{
// ...
}
int main(int argc, char *argv[])
{
func1({1, 2, 3, 4}); // 错误,因为字面量{1, 2, 3, 4}是右值,不能绑定到左值引用上。
func2({1, 2, 3, 4}); // 正确,按值传递,但是会导致容器的复制,浪费资源。
func3({1, 2, 3 ,4}); // 正确,右值可以绑定到常量左值引用上,但是无法修改容器中的值。
{
std::initializer_list<int> lst = {1, 2, 3, 4};
func1(lst); // 正确,左值lst可以绑定到左值引用上,可以修改容器中的值,但是书写麻烦。
}
func4({1, 2, 3 ,4}); // 完美,右值可以绑定到右值引用上,书写简单,
// 不会导致容器复制,且可以修改容器中的值。
return 0;
}
四、万能引用
万能引用既可以绑定到左值,也可以绑定到右值,它出现在自动类型推断的场合,包括模板、auto等:
template<typename T>
void func(T&& t) {} // t既可以绑定到左值,也可以绑定到右值。
int main(int argc, char *argv[])
{
auto&& a = /* ... */; // a既可以绑定到左值,也可以绑定到右值。
return 0;
}
如何判断右值引用究竟是左值还是右值呢?这需要使用引用折叠的概念:当模板或者自动类型推断实例化时,可能会推导出三个(或四个)引用符号,编译器会自动将这三个(或四个)引用符号合并为一个(或两个):
template<typename T>
void func(T&& t) {}
int main(int argc, char *argv[])
{
int a = 0;
int &ra = a;
func(ra); // 此时,由于T是&&,并且ra是&,最终会推导出int&&&的结果,自动合并为&(左值引用)。
func(std::move(ra)); // 此时,由于T是&&,并且ra是&&,
// 最终会推导出int&&&&的结果,自动合并为&&(右值引用)。
auto &&b = ra; // 此时,由于b是&&,并且ra是&,最终会推导出int&&&的结果,自动合并为&(左值引用)。
auto &&c = std::move(ra); // 此时,由于T是&&,并且ra是&&,
// 最终会推导出int&&&&的结果,自动合并为&&(右值引用)。
return 0;
}