引用是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; }
来源:https://www.cnblogs.com/rosefinch/p/12294378.html