1. 程式人生 > 其它 >右值引用、移動建構函式和move

右值引用、移動建構函式和move

技術標籤:C++c++

左值和右值

左值和右值判斷:
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左值強制轉化為右值。