1 引用
引用是一种更安全的“指针”,引用的底层是以指针的方式实现的。针对引用使用的地方,系统会自带解引用的过程。同时它也占据系统内存空间。
引用:必须初始化、不能二次定义引用变量。
int a = 5;
int& b ; //error!没有初始化
int& b = 3; //true
int &a = 1;
int &a = 2; //error!多次重定义
int a = 10; //这里的a是左值,10是右值
2 左值引用、右值引用
首先了解左值、右值的概念:
- 左值:能用取地址
&
运算符获取对象的内存地址,表达式结束后依然存在的持久化内存对象,左值可出现在等式左边,也可出现在等式右边。 - 右值:不能用取地址
&
符号说明的对象,只能存在于等式右边,使用后会自动消除。
C++98之前能够使用的引用是左值引用,右值引用是C++11提出的概念,常用于代码优化中使用,赋值号左边的参数我们称呼为左值,右边的参数为右值。
同时定义:
左值引用:Type& 引用名 = 左值表达式;
右值引用:Type&& 引用名 = 右值表达式;
如:
int i = 500;
int& r = i; //正确的左值引用
int& r = 500; //错误,500是一个右值
int&& r = 500; //正确的右值引用
int&& r = 500 * i ;//正确的右值引用
右值引用主要实现转移语义
和完美转发
。
2.1 转移语义
移动语义
出现之前主要是拷贝语义
。
拷贝语义:对象的移动依靠内存拷贝实现,为新对象分配内存空间,拷贝旧对象进内存空间,删除旧对象。
移动语义:转移旧对象所有权到新对象,避免旧对象的复制和释放
。
因此移动语义可以极大提高效率。
//基于 “拷贝语义” 的 swap 函数
template <typename T>
void swap(T &a, T &b){
T tmp = a; //拷贝构造:将 a 拷贝至 tmp
a = b; //拷贝赋值:将 b 拷贝至 a
b = tmp; //拷贝赋值:将 tmp 拷贝至 b
}
//基于 “移动语义” 的 swap 函数
template <typename T>
void swap(T &&a, T &&b){
T tmp(std::move(a)); //移动构造:将 a 的所有权转让给局部对象 tmp
a = std::move(b); //移动赋值:将 b 的所有权转让给 a
b = std::move(tmp); //移动赋值:将 tmp 的所有权转让给 b
}
std::move 标准库函数可以接受一个左值并返回该左值的右值引用,因此方法二比方法一少了三次拷贝数据过程,极大提高swap函数的效率。
2.2 完美转发
完美转发:将函数参数传递给另一个参数时,参数的类型、值以及所有属性都不能改变,这在泛型编程当中有着广泛的应用。在 C++ 中,函数参数除了类型和值以外,还有 const 和 non-const 以及 左值和右值 两组属性。
通俗理解就是一个函数可以实现多种功能,减少函数的重写和重载,提高泛用性。
比如:
//代码 1
template <typename T> void f1(T &t){
cout << "hello world" << endl;
}
template <typename T> void f1(const T &t){
cout << "hello world" << endl;
}
int main(void){
int i = 10;
const int ci = 11;
f1(i); //调用 f1(int&),模板参数 T 是 int
f1(ci); //调用 f1(const int&),模板参数 T 是 int
f1(31); //调用 f1(const int&),模板参数 T 是 int
return 0;
}
若实现f1不同参数传入,需要重载函数f1,如参数增加会提高维护量。
//代码 2
template <typename T> void f1(T &&t){
cout << "hello world" << endl;
}
int main(void){
int i = 10;
const int ci = 11;
f1(i); //实参是一个左值,模板参数 T 是 int&
f1(ci); //实参是一个左值,模板参数 T 是 const int&
f1(31); //实参是一个右值,模板参数 T 是 int
return 0;
}
模板函数C++11在原有绑定规则新增了引用折叠规则:
- 若模板函数 func 的形参类型为 T&, 则不论传递给 func 的实参是左值还是右值,一律统统折叠为左值引用
- 若模板函数 func 的形参类型为 T&&, 则传递给函数的左值实参折叠为左值引用,右值实参折叠为右值引用
因此在定义函数模板时使用右值引用做形参,能够保证参数的属性(const 属性和 左/右值属性)不发生变化。极大的减少了工作量。
3 常引用
const 类型& 变量名;
引用绑定一个变量后就不能更改,不能解绑。const则限定了引用类型。
通俗来说使得引用的对象不能修改,不能对引用值修改,保证了引用变量的安全性。
如下例子:
int a = 5;
const int& b = a;
b = 2; //error!不能对常引用类型b进行修改,提示表达式必须为可修改左值
这里的a
是被常引用类型绑定的,可以这样理解,int& b是一个地址,添加const修饰符之后这个地址变成了const类型
。保证了变量的只读,在函数设计中常采用常引用传参
,返回值为引用
的方法,因为若传入的是形参
,函数体内部会对形参产生一个临时内存地址,所以对形参的改变实际是对另一个临时变量的修改,不会影响到实参。但这种处理方式会让函数运行更缓慢,涉及到内存的创建和销毁。
因此若函数传入参数并不涉及影响到传参值时,使用常引用作为函数输入能够更高效
。
4 参考
[1] C++ 引用类型 — 左值引用、常引用和右值引用
[2] C++中常引用的作用和区别
[3] C++引用及常引用
[4] 左值引用和右值引用