左值与右值
- 左值:能用取址符号 & 取出地址的值
- 右值:不能用取值符合取出地址的值,如临时对象
int i = 1; // i 是左值,1 是右值
int GetZero {
int zero = 0;
return zero;
}
//j 是左值,GetZero() 是右值,因为返回值存在于寄存器中
int j = GetZero();
//s 是左值,string("no name") 是匿名变量,是右值
string s = string("no name");
左值引用与右值引用
左值引用就是最传统的引用 &,如下:
int a = 10;
int& refA = a; // refA是a的别名, 修改refA就是修改a, a是左值,左移是左值引用
int& b = 1; //编译错误! 1是右值,不能够使用左值引用
c++11中增加了右值引用,右值引用关联到右值时,右值被存储到特定位置,右值引用指向该特定位置,也就是说,右值虽然无法获取地址,但是右值引用是可以获取地址的,该地址表示临时对象的存储位置。具体语法如下:
int&& a = 1; // &&是右值引用的符号,实质上就是将不具名(匿名)变量取了个别名
int b = 1;
int && c = b; //编译错误! 不能将一个左值复制给一个右值引用
右值引用的三个特点:
- 通过右值引用的声明,右值又“重获新生”,其生命周期与右值引用类型变量的生命周期一样长,只要该变量还活着,该右值临时量将会一直存活下去
class A {
public:
int a;
};
A getTemp()
{
return A();
}
A && a = getTemp(); //getTemp()的返回值是右值(临时变量)getTemp()返回的右值本来在表达式语句结束后,其生命也就该终结了(因为是临时变量),
//而通过右值引用,该右值又重获新生,其生命期将与右值引用类型变量a的生命期一样,只要a还活着,该右值临时变量将会一直存活下去。
- 右值引用独立于左值和右值。意思是右值引用类型的变量可能是左值也可能是右值。
从下面的代码中可以看到,T&&表示的值类型不确定,可能是左值又可能是右值,这就是右值引用的一个特点
template<typename T>
void f(T&& t){}
f(10); //t是右值
int x = 10;
f(x); //t是左值
- T&& t在发生自动类型推断的时候,它是未定的引用类型(universal references),如果被一个左值初始化,它就是一个左值;如果它被一个右值初始化,它就是一个右值,它是左值还是右值取决于它的初始化。
template<typename T>
void f(T&& t){}
f(10); //t是右值 //这里发生自动类型推断
int x = 10;
f(x); //t是左值 //发生自动类型推断
template<typename T>
class Test {
Test(Test&& rhs); //这里不会发生类型推断,因为已经是确定的Tset &&类型的
};
const左值引用
左值引用只能绑定左值,右值引用只能绑定右值,如果绑定的不对,编译就会失败。
但是const左值引用可以绑定非常量左值、常量左值、右值,而且在绑定右值的时候,常量左值引用还可以像右值引用一样将右值的生命期延长,缺点是,只能读不能改。
const int & a = 1; //常量左值引用绑定 右值, 不会报错
class A {
public:
int a;
};
A getTemp()
{
return A();
}
const A & a = getTemp(); //不会报错 而 A& a 会报错
左值引用, 使用 T&, 只能绑定左值
右值引用, 使用 T&&, 只能绑定右值
常量左值, 使用 const T&, 既可以绑定左值又可以绑定右值
已命名的右值引用,编译器会认为是个左值
std::move
std::move() 能把左值强制转换为右值。
移动构造函数的出现原因
首先,移动构造函数是一个构造函数,他是用来构造一个对象的,和拷贝构造函数、构造函数等价。但与默认构造函数不同,编译器不提供默认移动构造函数。移动构造函数和移动赋值函数所执行的是同样的操作,只不过情况不一样,一种是直接构造一个对象,另一种是利用“=”运算符把一个对象赋值给另一个对象的时候。
移动构造函数和移动赋值函数与拷贝构造函数所执行的作用的是一样的,都是通过一个对象去构造一个新对象。但有时候我们会遇到这样一种情况,我们用对象a初始化对象b,后对象a我们就不在使用了,但是对象a掌握的内存资源仍然存在(在析构之前),既然拷贝构造函数,实际上就是把a对象的内容复制一份到b中,那么为什么我们不能直接使用a对象掌握的内存空间?这样就避免了新的空间的分配,大大降低了构造的成本。这就是移动构造函数设计的初衷。
总的来说,就是类中有指针类型的成员变量时,当遇到对象构造对象时,需要使用拷贝构造函数中的深拷贝的方式把该指针成员赋值,这种深拷贝的方法会重新在堆上分配成员指针分配内存,当出现多次上述对象a初始化对象b之后,对象a不在存在的情况是,程序就会在堆上分配多次内存,大大降低程序运行效率,所以移动构造函数就将即将放弃的对象的内存空间直接给新对象使用,就能避免许多临时对象的创建,也能避免程序多次在堆上申请空间,从而大大的提高了执行效率
移动构造函数的实现
移动构造函数的参数和拷贝构造函数不同,拷贝构造函数的参数是一个左值引用,但是移动构造函数的初值是一个右值引用。这意味着,移动构造函数的参数是一个右值或者将亡值的引用。也就是说,只有当用一个右值,或者将亡值初始化另一个对象的时候,才会调用移动构造函数。移动构造函数的例子如下:
#include<iostream>
using namespace std;
class myobj {
public:
myobj() :pdata(nullptr) {
cout << "默认构造函数..." << endl;
}
myobj(int data) :pdata(new int(data)) {
cout << "有参构造函数..." << endl;
}
myobj(myobj& md) :pdata(new int(*(md.pdata))) {
cout << "深拷贝构造函数..." << endl;
}
myobj(myobj&& md) :pdata(md.pdata) {
md.pdata = nullptr;
cout << "移动构造函数..." << endl;
}
~myobj()
{
delete this->pdata;
cout << "析构函数..." << endl;
}
friend ostream& operator<<(ostream& os, myobj& md);
myobj& operator=(myobj&& md)
{
if (md.pdata != this->pdata)
{
delete pdata;
this->pdata = md.pdata;
pdata = nullptr;
}
cout << "移动赋值运算符..." << endl;
return *this;
}
private:
int* pdata;
};
ostream& operator<<(ostream& os, myobj& md) {
os << *(md.pdata);
return os;
}
myobj getdata()
{
myobj m(1);
return m;
}
void test()
{
myobj m1(10);
myobj m2 = std::move(m1);//调用移动赋值运算符
cout << m2 << endl;
/*
本来应该调用移动构造函数,因为getdata函数返回的是一个临时对象,是一个右值
但是编译器可以通过优化将对象 m 直接放置到 m3 中,
而不进行实际的移动构造。
这意味着 m3 实际上是 m,而不是通过移动构造函数从 m 移动过来的。
可以使用-fno-elide-constructors编译选项来禁用这些优化。
*/
myobj m3(getdata());
cout << m3 << endl;
}
int main()
{
test();
}
VS中的运行结果:
使用-fno-elide-constructors
编译选项禁用优化后的结果:
对于第三次移动构造的发生,笔者查阅后有说可能是与编译器的特殊优化有关 ,目前暂时不清楚其出现的原因
参考:
C++之移动构造函数-CSDN博客
【精选】C++ 移动构造函数详解_吾爱技术圈的博客-CSDN博客