前言
(左值)引用作为指针的非完全替代品,不仅降低了用户的编写难度,又由于其直接作为别名的特点,不用申请新空间去保存由于赋值、函数返回等引起的不必要的拷贝中产生的临时变量,而提升了效率。
引言:如何区分左值和右值
①左值
左值是一个表示数据的表达式(如变量名或解引用的指针),我们可以获取它的地址 可以对它赋值,左值可以出现赋值符号的左边,右值不能出现在赋值符号左边。定义时const修饰符后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
代码语言:javascript复制int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
②右值
右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引用返回)等等,右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能取地址。右值引用就是对右值的引用,给右值取别名。
代码语言:javascript复制int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x y = 1;
fmin(x, y) = 1;
return 0;
}
一、左值引用
注:本文不重点探讨左值引用的特点与用法
1、左值引用的短板
在前言中,我们提到了(左值)引用,可以直接给变量取别名来达到减少拷贝提高效率的作用。但是某些变量受限于其本身的生命周期/作用域的原因(出作用域销毁),左值(引用)就无能为力了。 例如:
代码语言:javascript复制template<class T>
T func(const T& x)
{
T ret;
// ...
return ret;
//由于ret是在函数内部定义,出了函数域将会销毁,所以不能返回左值引用
}
当然我们可以使用输出性参数来解决这个问题.
代码语言:javascript复制template<class T>
T& func(const T& x,T& ret)
{
// ...
return ret;
//由于ret是在函数内部定义,出了函数域将会销毁,所以不能返回左值引用
}
二、右值引用
1、右值引用使用场景和意义
①移动返回
注:当需要用右值引用引用一个左值时,可以通过move函数将左值转化为右值。C 11中,std::move()函数位于头文件中,该函数名字具有迷惑性,它并不搬移任何东西,唯一的功能就是将一个左值强制转化为右值引用,然后实现移动语义。
代码语言:javascript复制int&& rvalueRefFunc() {
int a = 1;
return std::move(a);//a返回后出了函数域被销毁
}
int main() {
// 利用右值引用和move,将a的资源给ref
int&& ref = rvalueRefFunc();
std::cout << ref << std::endl; // 输出 1
return 0;
}
② 移动构造
注:我们模拟一个String类的行为,来举例子 在string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占位已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。
代码语言:javascript复制// 移动构造
string(string&& s)
:_str(nullptr)
,_size(0)
,_capacity(0)
{
cout << "string(string&& s) -- 移动语义" << endl;
swap(s);
}
再运行上面bit::to_string的两个调用,我们会发现,这里没有调用深拷贝的拷贝构造,而是调用了移动构造,移动构造中没有新开空间,拷贝数据,所以效率提高了。
③移动赋值
注:我们模拟一个String类的行为,来举例子
代码语言:javascript复制// 移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动语义" << endl;
swap(s);
return *this;
}
一个右值引用神奇的特性(不是右值引用的重点使用场景)
先来看一段代码:
代码语言:javascript复制int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x y;
rr1 = 20; //rr1是右值引用但是能修改!!
//(修改的是初始化的"常量"10吗?如果不是那是修改的那个变量?)
rr2 = 5.5; // 报错
return 0;
}
右值是不能取地址且不可修改的,但是给右值取别名后,会导致 右值被存储到特定位置,且可以取到该位置的地址 即变成左值了,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用,是不是感觉很神奇。
三、完美转发
在模板编程时,由于泛型,在未来使用该模板类时,我们不知道引用是右值还是左值引用,再加上左右值引用不能直接复用的原因。就导致了我们无法利用同一个模板既能接收左值引用又能接收右值引用,此时完美转化就出来了。
代码语言:javascript复制void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }
void Fun(int &&x){ cout << "右值引用" << endl; }
void Fun(const int &&x){ cout << "const 右值引用" << endl; }
// 模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
// 模板的万能引用只是提供了能够接收同时接收左值引用和右值引用的能力,
// 但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,
// 我们希望能够在传递过程中保持它的左值或者右值的属性, 就需要用我们下面学习的完美转发
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}