右值引用和移动语义
左值:可以获取地址,一般可以对它赋值,左值可以出现在赋值符号的左边,右值不能出现在赋值符号的左边。定义const修饰后的左值,不能给他赋值,但是可以取它的地址。左值引用就是给左值的引用,给左值取别名。
int b = 1;
int *p = new int(0);
const int a = 1;
右值:右值也是一个表示数据的表达式,如:字面常量,表达式返回值,函数返回值。
可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,右值不能取地址。
int fmin(int a, int b)
{
return a < b : a ? b;
}
10;
x + y;
fmin(x,y); //传值返回的临时对象
左值引用:给左值取别名
右值引用:给右值取别名
int main()
{
//左值引用
int a = 0;
int& r1 = a;
//右值引用
int&& r2 = 10;
double x = 1,1,y = 2.2;
double&& r3 = x + y;
//左值引用给右值取别名的情况
//x + y 的返回值是一个临时变量,加const就可以
const int& r4 = 10;
const double& r5 = x + y;
//右值引用可以引用move以后的左值
int&& r6 = move(a);
return 0;
}
============================================================================================
void func(const int& a)
{
cout<<"void func(const int& a)"<<endl;
}
void func(int&& a)
{
cout<<"void func(int&& a)"<<endl;
}
//这两个函数构成了函数重载,走更加匹配的那个函数。
int main()
{
int a = 0;
int b = 1;
func(a); //执行第一个函数
func(a+b); //执行第二个函数
return 0;
}
左值引用的价值:1.做参数 2.做返回值 都可以减少拷贝。
但是左值引用并不能解决传值返回的多次深拷贝问题。所以就有了右值引用去实现移动构造和移动赋值。
传值返回中左值引用多次深拷贝的场景
// 拷贝构造
string(const string& s)
: _str(nullptr)
{
cout << "string(const string& s) -- 深拷贝" << endl;
string tmp(s._str);
swap(tmp);
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
第一种情况
//上面是我们代码所用到的构造函数
//第一种情况
hlp::string func()
{
hlp::string s1("1234");
return s1;
}
int main()
{
hlp::string s2;
s2 = func();
return 0;
}
这里将会发生两次深拷贝,第一次是s1做返回值时,会临时拷贝构造出一个临时对象,用于传值返回。第二次是s2调用赋值重载函数,临时对象的返回值为s2赋值时。
第二种情况
string(const char* str = "")
: _size(strlen(str)), _capacity(_size)
{
cout << "string(char* str)" << "--构造" << endl;
_str = new char[_capacity + 1];
strcpy(_str, str);
}
//这是用到的构造函数
hlp::string func()
{
hlp::string s1("1234");
return s1;
}
int main()
{
//hlp::string s2;
//s2 = func();
hlp::string s3 = func();
return 0;
}
这里只有一次拷贝构造,跟第一种情况的两次深拷贝不同,编译器会将同一行当中连续的拷贝构造,优化为一次拷贝构造。
两种情况的对比:在C++中,hlp::string s3 = func();语句不是调用赋值重载函数的原因与赋值和初始化的区别有关。
- 初始化 vs 赋值
初始化:对象在创建时通过拷贝构造函数(或者移动构造函数)进行初始化。在 hlp::string s3 = func();; 中,s3 是一个新对象,它在定义时就被初始化。因此,拷贝构造函数会被调用,而不是赋值操作符重载函数。
赋值:赋值操作是将一个已存在的对象的值更新为另一个对象的值。赋值操作符重载函数(operator=)会在这种情况下被调用。但这只发生在对象已经存在时,而不是在对象创建时。
- hlp::string s3 = func();语句
这条语句可以理解为“拷贝初始化”。具体过程如下:func() 返回一个临时对象。
s3在定义时通过拷贝构造函数使用该临时对象进行初始化。
因为 s3 是在定义时直接进行的初始化,而不是先创建一个默认的 string 对象再用 = 赋值,所以会调用拷贝构造函数,而不会调用赋值重载函数。
总结:这两种情况都最少需要一次深拷贝,而深拷贝对于vector<vector> ,map<string,string> 等这样的类型,一次拷贝的代价是非常大的。
移动构造
移动构造,是为了解决自定义类型中发生深拷贝的类,必须传值返回的情景。
//构造函数
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;
string tmp(s._str);
swap(tmp);
}
//移动构造
string(string&& s)
{
cout<<"移动构造"<<endl;
swap(s);
}
//移动赋值
string& operator=(string &&s)
{
swap(s);
return *this;
}
// 赋值重载
string& operator=(const string& s)
{
cout << "string& operator=(string s) -- 深拷贝" << endl;
string tmp(s);
swap(tmp);
return *this;
}
hlp::string func()
{
hlp::string s1("1234");
return s1; //这里正常来写是move(s1)
}
int main()
{
hlp::string ret1 = func(); //在编译器不加优化的情况下,func()函数的返回值s1本身是左值,应该在传值返回的过程中发生一次拷贝构造,创建一个临时对象,之后这个临时对象将会调用移动构造,给ret1初始化。
//但是编译器将会优化,直接将s1看作右值(将亡值),直接调用一次移动构造即可。
hlp::string ret2;
ret2 = func(); //这样写编译器也做了特殊处理,本来应该是拷贝构造 和 移动赋值,优化后为移动拷贝和移动赋值。
return 0;
}
右值引用解决问题的场景就是:左值引用中不能解决的自定义类型中深拷贝的类,必须传值返回的场景。浅拷贝就不用实现,没有必要,因为没有另外开辟空间。
#include<iostream>
#include<list>
int mian()
{
list<string> l1;
string s1("xxxx");
l1.push_back(s1); //有一次构造
cout<<endl;
l1.push_back("xxxxxxxx"); //直接移动构造,减少拷贝,构造加移动构造。原来是一次构造加一次拷贝构造
return 0;
}
void push_back(const value_type& val)
{
Node* newnode = new Node(val);
}
Node(string&& val)
:_val(val)
{
}
Node(const string& val)
:_val(val)
{
}
在容器的插入接口,如果插入对象是右值,可以用移动构造转移资源给数据结构中的对象。
完美转发
模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。
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);
Fun(forward<T> 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;
}
右值:不能被修改
右值的引用:属性是左值,可以被修改
erfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
右值:不能被修改
右值的引用:属性是左值,可以被修改
**std::forward 完美转发在传参的过程中保留对象原生类型属性**