c++11引入了右值引用和移动语义,通过避免无谓的复制,以提高程序的执行效率。
1、左值与右值
c++中的数值必属于左值或右值之一,通常有以下方法进行区分:
- 左值:在赋值语句左侧,右值:在赋值语句右侧
- 左值:表达式结束后仍存在的对象,右值:表达式结束后就不存在的临时变量
- 左值:能够取地址,右值:无法对其取地址
左值引用范例:
int a = 10;
int& refA = a; // refA是a的别名, 修改refA就是修改a, a是左值,左移是左值引用
int& b = 1; //编译错误! 1是右值,不能够使用左值引用
右值引用范例:
int&& a = 1; //实质上就是将不具名(匿名)变量取了个别名
int b = 1;
int && c = b; //编译错误! 不能将一个左值复制给一个右值引用
class A {
public:
int a;
};
A getTemp()
{
return A();
}
A && a = getTemp(); //getTemp()的返回值是右值(临时变量)
分析:左值引用(T&)只能绑定左值,右值引用(T&&)只能绑定右值,如果绑定的不对,编译就会失败。
特例:常量左值引用(const T&)可以算是一个“万能”的引用类型,它可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是,只能读不能改。
常量左值范例:
const int & a = 1; //常量左值引用绑定 右值, 不会报错
class A {
public:
int a;
};
A getTemp()
{
return A();
}
const A & a = getTemp(); //不会报错 而 A& a 会报错
2、移动构造和移动赋值
回顾一下如何用c++实现一个字符串类MyString
,MyString
内部管理一个C语言的char *
数组,这个时候一般都需要实现拷贝构造函数和拷贝赋值函数,因为默认的拷贝是浅拷贝,而指针这种资源不能共享,不然一个析构了,另一个也就完蛋了。
String实现代码:
#include <iostream>
#include <cstring>
#include <vector>
using namespace std;
class MyString
{
public:
static size_t CCtor; //统计调用拷贝构造函数的次数
// static size_t CCtor; //统计调用拷贝构造函数的次数
public:
// 构造函数
MyString(const char* cstr=0){
if (cstr) {
m_data = new char[strlen(cstr)+1];
strcpy(m_data, cstr);
}
else {
m_data = new char[1];
*m_data = '\0';
}
}
// 拷贝构造函数
MyString(const MyString& str) {
CCtor ++;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
}
// 拷贝赋值函数 =号重载
MyString& operator=(const MyString& str){
if (this == &str) // 避免自我赋值!!
return *this;
delete[] m_data;
m_data = new char[ strlen(str.m_data) + 1 ];
strcpy(m_data, str.m_data);
return *this;
}
~MyString() {
delete[] m_data;
}
char* get_c_str() const { return m_data; }
private:
char* m_data;
};
size_t MyString::CCtor = 0;
int main()
{
vector<MyString> vecStr;
vecStr.reserve(1000); //先分配好1000个空间,不这么做,调用的次数可能远大于1000
for(int i=0;i<1000;i++){
vecStr.push_back(MyString("hello"));
}
cout << MyString::CCtor << endl;
}
分析:
发现执行了1000
次拷贝构造函数,如果MyString("hello")
构造出来的字符串本来就很长,构造一遍就很耗时了,最后却还要拷贝一遍,而MyString("hello")
只是临时对象,拷贝完就没什么用了,这就造成了没有意义的资源申请和释放操作,如果能够直接使用临时对象已经申请的资源,既能节省资源,又能节省资源申请和释放的时间。而C++11
新增加的移动语义就能够做到这一点。
要实现移动语义就必须增加两个函数:移动构造函数和移动赋值构造函数。
// 移动构造函数
MyString(MyString&& str) noexcept
:m_data(str.m_data) {
MCtor ++;
str.m_data = nullptr; //不再指向之前的资源了
}
// 移动赋值函数 =号重载
MyString& operator=(MyString&& str) noexcept{
MAsgn ++;
if (this == &str) // 避免自我赋值!!
return *this;
delete[] m_data;
m_data = str.m_data;
str.m_data = nullptr; //不再指向之前的资源了
return *this;
}
重新执行上述String测试程序,发现调用猎人1000次移动构造函数,而MyString("hello")
是个临时对象,是个右值,优先进入移动构造函数而不是拷贝构造函数。而移动构造函数与拷贝构造不同,它并不是重新分配一块新的空间,将要拷贝的对象复制过来,而是"偷"了过来,将自己的指针指向别人的资源,然后将别人的指针修改为nullptr
,这一步很重要,如果不将别人的指针修改为空,那么临时对象析构的时候就会释放掉这个资源,"偷"也白偷了。下面这张图可以解释copy和move的区别。
![](https://i-blog.csdnimg.cn/blog_migrate/46d6c3baba72cffe986c7414af6368d0.png)
对于一个左值,肯定是调用拷贝构造函数了,但是有些左值是局部变量,生命周期也很短,能不能也移动而不是拷贝呢?C++11
为了解决这个问题,提供了std::move()
方法来将左值转换为右值,从而方便应用移动语义。我觉得它其实就是告诉编译器,虽然我是一个左值,但是不要对我用拷贝构造函数,而是用移动构造函数吧。。。
int main()
{
vector<MyString> vecStr;
vecStr.reserve(1000); //先分配好1000个空间
for(int i=0;i<1000;i++){
MyString tmp("hello");
vecStr.push_back(tmp); //调用的是拷贝构造函数
}
cout << "CCtor = " << MyString::CCtor << endl;
cout << "MCtor = " << MyString::MCtor << endl;
cout << "CAsgn = " << MyString::CAsgn << endl;
cout << "MAsgn = " << MyString::MAsgn << endl;
cout << endl;
MyString::CCtor = 0;
MyString::MCtor = 0;
MyString::CAsgn = 0;
MyString::MAsgn = 0;
vector<MyString> vecStr2;
vecStr2.reserve(1000); //先分配好1000个空间
for(int i=0;i<1000;i++){
MyString tmp("hello");
vecStr2.push_back(std::move(tmp)); //调用的是移动构造函数
}
cout << "CCtor = " << MyString::CCtor << endl;
cout << "MCtor = " << MyString::MCtor << endl;
cout << "CAsgn = " << MyString::CAsgn << endl;
cout << "MAsgn = " << MyString::MAsgn << endl;
}
/* 运行结果
CCtor = 1000
MCtor = 0
CAsgn = 0
MAsgn = 0
CCtor = 0
MCtor = 1000
CAsgn = 0
MAsgn = 0
*/
举例:
MyString str1("hello"); //调用构造函数
MyString str2("world"); //调用构造函数
MyString str3(str1); //调用拷贝构造函数
MyString str4(std::move(str1)); // 调用移动构造函数、
// cout << str1.get_c_str() << endl; // 此时str1的内部指针已经失效了!不要使用
//注意:虽然str1中的m_dat已经称为了空,但是str1这个对象还活着,知道出了它的作用域才会析构!而不是move完了立刻析构
MyString str5;
str5 = str2; //调用拷贝赋值函数
MyString str6;
str6 = std::move(str2); // str2的内容也失效了,不要再使用
参考: