C++11第二弹(右值引用与移动语义)
嘿,各位技术潮人!好久不见甚是想念。生活就像一场奇妙冒险,而编程就是那把超酷的万能钥匙。此刻,阳光洒在键盘上,灵感在指尖跳跃,让我们抛开一切束缚,给平淡日子加点料,注入满满的
passion。准备好和我一起冲进代码的奇幻宇宙了吗?Let’s go!
我的博客:yuanManGan
我的专栏:C++入门小馆 C言雅韵集 数据结构漫游记 闲言碎语小记坊 进阶数据结构 走进Linux的世界 题山采玉 领略算法真谛
C++ 11
3.右值引用与移动语义
3.1 左值和右值
- 左值就是可以取地址的值,右值就是不能取地址的值。
- 左值一般是变量,右值要么是自变量常量,要么是求值过程中的临时变量(如匿名对象等)
int main()
{// 左值:可以取地址// 以下的p、b、c、*p、s、s[0]就是常见的左值int* p = new int(0);int b = 1;const int c = b;*p = 10;string s("111111");s[0] = 'x';cout << &c << endl;cout << (void*)&s[0] << endl;int*& r1 = p;char& r2 = s[0];// 右值:不能取地址double x = 1.1, y = 2.2;// 以下几个10、x + y、fmin(x, y)、string("11111")都是常见的右值10;x + y;fmin(x, y);string("11111");return 0;
}
我们发现const int c
和 s[0]
的地址相近,证明c和s[0]都存储在栈上
,const修饰的变量不是常量,而是常变量
。
3.2 左值引用和右值引用
- 左值引用就是引用左值,右值引用就是引用右值
int& r1 = x (左值引用)
int&& r2 = y (右值引用)
- 左值引用不能直接引用右值,但加上
const
可以引用右值 - 右值引用同样不能直接引用左值,但加上
move
可以引用左值 move
可以将变量变为常量,即右值转为左值,本质就是强制类型转换
int main()
{// 右值引用给右值取别名int&& rr1 = 10;double&& rr2 = x+y;double&& rr3 = fmin(x, y);string&& rr4 = string("11111");//左值引用加const可以引用右值const int& r5 = 10;const double& r6 = x+y;//move将左值强转为右值int&& rr5 = move(b);string&& rr6 = move(s);// move底层本质跟这里类似,就是强转string&& rr7 = (string&&)s;
}
3.3 延长生命周期
- 我们要注意所有的变量表达式都是左值,也就是一个右值被右值引用绑定后,右值引用的变量变量表达式的属性是左值。
int&& rr1 = 10; double&& rr2 = x+y; 这里的rr1和rr2的属性都是左值
- 右值引⽤可⽤于为临时对象延⻓⽣命周期,
const
的左值引⽤也能延⻓临时对象⽣存期,但这些对象⽆法被修改。
int main()
{std::string s1 = "Test";// std::string&& r1 = s1; // 错误:右值引用不能绑定到左值const std::string& r2 = s1 + s1; // OK:到 const 的左值引⽤延⻓⽣存期// r2 += "Test"; // 错误:不能通过到 const 的引⽤修改std::string&& r3 = s1 + s1; // OK:右值引⽤延⻓⽣存期r3 += "Test"; // OK:能通过到⾮ const 的引⽤修改std::cout << r3 << '\n';return 0;
}
3.4 左值和右值的参数匹配
- 在
C++98
中我们实现⼀个const左值引⽤
作为参数的函数,那么实参传递左值和右值都可以匹配。 C++11
以后,分别重载左值引⽤、const
左值引⽤、右值引⽤作为形参的f
函数,那么实参是左值会匹配f(左值引⽤)
,实参是const左值
会匹配f(const 左值引⽤)
,实参是右值会匹配f(右值引⽤)
。
#include<iostream>
using namespace std;
void f(int& x)
{std::cout << "左值引⽤重载 f(" << x << ")\n";
}
void f(const int& x)
{std::cout << "到 const 的左值引⽤重载 f(" << x << ")\n";
}
void f(int&& x)
{std::cout << "右值引⽤重载 f(" << x << ")\n";
}
int main()
{int i = 1;const int ci = 2;f(i); // 调⽤ f(int&)f(ci); // 调⽤ f(const int&)f(3); // 调⽤ f(int&&),如果没有 f(int&&) 重载则会调⽤ f(const int&)f(std::move(i)); // 调⽤ f(int&&)// 右值引⽤变量在⽤于表达式时是左值int&& x = 1;f(x); // 调⽤ f(int& x)f(std::move(x)); // 调⽤ f(int&& x)return 0;
}
3.5 右值引用和移动语义的使用场景
3.5.1 左值引用主要使用场景回顾
我们之前使用左值引用的意义是什么呢?
1.左值引用作形参时可以引⽤传参,代替指针的功能,能改变传入的变量的值
2.左值引⽤传返回值时可以减少拷贝。
左值引⽤已经解决⼤多数场景的拷⻉效率问题,但是有些场景不能使⽤传左值引⽤返回。如下面这个函数:
class Solution {
public:// 传值返回需要拷⻉string addStrings(string num1, string num2) {string str;int end1 = num1.size()-1, end2 = num2.size()-1;// 进位int next = 0;while(end1 >= 0 || end2 >= 0){int val1 = end1 >= 0 ? num1[end1--]-'0' : 0;int val2 = end2 >= 0 ? num2[end2--]-'0' : 0;int ret = val1 + val2+next;next = ret / 10;ret = ret % 10;str += ('0'+ret);}if(next == 1)str += '1';reverse(str.begin(), str.end());return str;}
}
如果这里如果强行使用左值引用作为返回值的话,这里就成了野引用,因为我们创建的str
对象出了作用域就会被销毁,那存在引用呢?
如果想要减少拷贝,在之前的c++98
版本中是怎样做的呢?
class Solution {
public:// 传值返回需要拷⻉void addStrings(string num1, string num2, string str) {int end1 = num1.size()-1, end2 = num2.size()-1;// 进位int next = 0;while(end1 >= 0 || end2 >= 0){int val1 = end1 >= 0 ? num1[end1--]-'0' : 0;int val2 = end2 >= 0 ? num2[end2--]-'0' : 0;int ret = val1 + val2+next;next = ret / 10;ret = ret % 10;str += ('0'+ret);}if(next == 1)str += '1';reverse(str.begin(), str.end());}
}
int main()
{string ret;Solution().addStrings("123", "456", ret);return 0;
}
我们就得提前开辟空间,然后传入参数的形式来得到预期的结果。为了解决这个问题C++11就引出了移动赋值和移动构造。
3.5.2 移动构造和移动赋值
- 移动构造函数是⼀种构造函数,类似拷⻉构造函数,移动构造函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤,如果还有其他参数,额外的参数必须有缺省值。
- 移动赋值是⼀个赋值运算符的重载,他跟拷⻉赋值构成函数重载,类似拷⻉赋值函数,移动赋值函数要求第⼀个参数是该类类型的引⽤,但是不同的是要求这个参数是右值引⽤。
下面是一个string
类
namespace gl
{class string{public:typedef char* iterator;typedef const char* const_iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}const_iterator begin() const{return _str;}const_iterator end() const{return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){cout << "string(char* str)-构造" << endl;_str = new char[_capacity + 1];strcpy(_str, str);}// 拷贝构造string(const string& s):_str(nullptr){cout << "string(const string& s) -- 拷贝构造" << endl;reserve(s._capacity);for (auto ch : s){push_back(ch);}}// 移动构造// 右值,通常是临时对象,匿名对象string(string&& s){cout << "string(const string& s) -- 移动构造" << endl;this->swap(s);}string& operator=(const string& s){cout << "string& operator=(const string& s) -- 拷贝赋值" << endl;if (this != &s){_str[0] = '\0';_size = 0;reserve(s._capacity);for (auto ch : s){push_back(ch);}}return *this;}string& operator=(string&& s){cout << "string& operator=(string&& s) -- 移动赋值" << endl;this->swap(s);}~string(){cout << "~string() -- 析构" << endl;delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}void reserve(size_t n){//...}void push_back(char ch){//...}string& operator+=(char ch){push_back(ch);return *this;}const char* c_str() const{return _str;}size_t size() const{return _size;}private:char* _str = nullptr;size_t _size = 0;size_t _capacity = 0;};}
如果我用右值去构造一个string
类
gl::string s = "abc";
它会使用左值版本的构造函数,如果我们重构实现一个右值版本的构造函数:
string(string && s)
{cout << "string(const string& s) -- 移动构造" << endl;this->swap(s);
}
为什么可以这样写呢?我们的s
是要销毁的值,我们把它交换给我们需要构造的值,交换后s
里面为空释放资源也轻松。
移动赋值也是一样的。
string& operator=(string&& s)
{cout << "string& operator=(string&& s) -- 移动赋值" << endl;this->swap(s);
}
3.5.3 右值引用和移动语义解决传值返回问题
- 右值对象构造,只有拷贝构造,没有移动构造的场景
会进行两次拷贝构造,并且析构两次资源。而右边编译器直接把str
拷贝构造给ret
不创建临时对象
- 右值对象构造,有拷⻉构造,也有移动构造的场景
图一进行了两次移动构造,析构了两次但没有释放空间,而编译器优化后直接用str
拷贝构造ret
- 右值对象赋值,只有拷⻉构造和拷⻉赋值,没有移动构造和移动赋值的场景
编译器同样少构造一次直接将str
转化为临时对象,然后再赋值给ret
。 - 右值对象赋值,既有拷⻉构造和拷⻉赋值,也有移动构造和移动赋值的场景
这里同样的,减少了一次移动构造。
3.5.4 右值引用和移动语义在传参中的提效
-
如果细心一点,我们在看
C++
的STL
文档中会发现push
和insert
系列的接口中增加了右值引用的版本
-
当我们传入左值时,容器会调用拷贝构造进行拷贝,将对象拷贝到容器空间的对象。
-
当我们传入右值时,容器会调用移动构造,将右值对象的资源转移到容器空间的对象上。
int main()
{std::list<bit::string> lt;bit::string s1("111111111111111111111");lt.push_back(s1);cout << "*************************" << endl;lt.push_back(bit::string("22222222222222222222222222222"));cout << "*************************" << endl;lt.push_back("3333333333333333333333333333");cout << "*************************" << endl;lt.push_back(move(s1));cout << "*************************" << endl;return 0;
}
运⾏结果:
string(char* str)
string(const string& s) -- 拷⻉构造
*************************
string(char* str)
string(string&& s) -- 移动构造
~string() -- 析构
*************************
string(char* str)
string(string&& s) -- 移动构造
~string() -- 析构
*************************
string(string&& s) -- 移动构造
*************************
~string() -- 析构
~string() -- 析构
~string() -- 析构
~string() -- 析构
~string() -- 析构
3.6 类型分类
C++11
后进一步对类型进行了划分,右值被分为了,纯右值和将亡值。- 纯右值指的是之前意义下的右值,就是C++98里面的右值
- 将亡值是指返回右值引⽤的函数的调⽤表达式和转换为右值引⽤的转换函数的调⽤表达,如
move(x)、static_cast<X&&>(x)
- -泛左值指的是将亡值和左值
- 值类别
-
3.7 引⽤折叠
- 如果
c++
中直接写int & && r = i;
会直接报错,必须要typedef
之后才可以构成引用的引用。
- 对引用的进行引用有个规则,如果你想问为什么,那我只能告诉你,这是由
C++委员会
制定的,你记住就好。 - 右值引⽤的右值引⽤折叠成右值引⽤,所有其他组合均折叠成左值引⽤。
#include<iostream>
using namespace std;
int main()
{typedef int& lref;typedef int&& rref;int n = 1;lref& r1 = n; // r1 的类型是 int&lref&& r2 = n; // r2 的类型是 int&rref& r3 = n; // r3 的类型是 int&rref&& r4 = 1; // r4 的类型是 int&&return 0;
}
- 由于引用折叠的存在,我们像
f2
这个函数模板就成了万能引用。
// 由于引⽤折叠限定,f1实例化以后总是⼀个左值引⽤
template<class T>
void f1(T& x)
{}
// 由于引⽤折叠限定,f2实例化后可以是左值引⽤,也可以是右值引⽤
template<class T>
void f2(T&& x)
{}
int main()
{// 没有折叠->实例化为void f1(int& x)f1<int>(n);f1<int>(0); // 报错// 折叠->实例化为void f1(int& x)f1<int&>(n);f1<int&>(0); // 报错// 折叠->实例化为void f1(int& x)f1<int&&>(n);f1<int&&>(0); // 报错// 折叠->实例化为void f1(const int& x)f1<const int&>(n);f1<const int&>(0);// 折叠->实例化为void f1(const int& x)f1<const int&&>(n);f1<const int&&>(0);// 没有折叠->实例化为void f2(int&& x)f2<int>(n); // 报错f2<int>(0);// 折叠->实例化为void f2(int& x)f2<int&>(n);f2<int&>(0); // 报错// 折叠->实例化为void f2(int&& x)f2<int&&>(n); // 报错f2<int&&>(0);return 0;
}
3.8 完美转发
template<class T>
void Function(T&& t)
{Fun(t);
}
这个函数中,无论我们传入的值是左值还有右值t
的属性都是左值。
#include<iostream>
using namespace std;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<class T>
void Function(T&& t)
{Fun(t);
}
int main()
{// 10是右值,推导出T为int,模板实例化为void Function(int&& t)Function(10); // 右值int a;// a是左值,推导出T为int&,引⽤折叠,模板实例化为void Function(int& t)Function(a); // 左值// std::move(a)是右值,推导出T为int,模板实例化为void Function(int&& t)Function(std::move(a)); // 右值const int b = 8;// a是左值,推导出T为const int&,引⽤折叠,模板实例化为void Function(const int&t)Function(b); // const 左值// std::move(b)右值,推导出T为const int,模板实例化为void Function(const int&&t)Function(std::move(b)); // const 右值return 0;
}
- 如果我们像让
t
和我们传入的参数有关的话,我们就得使用C++11
中的完美转发,这个完美转发本质就是一个强制类型转换。
void Function(T&& t)
{//Fun(t);Fun(forward<T>(t));
}