左值引用与右值引用详解
一、左值和右值
1.左值
左值是一个表示数据的表达式,比如:变量名、解引用的指针变量。
一般地,我们可以获取它的地址和对它赋值, 定义时const修饰符后的左 值,不能给他赋值,但是可以取它的地址。
总体而言,可以取地址的对象就是左值。
int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
}
2.右值
右值也是一个表示数据的表达式,比如:字面常量、表达式返回值,传值返回函数的返回值(是传值返回,而非传引用返回),右值不能出现在赋值符号的左边且不能取地址。
右值可以出现在赋值符号的右边,但是不能出现出现在赋值符号的左边,右值不能 取地址。右值引用就是对右值的引用,给右值取别名
总体而言,不可以取地址的对象就是右值。
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
}
二、左值引用和右值引用
左值引用与右值引用是C++11中出现的新语法 , 无论左值引用还是右值引用,都是给对象取别名。
1.左值引用
左值引用就是对左值的引用,给左值取别名。
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;
}
2.右值引用
右值引用就是对右值的引用,给右值取别名。
右值引用的表示是在具体的变量类型名称后加两个 &,比如:
int&& rr = x+y;
。
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;
}
三、左值引用与右值引用对比
左值引用:
左值引用只能引用左值,不能引用右值
但const修饰的左值引用可以引用左值, 也可以引用右值.
这个在C++98中解释为, 右值是临时对象, 临时对象具有常性, 因此要引用要加const修饰.
int main()
{
// 左值引用只能引用左值,不能引用右值。
int a = 10;
int& ra1 = a;
// ra为a的别名
//int& ra2 = 10; // 编译失败,因为10是右值
// const左值引用既可引用左值,也可引用右值。
const int& ra3 = 10;
const int& ra4 = a;
return 0;
}
右值引用:
右值引用只能引用右值, 不能引用左值.
但右值引用可以引用move以后的左值
int main()
{
// 右值引用只能右值,不能引用左值。
int&& r1 = 10;
// error C2440: “初始化”: 无法从“int”转换为“int &&”
// message : 无法将左值绑定到右值引用
int a = 10;
int&& r2 = a;
// 右值引用可以引用move以后的左值
int&& r3 = std::move(a);
return 0;
}
move(x):将x从左值强制转化为右值,通过返回值的方式输出
注意:右值引用变量是一个左值, 可以被取地址与修改, 但const修饰的右值引用变量不能被修改
四、使用场景及实际意义
与C++98不同的是C++11出现了右值引用, 要了解右值引用的引用就要先了解左值引用有什么意义.
左值引用的意义:
传值传参和传值返回都会产生拷贝,有的甚至是深拷贝,代价很大。而左值引用的实际意义在于做参数和做返回值都可以减少拷贝,从而提高效率。
int test1()
{
int n = 0;
n++;
return n;
}
int& test2()
{
static int n = 0;
n++;
return n;
}
int main()
{
int ret1 = test1();
int ret2 = test2();
return 0;
}
在上面的代码中,出现了test1和test2两个函数, test是传值返回, 而test2是传引用返回, 在传引用返回中就出现了左值引用返回.
但也出现了许多问题:如
当对象出了函数作用域以后仍然存在时,可以使用左值引用返回,这是没问题的。
但当对象(对象是函数内的局部对象)出了函数作用域以后不存在时,就不可以使用左值引用返回了。此时只能使用传值返回.
>C++11就出现了右值引用
右值引用的意义
于是,为了解决上述传值返回的拷贝问题,C++11标准就增加了右值引用和移动语义。
1.移动语义
(1)移动构造
下面就是一个移动构造的例子:
string(string&& s)
:_str(nullptr)
, _size(0)
, _capacity(0)
{
swap(s);
}
但要注意的是:
在进行交换的时候,如果交换的对象是一个move标记的对象,就可以使交换的对象改变.如:
string str;
string str1=str;
string str2=move(str);
对于str1对象调用拷贝构造函数, 对于str2调用移动构造函数, 此时str2将交换str的资源内容.
A
拷贝构造函数和移动构造函数都是构造函数的重载函数,所不同的是:
- 拷贝构造函数的参数是 const左值引用,接收左值或右值;
- 移动构造函数的参数是右值引用,接收右值或被 move 的左值。
在存在移动构造函数的时候, 如果传来的参数是一个右值, 就会自动匹配移动构造函数.
因此:
若是左值做参数,那么就会调用拷贝构造函数,做一次拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次拷贝构造就会做一次深拷贝)。
若是右值做参数,那么就会调用移动构造,而调用移动构造就会减少拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次移动构造就会少做一次深拷贝)。
当只有拷贝构造没有移动构造:
既有拷贝构造也有移动构造:
因为函数中的 str 是将亡值(右值) ,在构造的时候直接调用移动拷贝
但现在的编译器一般都会进行优化:
因为临时对象有 ret 来接收,先拷贝构造出临时对象再用它移动构造出 ret ,临时对象好像没必要产生一样,不如省略掉。既然 str 是 to_string 函数栈帧的局部对象,最后还是要销毁,不如将 str 视为右值,直接转移 str 的资源用来构造 ret .
只有拷贝构造没有移动构造:
既有拷贝构造也有移动构造:
此外,C++11标准的STL 容器的相关接口函数也增加了右值引用版本
3.完美转发
在此之前我们需要知道什么是万能引用:
确定类型的 && 表示右值引用(比如:int&& ,string&&),
但函数模板中的 && 不表示右值引用,而是万能引用,模板类型必须通过推断才能确定,其接收左值后会被推导为左值引用,接收右值后会被推导为右值引用。
注意区分右值引用和万能引用:下面的函数的 T&& 并不是万能引用,因为类的实例化的时候就确定了 T 的类型.
template<typename T>
class A
{
void func(T&& t); // 模板实例化时T的类型已经确定,调用函数时T是一个确定类型,所以这里是右值引用
};
template<typename T>
void f(T&& t) // 万能引用
{
//...
}
int main()
{
int a = 5; // 左值
f(a); // 传参后万能引用被推导为左值引用
const string s("hello"); // const左值
f(s); // 传参后万能引用被推导为const左值引用
f(to_string(1234)); // to_string函数会返回一个string临时对象,是右值,传参后万能引用被推导为右值引用
const double d = 1.1;
f(std::move(d)); // const左值被move后变成const右值,传参后万能引用被推导为const右值引用
return 0;
}
在上文中提到, 右值引用的变量是左值,因此右值属性在函数传递中可能被改变
因而出现C++的完美转化
(2)概念
完美转发是指在函数模板中,完全依照模板的参数类型,将参数传递给当前函数模板中的另外一个函数。
因此,为了实现完美转发,除了使用万能引用之外,我们还要用到std::forward
(C++11),它在传参的过程中保留对象的原生类型属性。
void Func(int& x) { cout << "左值引用" << endl; }
void Func(const int& x) { cout << "const左值引用" << endl; }
void Func(int&& x) { cout << "右值引用" << endl; }
void Func(const int&& x) { cout << "const右值引用" << endl; }
template<typename T>
void PerfectForward(T&& t) // 万能引用
{
Func(std::forward<T>(t)); // 根据参数t的类型去匹配合适的重载函数
}
int main()
{
int a = 4; // 左值
PerfectForward(a);
const int b = 8; // const左值
PerfectForward(b);
PerfectForward(10); // 10是右值
const int c = 13;
PerfectForward(std::move(c)); // const左值被move后变成const右值
return 0;
}
实现完美转发需要用到万能引用和 std::forward 。
例子
#include<iostream>
using namespace std;
template<class T>
struct ListNode
{
ListNode* _next = nullptr;
ListNode* _prev = nullptr;
T _data;
};
template<class T>
class List
{
typedef ListNode<T> Node;
public:
List()
{
_head = new Node;
_head->_next = _head;
_head->_prev = _head;
}
void PushBack(const T& x) // 左值引用
{
Insert(_head, x);
}
void PushFront(const T& x) // 左值引用
{
Insert(_head->_next, x);
}
void PushBack(T&& x) // 右值引用
{
Insert(_head, forward<T>(x)); // 关键位置:保留对象的原生类型属性
}
void PushFront(T&& x) // 右值引用
{
Insert(_head->_next, forward<T>(x)); // 关键位置:保留对象的原生类型属性
}
template<class TPL> // 该函数模板实现了完美转发
void Insert(Node* pos, TPL&& x) // 万能引用
{
Node* prev = pos->_prev;
Node* newnode = new Node;
newnode->_data = forward<TPL>(x); // 关键位置:保留对象的原生类型属性
// prev newnode pos
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
private:
Node* _head;
};
--------------------------------------------------------------------------------------------------------------------------------
本文的讲解到此结束,谢谢大家的观看,有问题欢迎给我留评论。