右值引用、移动构造函数和move

左值和右值

左值和右值判断:
1)可位于赋值号(=)左侧的表达式就是左值;反之,只能位于赋值号右侧的表达式就是右值。
2)有名称的、可以获取到存储地址的表达式即为左值;反之则是右值。
例如:

int i = 10;
10 = i;
错误,10为右值,不能当左值用
int j = 20;
j = i;
i和j都是左值,但是i可以当右值用

  以上面定义的变量 i、j 为例,i 和 j是变量名,且通过 &a 和 &b 可以获得他们的存储地址,因此 a 和 b 都是左值;反之,字面量10、20,它们既没有名称,也无法获取其存储地址(字面量通常存储在寄存器中,或者和代码存储在一起),因此10、20 都是右值。
  注意,上面 2 种判定方法只适用于大部分场景。

右值引用

int i = 10;
int &a = i;
int &b = 10;//错误
const int &c = 10;//正确

常规的我们使用的一个&表示左值引用,普通左值引用只能接收左值,不能接收右值,但是常量左值引用既可以接收左值也可以接收右值。

int j = 10;
int &&d = 10;
int &&e = j;//错误

两个&表示右值引用,常规右值引用可以用来接收右值,但是不能用来接收左值
在这里插入图片描述
上图描述了左值引用和右值引用可以接收的数据类型,其中非常量的右值引用常用来进行移动构造函数和完美转发。

移动构造函数

看下面这个场景:

#include <iostream>
using namespace std;
class demo
{
    
    
public:
	demo() :num(new int(0))
	{
    
    
		cout << "construct!" << endl;
	}
	//拷贝构造函数
	demo(const demo &d) :num(new int(*d.num))
	{
    
    
		cout << "copy construct!" << endl;
	}
	~demo()
	{
    
    
		if(num != nullptr)
		{
    
    
			delete num;
			num = nullptr;
		}
		cout << "class destruct!" << endl;
	}
private:
	int *num;
};
demo get_demo()
{
    
    
	return demo();
}
int main()
{
    
    
	demo a = get_demo();
	return 0;
}

可以看到,该程序运行调用了两次拷贝构造函数,第一次是在get_demo函数中,第二次是在a对象初始化的时候,并且这两次拷贝都是深拷贝,在Linux可以看看运行结果:
使用该命令编译文件,否则看不到完整输出结果

g++ main.cpp -fno-elide-constructors

在这里插入图片描述
试想一下,如果两次深拷贝过程中在堆上申请了大量内存空间,会浪费大量时间,程序效率低下,那么如何优化这个问题呢,其实仔细想一下,这两次深拷贝过程生成的都是匿名对象,无法通过&获取地址,因此它是一个右值,可以通过引入移动构造函数来优化程序。

#include <iostream>
using namespace std;
class demo
{
    
    
public:
	demo() :num(new int(0))
	{
    
    
		cout << "construct!" << endl;
	}
	demo(const demo &d) :num(new int(*d.num))
	{
    
    
		cout << "copy construct!" << endl;
	}
	//添加移动构造函数
	demo(demo &&d) :num(d.num)
	{
    
    
		d.num = nullptr;
		cout << "move construct!" << endl;
	}
	~demo()
	{
    
    
		if(num != nullptr)
		{
    
    
			delete num;
			num = nullptr;
		}
		cout << "class destruct!" << endl;
	}
private:
	int *num;
};
demo get_demo()
{
    
    
	return demo();
}
int main()
{
    
    
	demo a = get_demo();
	return 0;
}

查看上面程序的执行结果:
在这里插入图片描述
  可以看到两次深拷贝的过程都是通过移动构造函数来完成的,在移动构造函数中,参数是一个demo的右值引用,并且直接将新生成的对象的指针成员指向匿名对象所申请的堆空间,将匿名对象的成员指针置空,这样就防止每次调用拷贝构造函数向堆上申请新的内存空间了,大大提高了效率。
  所谓移动语义,指的就是以移动而非深拷贝的方式初始化含有指针成员的类对象。简单的理解,移动语义指的就是将其他对象(通常是临时对象)拥有的内存资源“移为已用”。

move

默认情况下,左值初始化同类对象只能通过拷贝构造函数完成,如果想调用移动构造函数,则必须使用右值进行初始化。C++11 标准中为了满足用户使用左值初始化同类对象时也通过移动构造函数完成的需求,新引入了 std::move() 函数,它可以将左值强制转换成对应的右值,由此便可以使用移动构造函数。

#include <iostream>
using namespace std;
class MoveDemo
{
    
    
public:
	MoveDemo() :num(new int(0))
	{
    
    
		cout << "construct!" << endl;
	}
	MoveDemo(const MoveDemo &d) :num(new int(*d.num))
	{
    
    
		cout << "copy construct!" << endl;
	}
	//添加移动构造函数
	MoveDemo(MoveDemo &&d) :num(d.num)
	{
    
    
		d.num = nullptr;
		cout << "move construct!" << endl;
	}
	~MoveDemo()
	{
    
    
		if (num != nullptr)
		{
    
    
			delete num;
			num = nullptr;
		}
		cout << "class destruct!" << endl;
	}
public:
	int *num;
};
MoveDemo get_demo()
{
    
    
	return MoveDemo();
}
int main()
{
    
    
	MoveDemo demo;
	cout << "demo2:\n";
	MoveDemo demo2 = demo;
	//cout << *(demo2.num) << endl;   //可以执行
	cout << "demo3:\n";
	MoveDemo demo3 = std::move(demo);
	//此时 demo.num = NULL,因此下面代码会报运行时错误
	//cout << *(demo.num) << endl;
	return 0;
}

在这里插入图片描述
可以看到初始化demo3的过程中调用的是移动构造函数,原因是使用move将demo左值强制转化为右值。

猜你喜欢

转载自blog.csdn.net/bureau123/article/details/112696446