C++基礎知識,對象移動,拷貝構造函數,移動拷貝構造函數,賦值運算符,移動賦值運算符

如果沒耐心看詳細的講解,直接到最下面看一個統一的例子

使用到的類:

class classB
{
public:
	int val1;
public:
	classB() :val1(100) 
	{
		cout << "B類構造函數執行了" << endl;
	};

	//拷貝構造函數
	classB(const classB& obj) :val1(obj.val1) 
	{
		cout << "B類拷貝構造函數執行了" << endl;
	};
	//析構函數
	virtual ~classB()
	{
		cout << "B類析構函數執行了" << endl;
	}

	//移動構造函數
	classB(classB&& obj)
	{
		cout << "B類!!!移動!!!構造函數執行" << endl;
	}
};


class classA
{
private:
	classB *innerClassB;

public:
	classA() :innerClassB(new classB())
	{
		cout << "A類構造函數執行" << endl;
	}

	//拷貝構造函數
	classA(const classA& obj):innerClassB( new classB( *(obj.innerClassB) ) )
	{
		cout << "A類拷貝構造函數執行" << endl;
	}

	virtual ~classA()
	{
		delete innerClassB;
		cout << "A類析構函數執行" << endl;
	}


	//移動構造函數(右值)  接管原來的內存
	//noexcept:通知標準庫,函數不拋出任何異樣,提升編譯器工作效率
	classA(classA&& obj) noexcept:innerClassB(obj.innerClassB)
	{
		//打斷原來的指針,切記
		obj.innerClassB = nullptr;
		cout << "A類!!!移動!!!構造函數執行" << endl;
	}


	//拷貝賦值運算符
	classA& operator=(const classA& obj)
	{
		if (this == &obj)
		{
			return *this;
		}
		delete innerClassB;
		//可不能這樣寫,這是移動賦值運算符的寫法,拷貝可不銷燬原先的
		//innerClassB = obj.innerClassB;
		cout << "執行了類A的拷貝賦值運算符" << endl;
		return *this;
	}

	//移動賦值運算符,因爲凡是帶有“移動”概念的,都要部分摧毀傳進來的對象,所以參數肯定不會帶const
	//並且都是右值,需要用std::move()傳遞進來
	classA& operator=(classA&& obj)
	{
		if (this == &obj)
		{
			return *this;
		}
		//斬斷自己
		delete innerClassB;
		//重新指向
		innerClassB = obj.innerClassB;
		//斬斷參數
		obj.innerClassB = nullptr;
		cout << "執行了類A的!!!移動賦值!!!運算符" << endl;
	}
};

對象移動

對象拷貝,消耗性能,提出對象移動的概念
許多臨時對象生命週期短,把臨時對象中的某些所有權拿過來,知銷燬不需要的數據即可
把一個不想用的對象中有用的數據供自己使用。
說白了,就是因爲我們平時寫的代碼,會有大量的臨時對象產生,你的對象小了還好,要是一個vector,裏面裝了1000個巨大的對象,對象裏面又有複雜的數據,那麼這樣的東西讓系統產生許多臨時對象就吃不消了,所以產生了對象移動的概念,就是在產生新的對象時,讓就對象的一部分數據的所有權交給我們的新對象,從而舊對象銷燬,而新的對象也不必申請可複用的一部分的數據空間,這是既節省了時間,又節省了空間,誰說空間和時間一定是槓桿的?

那什麼叫移動呢?
移動:

  • A移動到B,那麼A就不能再使用了
  • 並不是內存變更,而是內存中數據的所有權變更,內存是不變的

C++11:移動構造函數,拷貝構造函數:進一步提高程序效率

  • 拷貝構造函數:classA::classA(const classA& obj){…}左值引用
  • 移動構造函數:classA::classA(const classA&& obj){…}右值引用

如果有其他參數的話,必須要有默認值,和拷貝函數一樣

移動構造函數

應該完成的事情

  • 1、完成必要的內存移動,斬斷原對象的關係
  • 2、確保移動後源對象處於一種“即使被銷燬也沒有什麼問題”的狀態,既然移動了,就應該使用移動後的對象
    我們先看一下移動構造函數的寫法
	//移動構造函數,這裏是右值引用
	classB(classB&& obj)
	{
		//在這裏完全可以進行內存所有權的交接
		//1、刪除現在對象的部分內存所有權、
		//2、拿到傳過來的對象的部分內存所有權
		//3、斬斷傳遞對象的部分內存所有權
		cout << "B類!!!移動!!!構造函數執行" << endl;
	}
	//移動構造函數(右值)  接管原來的內存
	//noexcept:通知標準庫,函數不拋出任何異樣,提升編譯器工作效率
	classA(classA&& obj) noexcept:innerClassB(obj.innerClassB)
	{
		//打斷原來的指針,切記
		obj.innerClassB = nullptr;
		cout << "A類!!!移動!!!構造函數執行" << endl;
	}

static classA getA()
{
	classA a;//不直接return classA()就是爲了讓移動構造函數有機會執行
	return a;//如果有移動構造函數,就不會再去調用拷貝構造函數
}
int main()
{
	//執行的是拷貝構造函數,雖然有移動構造函數,但是你的代碼沒有不使用myClass1的傾向
	classA myClass1 = getA();
	//但是拷貝構造函數你完全可以移動內存
	classA myclass2(myClass1);

}

執行效果:
在這裏插入圖片描述
提出問題:
1、爲什麼A類執行析構函數的時候,作爲A類的一個成員變量,B類沒有執行析構函數呢?
這正是因爲再函數getA()裏面return的時候執行的是移動構造函數,而我們自定義的移動構造函數中,將B的所有權進行了交接,所以getA函數內部的A對象析構時,裏面根本沒有B的實例。
2、爲什麼我寫了移動構造函數,但是classA myclass2(myClass1)這一句執行的卻還是拷貝構造函數呢?
因爲移動構造函數的參數是右值拷貝構造函數的參數是左值,而myClass1很明顯是一個左值,如果想讓他執行移動構造函數,很簡單,把他用std::move()變成右值

int main()
{
	classA myClass1 = getA();
	classA myClass2(std::move(myClass1));
}

執行效果如下:
在這裏插入圖片描述

但是它畢竟還是調用了一個移動構造函數,我們完全可以再進行優化,直接不創建新的對象,給myClass1起一個別名:

	classA myClass1 = getA();
	classA&& myClass2(std::move(myClass1));

執行效果如下:
在這裏插入圖片描述

移動賦值運算符

移動賦值運算符的寫法:

//移動賦值運算符,因爲凡是帶有“移動”概念的,都要部分摧毀傳進來的對象,所以參數肯定不會帶const
//並且都是右值,需要用std::move()傳遞進來
classA& operator=(classA&& obj)
{
	if (this == &obj)
	{
		return *this;
	}
	//斬斷自己的內存
	delete innerClassB;
	//重新指向別人的內存
	innerClassB = obj.innerClassB;
	//別人斬斷內存聯繫
	obj.innerClassB = nullptr;
	cout << "執行了類A的!!!移動賦值!!!運算符" << endl;
}
classA&& a = getA();	//一個構造函數,一個移動構造函數,一個析構函數
classA a2;				//一個構造函數
a2 = std::move(a);		//一個移動賦值運算符

執行效果如下:
在這裏插入圖片描述

合成的移動操作

某些條件下,編譯器能合成“移動賦值運算符”,“移動構造函數”

  • 1、有自己的拷貝構造函數,拷貝賦值運算符,析構函數,編譯器就不會生成“移動構造函數”,“移動賦值運算符”
    也就是說自己有拷貝對象,釋放對象,編譯器就不會合成
  • 2、沒有移動函數的話,會用“拷貝構造函數”,“拷貝賦值運算符”來代替“移動構造函數”,“移動賦值運算符”
  • 3、只有當沒有任何拷貝成員,,且所有的非靜態成員都可以移動
    拷貝成員:拷貝賦值運算符,拷貝構造函數
    可以移動:內置類型,類類型要有相關移動相關的函數

滿足以上,編譯器就可以合成移動

	myClassC myClass1;
	myClass1.a = 99;
	myClassC myClass2 = std::move(myClass1);

統一的例子,簡單明瞭,最後總結

用到的類myClassC

class myClassC
{
public:
	int a;
	char x[100];
public:
	myClassC() :a(100) ,x("I Love You")
	{
		cout << "執行了構造函數" << endl;
	};
	myClassC(const myClassC& obj)
	{
		a = obj.a;
		for (int i = 0; i < strlen(obj.x); i++)
		{
			x[i] = obj.x[i];
		}
		cout << "執行了拷貝構造函數" << endl;
	}
	myClassC(myClassC&& obj)
	{
		//delete x;
		//x = obj.x;
		//delete obj.x;
		cout << "執行了移動拷貝構造函數" << endl;
	}
	myClassC& operator=(const myClassC& obj)
	{
		a = obj.a;
		cout << "執行了賦值運算符函數" << endl;
		return *this;
	}
	myClassC& operator=(myClassC&& obj)
	{
		if (this == &obj)
		{
			return *this;
		}
		//delete x;
		//x = obj.x;
		//delete obj.x;
		cout << "執行移動賦值運算符" << endl;
		return *this;
	}
};
int main()
{
	myClassC myClass1;							//構造函數
	myClassC myClass2 = myClass1;				//拷貝構造函數
	myClassC myClass3 = std::move(myClass1);	//移動拷貝構造函數
	myClassC myClass4;
	myClass4 = myClass1;						//賦值運算符
	myClassC myClass5;
	myClass5 = std::move(myClass1);				//移動賦值運算符
}

執行結果
在這裏插入圖片描述

1、賦值運算符和拷貝構造函數的區別
拷貝構造函數移動拷貝構造函數都是 用一個對象去初始化一個類生成對象
賦值運算符移動賦值運算符都是 用一個對象去給另一個對象賦值

總結:
1、儘量給類添加移動構造函數,移動賦值運算符(佔據大量內存的),因爲臨時對象都是右值,返回臨時對象或者其他操作會調用很多移動構造函數和移動賦值運算符
2、noexcept加上,才能保證該調用移動的時候調用
3、移動完之後,之前的對象儘量delete
4、沒有移動,會用拷貝代替

發佈了157 篇原創文章 · 獲贊 167 · 訪問量 2萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章