[c++] 五種構造函數的相關行爲

前言:

c++編譯器爲我們做了很多默認動作,這其中非常重要的一部分就是關於構造函數的。

 

默認構造函數:

默認構造函數是指 沒有參數的構造函數,如果一個構造函數都沒有定義,那麼編譯器會爲我們創建默認構造函數,這個構造函數什麼都不做。這個由編譯器爲我們創建的構造函數成爲 合成的默認構造函數。

ps:所有由編譯器爲我們創建的構造函數都叫做 合成的 xxx 構造函數。

 

拷貝構造函數:

原型:A(const A& a)

  • 拷貝構造函數儘量不要被聲明爲explicit,因爲很多時候拷貝構造都是隱式調用的,比如類作爲函數參數進行傳值調用;除非使用者知道自己在做什麼!
  • 編譯器始終會爲類創建 合成的拷貝構造函數,不論當前是不是有其他構造函數,這點有別於默認構造函數;
  • 關閉拷貝構造函數的兩個方法:1)使用 =delete;2)定義賦值構造/移動拷貝/移動賦值,這三者其中之一,而不定義拷貝構造;
  • 讓拷貝構造函數爲自定義行爲 或者 爲空白動作 的唯一方法就是顯示聲明和定義拷貝構造函數。

編譯器生成的拷貝構造函數行爲:大部分情況下,編譯器生成的拷貝構造函數會將 非static成員變量挨個拷貝構造給新的對象。如果是基礎類型,則直接賦值,如果是類類型,則調用類類型的拷貝構造。

注:如果成員變量中有類類型(不是指針),那麼就要求這個成員變量的類類型具備拷貝構造函數,例子如下:

#include "pch.h"
#include <iostream>

class A {
public:
	A()=default;
	//A(const A& a) = delete;				//如果放開,則編譯不通過
	A& operator=(const A& a) = delete;	    //禁用賦值構造不會影響 B b2(b1);這行,因爲此行調用拷貝構造而不是賦值構造

};


class B {

public:
	int i;
	A a;
};

int main()
{
	B b1;
	b1.i = 10;

	B b2(b1);

    std::cout << "Hello World!\n"; 
}

 

賦值構造函數:

原型:A& operator=(const A& a)

所有特性類比拷貝構造函數的四點。

注:如果成員變量中有類類型(不是指針),那麼就要求這個成員變量的類類型具備拷貝構造函數,注意,這裏同樣要求是具備拷貝構造,而不是要求具備賦值構造,可見編譯器的默認動作都是拷貝,而不是賦值。例子如下:

#include "pch.h"
#include <iostream>

class A {
public:
	A()=default;
	//A(const A& a) = delete;				//如果放開,則編譯不通過,賦值構造在進行類類型的成員變量拷貝賦值時使用的拷貝構造,而不是賦值構造
	A& operator=(const A& a) = delete;		//禁用賦值構造,不會影響B b2 = b1;這條語句,因爲編譯器在底層使用的是拷貝構造完成成員變量的拷貝賦值

};


class B {

public:
	int i;
	A a;
};

int main()
{
	B b1;
	b1.i = 10;

	B b2 = b1;

    std::cout << "Hello World!\n"; 
}

賦值構造函數的固定寫法:

A& A::operator=(const A& a){
    ...			//成員變量挨個賦值 ,以及其他的一些想要附加的動作
    return *this;		//(!)把指向自己的指針this的值返回,即就是自己的引用
}

//注:在賦值運算符中,切記不可對static成員變量進行賦值,即上面的 ... 中不能對
//static成員做賦值,一是沒意義,二是可能編譯不通過,三是即便通過運行起來可能會
//有問題

 

移動拷貝構造函數:

原型:A(const A&& a)

 

移動賦值構造函數:

原型:A& operator=(const A&& a)

 

上述五個構造函數之間的關聯:

正如本文開篇所屬,編譯器爲我們做了很多隱藏的動作,如果我們搞不明白這些動作的運行機制,那麼在開發的時候往往就是,丈二和尚摸不着頭腦,遇到問題也是一臉懵逼。下面就來梳理一下這些默認動作的相互關聯:

  • 如果用戶沒有定義任何構造函數,那麼編譯器爲我們生成默認構造函數,如果自行定義了隨便什麼樣的構造函數,那麼編譯器就不會爲我們生成默認構造函數。假如我們也沒有自行定義默認構造函數,那麼所有使用此類默認構造函數的地方都將編譯不通過。
  • 拷貝構造,賦值構造,移動拷貝構造,移動賦值構造,這四個構造函數編譯器會爲我們自動生成,除非發生如下情況:
  1. 一旦自行定義了上述四個構造函數中的任意一個,則其他三個必須也被定義,否則編譯器會將其他未定義的置爲 =delete;
  2. 編譯器創建默認的 “移動拷貝構造” 和 “移動賦值構造” 要滿足如下條件:所有非 static 都是可移動的;

 

三/五法則:

三:拷貝構造,賦值構造,析構函數,如果其中一個定義了,那麼其他兩個也要定義

五:拷貝構造,賦值構造,析構函數,移動拷貝構造,移動賦值構造,如果其中一個定義了,那麼其他四個也要定義

是對 三 的 擴展

 

案例:

案例一:

#include "stdafx.h"
#include <vector>
#include <algorithm>

using namespace std;

class A{
public:
	A()=default;
	~A()=default;
	A(const A&& tmp){
		this->vec = tmp.vec;
	}

public:
	vector<int> *vec;

public:
	void setvec(){
		vec = new vector<int>(10, 100);
	}
};

int _tmain(int argc, _TCHAR* argv[])
{
	A a1;
	a1.setvec();

	printf("a1 -> %p\n", a1.vec);

	A a2(move(a1));				//這裏用std::move來把類實例轉換成右值引用

	printf("a1 -> %p\n", a1.vec);
	printf("a2 -> %p\n", a2.vec);

	getchar();
	
	return 0;
}


	輸出: a1 -> 00495CA8
	       a1 -> 00495CA8		//只要未進行手動釋放,那麼原來的指針還是原來的位置
	       a2 -> 00495CA8 		//並沒有分配新內存,而是直接指過去

案例二:

#include "stdafx.h"
#include <vector>
#include <algorithm>

using namespace std;

class A{
public:
	A()=default;
	~A()=default;
	A(A&& tmp){					//爲了能釋放源對象資源,這裏不使用const
		this->vec = tmp.vec;
		tmp.vec = nullptr;			//手動釋放
	}

public:
	vector<int> *vec;

public:
	void setvec(){
		vec = new vector<int>(10, 100);
	}
};

int _tmain(int argc, _TCHAR* argv[])
{
	A a1;
	a1.setvec();

	printf("a1 -> %p\n", a1.vec);

	A a2(move(a1));				//這裏用std::move來把類實例轉換成右值引用

	printf("a1 -> %p\n", a1.vec);
	printf("a2 -> %p\n", a2.vec);

	getchar();
	
	return 0;
}


        輸出: a1 -> 00495CA8
               a1 -> 00000000		//原對象的資源指針已經被釋放
               a2 -> 00495CA8           //並沒有分配新內存,而是直接指過去

 

 

其他:

    拷貝行爲 和 移動行爲

如果一個類沒有定義移動行爲(移動拷貝構造和移動賦值構造),但是定義了拷貝行爲(拷貝構造和賦值構造),那麼試圖通過move調用移動行爲時不會成功,取而代之,會只用拷貝行爲作爲替代。

A a1,a2;

a1 = move(a2);        //如果沒有移動賦值,那麼這句話會被編譯器翻譯爲  a1 = a2;

A a3(move(a1));        //如果沒有移動拷貝,那麼這句話會被編譯器翻譯爲  a3(a1);

    左值和右值

    左值:可以被賦值的值,指內存中一塊區域的代名詞,即變量名
    右值:不可以被賦值的值,是一個運算結果,不對應內存中的任何一塊區域,是一個表達式

    如果我們現在已經有一個左值,那麼怎麼把左值的實際值作爲常量賦給右值引用呢???

    int i = 100;
    int &lr_i = i;
    int &&rr_i = i;            //錯誤
    int &&rr_i = std::move(i);    //正確
    int &&rr_i = std::move(lr_i);    //正確

    我們可以使用move函數(c++11)來實現一個動作 “把左值裏的值提取來,同時把左值對應的內存銷燬, 然後

    把左值的值作爲右值賦值給右值引用“ ,注意這裏的 “提取出來”,我們可以理解 move 動作是  “把指定內存

    單元中的值提出來作爲常量” ,這樣就方便理解爲什麼是右值了。
    
    move的伴隨動作時銷燬左值對應的變量。即一旦調用move,下文將無法再訪問變量(實際上可以,爲什麼???)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章