【C++】C++11 ~ 右值引用和移动语义
阿里云国内75折 回扣 微信号:monov8 |
阿里云国际,腾讯云国际,低至75折。AWS 93折 免费开户实名账号 代冲值 优惠多多 微信号:monov8 飞机:@monov6 |
🌈欢迎来到C++专栏~~右值引用和移动语义
- (꒪ꇴ꒪(꒪ꇴ꒪ )🐣,我是Scort
- 目前状态大三非科班啃C++中
- 🌍博客主页张小姐的猫~江湖背景
- 快上车🚘握好方向盘跟我有一起打天下嘞
- 送给自己的一句鸡汤🤔
- 🔥真正的大师永远怀着一颗学徒的心
- 作者水平很有限如果发现错误可在评论区指正感谢🙏
- 🎉🎉欢迎持续关注
文章目录
一. 基本概念
🌈左值 vs 右值
什么是左值
左值是一个表示数据的表达式(如 变量名或 解引用的指针)
- 左值可以被取地址也可以被修改const修饰的左值除外
- 左值可以出现在赋值符号的左边也可以出现在赋值符号的右边
int main()
{
//左值可以取地址
int a = 10;
const int b = 20;//const不能修改例外
int* p = &a;
*p = 10;
return 0;
}
什么是右值
右值也是一个表示数据的表达式如字母常量、表达式的返回值、函数的返回值不能是左值引用返回等等
- 右值不能被取地址也不能被修改
- 右值可以出现在赋值符号的右边但是不能出现在赋值符号的左边
int main()
{
double x = 1.1, y = 2.2;
//以下几个都是常见的右值不能取地址
10;
x + y;
fmin(x, y);
//错误示例右值不能出现在赋值符号的左边
//10 = 1;
//x + y = 1;
//fmin(x, y) = 1;
return 0;
}
- 右值本质就是一个临时变量或常量值比如代码中的10就是常量值表达式x+y和函数fmin的返回值就是临时变量这些都叫做右值
- 这些临时变量和常量值并没有被实际存储起来这也就是为什么右值不能被取地址的原因因为只有被存储起来后才有地址
🌈左值引用 vs 右值引用
C++11中新增了右值引用的语法特性为了进行区分于是将C++11之前的引用就叫做左值引用。但是无论左值引用还是右值引用本质都是给对象取别名
左值引用
左值引用就是对左值的引用给左值取别名通过“&
”来声明
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;
}
右值引用
右值引用就是对右值的引用给右值取别名通过“&&
”来声明
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);
return 0;
}
要注意的是
左值引用可以引用右值吗
- 左值引用不能引用右值因为这涉及权限放大的问题右值是不能被改变的而左值是可以修改的
- 有一个例外
const
const左值引用能够保证被引用的数据不会被修改使得const左值引用可以引用右值
那就是const
左值引用既可以引用左值也可以引用右值这样的其实我们已经见多了
template<class T>
void func(const T& x)//x既能接收左值也能接收右值
{
cout << x<< endl;
}
int main()
{
string s("hello");
func(s); //s为左值
func("world"); //"world"为右值
return 0;
}
🎃奇怪的现象
右值是不能取地址的但是给右值取别名后会导致右值被存储到特定位置这时这个右值可以被取到地址并且可以被修改如果不想让被引用的右值被修改可以用const修饰右值引用
int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20;
rr2 = 5.5; //报错
return 0;
}
此处的rr1
就被转化成左值了埋下伏笔哈哈哈后面会遇到
右值引用可以引用左值吗
- 右值引用只能引用右值不能引用左值
- 但是右值引用可以引用
move
以后的左值boss登场
move
函数是C++11标准提供的一个函数被move后的左值能够赋值给右值引用斯国一
int main()
{
int a = 10;
//int&& r1 = a; //右值引用不能引用左值
int&& r2 = move(a); //右值引用可以引用move以后的左值
return 0;
}
二. 右值引用的场景与意义
🎨左值引用的使用场景
我们先来看看左值引用的使用场景
- 做参数1️⃣减少拷贝提高效率 2️⃣做输出型参数
- 做返回值1️⃣减少拷贝提高效率2️⃣引用返回可以修改返回对象
🎨左值引用的短板
左值引用虽然能避免不必要的拷贝操作但左值引用并不能完全避免
- 左值引用做参数能够完全避免传参时不必要的拷贝操作
- 左值引用做返回值并不能完全避免函数返回对象时不必要的拷贝操作
💥短板如果函数返回的是一个局部对象该变量出了函数作用域就被销毁了这种情况下不能用左值引用作为返回值只能以传值的方式返回这就是左值引用的短板。
还好之前写了博客复习复习传送门
举个例子int版本的to_string函数这个to_string函数就不能使用左值引用返回因为to_string函数返回的是一个局部变量出作用域销毁了
namespace ljj
{
cl::string to_string(int value)
{
bool flag = true;
if (value < 0)
{
flag = false;
value = 0 - value;
}
cl::string str;
while (value > 0)
{
int x = value % 10;
value /= 10;
str += (x + '0');
}
if (flag == false)
{
str += '-';
}
std::reverse(str.begin(), str.end());
return str;
}
}
此时调用to_string函数返回时就一定会调用string的拷贝构造函数
int main()
{
ljj::string ret = to_string(3465);
return 0;
}
为此C++11就出手了提出右值引用就是为了解决左值引用的这个短板的
🎨右值引用和移动语义
那怎么样才能让编译器不优化我们手动操作呢那就要增加移动构造和移动赋值方法
psC++11对右值进行了划分
- 内置类型的右值 —— 纯右值
- 自定义类型的右值 —— 将亡值即将死亡的值
🥑移动构造
移动构造是一个构造函数该构造函数的参数是右值引用类型的移动构造本质就是将传入右值的资源窃取过来占为己有这样就避免了进行深拷贝所以它叫做移动构造就是窃取别人的资源来构造自己的意思
调用swap函数将传入右值的资源窃取过来占为己有
//移动构造
string(string&& s)//右值引用
:_str(nullptr)
, _size(0)
, _capacity(0)
{
cout << "string(string&& s) -- 资源转移" << endl;
swap(s); //资源互换
}
移动构造的价值
- 没有引入移动构造之前拷贝构造采用的是const左值引用接收无论传入的是左值还是右值都会调用拷贝构造
- 增加了移动构造之后采用的是右值引用接收参数如果传入的是右值的话就会调用移动构造最匹配原则
- string的拷贝构造函数做的是深拷贝而移动构造函数中只需要调用swap函数进行资源的转移因此调用移动构造的代价比调用拷贝构造的代价小少了一次深拷贝
我们来看看编译器的优化
当一个函数在返回局部对象时会先用这个局部对象拷贝构造出一个临时对象然后再用这个临时对象来拷贝构造我们接收返回值的对象深拷贝
编译器会优化成一步到位只需要一次拷贝构造还要什么临时对象我懂你意思直接给给ret
在C++11标准出来之前这里应该调用两次string的拷贝构造函数但最终被编译器优化成了一次减少了一次无意义的深拷贝。并不是所有的编译器都做了这个优化
C++11出来后编译器仍然保持了这种优化方式
“将亡值”str
马上就要被销毁了那还不如把它的资源转移给别人用因此编译器在识别这种“将亡值”时会将其识别为右值这样就可以匹配到参数类型为右值引用的移动构造函数
可以理解成
- 移动构造在战争中你穿上了别人不用的鞋子
- 拷贝构造没有鞋子给你穿你要自己去买拷贝一双
记住记住右值引用
swap
的是将亡值拷贝构造中不能直接swap因为对象不是将亡值下面的例子中swap完后s1就销毁了那我们不可以这样做
int main()
{
ljj::string s1("1111111");
ljj::string s2(s1);
return 0;
}
🥑移动赋值
😎移动赋值是一个赋值运算符重载函数该函数的参数是右值引用类型的移动赋值也是将传入右值的资源窃取过来占为己有这样就避免了深拷贝所以它叫移动赋值就是窃取别人的资源来赋值给自己的意思
- 如果我们不是用函数的返回值来构造一个对象而是用一个之前已经定义出来的对象来接收函数的返回值这时编译器就无法进行优化了
编译器并没有对这种情况进行优化因此在C++11标准出来之前对于深拷贝的类来说这里就会存在两次深拷贝因为深拷贝的类的赋值运算符重载函数也需要以深拷贝的方式实现
//移动赋值
string& operator=(string&& s)
{
cout << "string& operator=(const string&& s) -- 移动赋值" << endl;
swap(s);
return *this; //返回左值支持连续赋值
}
移动赋值的优势
- 在没有增加移动赋值之前由于原有
operator=
函数采用的是const左值引用接收参数因此无论赋值时传入的是左值还是右值都会调用原有的operator=函数 - 由于移动赋值采用的是右值引用接收参数因此如果赋值时传入的是右值那么就会调用移动赋值函数最匹配原则
- string原有的
operator=
函数做的是深拷贝而移动赋值函数中只需要调用swap
函数进行资源的转移因此 调用移动赋值的代价比调用原有operator=的代价小
此时当to_string函数返回局部的string对象时会先调用移动构造生成一个临时对象然后再调用移动赋值将临时对象的资源转移给我们接收返回值的对象这个过程虽然调用了两个函数但这两个函数要做的只是资源的移动而不需要进行深拷贝大大提高了效率
延长了资源的生命周期
😎容器新增内容
C++11标准出来之后STL中的容器都增加了移动构造和移动赋值
以string类为例这是string类增加的移动构造
这是string类增加的移动赋值
🎨右值引用能引用左值吗
字面上是不可以的但也不是完全不可以当需要用右值引用引用一个左值时可以通过move
函数将左值转化为右值
move函数的名字具有迷惑性move函数实际并不能搬移任何东西该函数唯一的功能就是将一个左值强制转化为右值引用然后实现移动语义
move定义如下
template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
//forward _Arg as movable
return ((typename remove_reference<_Ty>::type&&)_Arg);
}
🎨右值引用的其他使用场景
右值引用版本的插入函数
C++11标准出来之后STL容器插入接口函数也增加了右值引用版本
右值引用版本的意义
如果vector容器当中存储的是string对象那么在调用push_back向vector容器中插入元素
int main()
{
vector<ljj::string> v;
ljj::string s1("hello");
v.push_back(s1);//调用string的拷贝构造
cout << "——————————————————————————————————" << endl;
v.push_back("hello");//调用string的移动构造
return 0;
}
push_back函数需要先构造一个结点在内存池中定位new然后将该结点插入到底层的双链表当中
- C++11之前容器的push_back接口只有一个左值引用版本因此在push_back函数中构造结点时这个左值只能匹配到string的拷贝构造函数进行深拷贝
- C++11出来之后string类提供了移动构造函数并且容器的push_back接口提供了右值引用版本此时如果传入push_back函数的string对象是一个右值那么在push_back函数中构造结点时这个右值就可以匹配到string的移动构造函数进行资源的转移这样就避免了深拷贝提高了效率
三. 完美转发
✨万能引用
模板中的&&不代表右值引用而是万能引用其既能接收左值又能接收右值
template<class T>
void PerfectForward(T&& t)
{
//...
}
万能引用是根据传入实参的类型进行推导如果传入的实参是一个左值那么这里的形参t就是左值引用如果传入的实参是一个右值那么这里的形参t就是右值引用
举个例子
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<class T>
void PerfectForward(T&& t)
{
Func(t);
}
int main()
{
int a = 10;
PerfectForward(a); //左值
PerfectForward(move(a)); //右值
const int b = 20;
PerfectForward(b); //const 左值
PerfectForward(move(b)); //const 右值
return 0;
}
PerfectForward函数时传入左值、右值、const左值、const右值结果输出的全是左值为什么呢
- 根本原因是右值被引用后会导致右值被存储到特定位置这时这个右值可以被取到地址并且可以被修改所以在PerfectForward函数中调用Func函数时会将t识别成左值
就是说右值经过一次参数传递后其属性会退化成左值如果想要在这个过程中保持右值的属性就需要用到完美转发
✨完美转发保持值的属性
要想在参数传递过程中保持其原有的属性需要在传参时调用forward函数
template<class T>
void PerfectForward(T&& t)
{
//完美转发保持t引用的属性
Func(std::forward<T>(t));
}
✨完美转发的使用场景
一个简化版的list类类当中分别提供了左值引用版本和右值引用版本的push_back和insert函数
namespace ljj
{
template<class T>
struct ListNode
{
T _data;
ListNode* _next = nullptr;
ListNode* _prev = nullptr;
};
template<class T>
class list
{
typedef ListNode<T> node;
public:
//构造函数
list()
{
_head = new node;
_head->_next = _head;
_head->_prev = _head;
}
//左值引用版本的push_back
void push_back(const T& x)
{
insert(_head, x);
}
//右值引用版本的push_back
void push_back(T&& x)
{
insert(_head, std::forward<T>(x)); //完美转发
}
//左值引用版本的insert
void insert(node* pos, const T& x)
{
node* prev = pos->_prev;
node* newnode = new node;
newnode->_data = x;
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
//右值引用版本的insert
void insert(node* pos, T&& x)
{
node* prev = pos->_prev;
node* newnode = new node;
newnode->_data = std::forward<T>(x); //完美转发
prev->_next = newnode;
newnode->_prev = prev;
newnode->_next = pos;
pos->_prev = newnode;
}
private:
node* _head; //指向链表头结点的指针
};
}
只要右值每往下一层传都要完美转发否则统统变成左值
分别传入左值和右值调用不同版本的push_back
int main()
{
ljj::list<ljj::string> lt;
ljj::string s("1111");
lt.push_back(s); //调用左值引用
lt.push_back("2222"); //调用右值引用
return 0;
}
ps代码中push_back和insert函数的参数T&&
是右值引用而不是万能引用因为在list对象创建时这个类就被实例化了后续调用push_back和insert函数时参数T&&中的T已经是一个确定的类型了
📢写在最后
中国奇谭真不戳