1. 程式人生 > 其它 >C++建構函式和解構函式的總結

C++建構函式和解構函式的總結

1 前言

建立一個物件時候,常常需要作一些初始的工作,就像買房子的話,售房小姐就會問你是否需要傢俱,是否要精裝修等等的問題。注意,類的成員不能在宣告類的時候初始化的。

為了解決這個問題,C++編譯器提供了一個特殊的函式---建構函式(construction)來處理物件的初始化。建構函式是一種特殊的成員函式,與其他的函式不同,她不需要使用者來呼叫它,而是在建立物件時自動執行的。

2 建構函式和解構函式

2.1 建構函式和解構函式的概念

  1. 有關於建構函式的定義

    • C++中類可以定義和類名相同的特殊的成員函式,這種與類名一致的成員函式叫做建構函式
    • 建構函式在定義時可以有引數
    • 沒有任何返回型別的宣告
  2. 建構函式的呼叫

    • 自動呼叫:一般情況下C++編譯器會自動呼叫建構函式
    • 手動呼叫: 在一些情況下則是需要手動呼叫建構函式
  3. 解構函式的定義

    • C++類可以定義一個特殊的成員函式來對類進行清理,清理可以不太嚴謹,應該是類銷燬後需要進行的行為,語法為~ClassName()
    • 解構函式沒有引數,也沒有返回值
    • 解構函式在物件銷燬時自動被呼叫
    • 解構函式自動被C++編譯器呼叫

3 C++編譯器構造析構方案的優勢

3.1 設計建構函式和解構函式的原因

其實建構函式和解構函式的思想是從生活中而來的一種概念,生活中所有的物件,像手機、汽車出廠的時候必須進行初始化設定才可以到使用者的手中讓使用者使用。所以初始的狀態是物件普遍存在的一種狀態,那麼使用建構函式可以讓編譯來根據我們編寫的建構函式來初始化我們的物件,這樣不需要我們手動的對物件進行初始化,提高生產的效率。下面我們對比一下普通的方案和使用建構函式的方案。

普通方案:

  1. 為類提供一個publicinitializ函式
  2. 物件建立後立即呼叫initializ函式進行初始化工作

優缺點分析

  • initializ只是一個普通的函式,必須顯示的呼叫
  • 一旦忘記或者失誤導致物件沒有初始化,那麼結果是可以錯誤或者不確定

3.2 建構函式的分類以及呼叫規則

  • C++編譯器為我們使用者提供了三種物件初始化的方案
#include <iostream>
using namespace std;

class Test
{
public:
	Test();
	Test(int x);
	Test(int x, int y);

	~Test();

private:
	int x;
	int y;
};
// 無參造函式
Test::Test()
{
	this->x = 0;
	this->y = 0;
}
Test::Test(int x) 
{
	this->x = x;
}
// 有參造函式
Test::Test(int x1,int y1) 
{
	this->x = x1;
	this->y = y1;
}
// 解構函式
Test::~Test()
{
}

int main()
{
	// 1 在初始化直接呼叫,括號法
	Test t1(1, 2);
	// 2 C++編譯器對於=操作符進行了加強,可以使用=來進行操作
	Test t2 = 3;
	Test t3 = (4, 5);	
	// 3 直接顯式的呼叫建構函式
	Test t4 = Test(1); 
	return 0;  
}

3.3 拷貝建構函式的呼叫時機

為了可以更好的理解拷貝建構函式和建構函式的概念,我找了幾個典型的使用場景來舉例子給大家參考。

第一個場景和第二個場景

#include "iostream"
using namespace std;

class Test
{
public:
	Test() //無參建構函式 預設建構函式
	{
		cout << "我是無參建構函式" << endl;
        value = 0; 
	}
	Test(int test_val) //無參建構函式 預設建構函式
	{
		cout << "我是有參建構函式" << endl;
		value = test_val;
	}
	Test(const Test& obj)
	{
		cout << "我也是建構函式,我是通過另外一個物件obj2,來初始化我自己" << endl;
		value = obj.value + 10;
	}
	~Test()
	{
		cout << "我是解構函式,自動被呼叫了" << endl;
	}
	void getTestValue()
	{
		cout << "value的值" << value << endl;
	}
protected:
private:
	int value;
};
//單獨搭建一個舞臺來進行測試
void ObjShow01()
{
    /* 定義一個變數 */
	Test t0(5);
    Test t1(20);
    /* 使用賦值法來對t2進行初始化*/
    Test t2 = t0;
    t2.getTestValue();
    t1 = t0;  
    t1.getTestValue();
}
int main()
{
    ObjShow01();
    return 0;
}

結果顯然是不同,使用一個物件初始化和在初始化後使用=號,前一個C++編譯器會自動呼叫類中的拷貝建構函式,而後一個只是簡單的賦值,淺拷貝而已。同樣第一個場景也是類似的分析,只是和第一個場景不同是使用括號初始化。

第三個場景

#include "iostream"
using namespace std;

class Test
{
public:
	Test() //無參建構函式 預設建構函式
	{
		cout << "我是無參建構函式" << endl;
        value = 0; 
	}
	Test(int test_val) //無參建構函式 預設建構函式
	{
		cout << "我是有參建構函式" << endl;
		value = test_val;
	}
	Test(const Test& obj)
	{
		cout << "我也是建構函式,我是通過另外一個物件obj2,來初始化我自己" << endl;
		value = obj.value + 10;
	}
	~Test()
	{
		cout << "我是解構函式,自動被呼叫了" << endl;
	}
	void getTestValue()
	{
		cout << "value的值" << value << endl;
	}
protected:
private:
	int value;
};

void testFunction(Test obj)
{
	cout<<"test_function"<< endl;
	obj.getTestValue();
}
//單獨搭建一個舞臺來進行測試
void ObjShow03()
{
    /* 定義一個變數 */
	Test t1(10);
	testFunction(t1);
}
int main()
{
	ObjShow03();
    return 0;
}

這個案例是測試在函式呼叫物件時候,建構函式和解構函式是什麼一個過程,這個過程其實分析一下並不難。在ObjShow03先定義了Test t1(10);首先編譯會自動呼叫建構函式進行初始化,然後呼叫testFunction(t1);

這裡使用了Test中的拷貝建構函式來構造引數obj然後當testFunction執行完後執行obj物件的解構函式來析構obj最後退出ObjShow03函式呼叫t1的解構函式,輸出的結果如下圖

第四種場景,也是一種比較有意思的場景

#include "iostream"
using namespace std;

class Test
{
public:
	Test() //無參建構函式 預設建構函式
	{
		cout << "我是無參建構函式" << endl;
        value = 0; 
	}
	Test(int test_val) //無參建構函式 預設建構函式
	{
		cout << "我是有參建構函式" << endl;
		value = test_val;
	}
	Test(const Test& obj)
	{
		cout << "我也是建構函式,我是通過另外一個物件,來初始化我自己" << endl;
		value = obj.value + 10;
	}
	~Test()
	{
		cout << "我是解構函式,自動被呼叫了" << endl;
	}
	void getTestValue()
	{
		cout << "value的值" << value << endl;
	}
protected:
private:
	int value;
};

void ObjShow04()
{
	/* 第一種方式接 */
	Test test1;
	test1 = GetTest();
	/* 第二種方式接 */
	//Test test2 = GetTest();
}

/* 注意:初始化操作 和 等號操作 是兩個不同的概念 */
int main()
{
	ObjShow04();
    return 0;
}

這次我們直接放出兩種不同方式接的效果吧,第一張為第一種接的效果,第二張為第二種接的效果。為什麼了,第一種接在函式return匿名物件會發生析構,而第二種接編譯器會比較智慧,匿名物件會不析構直接轉正,編譯可以知道你後續要用這個物件來拷貝初始化新物件,於是直接用匿名物件取代新物件,這是一個編譯器的優化程式碼的功能。

3.4 預設建構函式

兩個特殊的建構函式

  • 預設的無參的建構函式

    當類當中是沒有定義建構函式時,編譯器會預設一個無參的建構函式,並且函式體為空

  • 預設的拷貝建構函式

    當類中沒有定義拷貝建構函式時,編譯器會提供一個預設的拷貝建構函式,簡單的進行成員變數的值的複製(注意這裡是值的複製)

3.5 建構函式的呼叫規則的研究

  1. 當類當中是沒有定義任意一個建構函式的時候,編譯會提供預設的無參建構函式和預設的拷貝建構函式
  2. 當類當中提供了拷貝建構函式時,C++不會提供無參的拷貝建構函式
  3. 預設的拷貝建構函式成員是簡單的賦值(這就涉及到深拷貝和淺拷貝的區別)

總結:只要你有手動的寫建構函式,那麼你就必須用

建構函式階段性總結

  • 建構函式是C++編譯器用於初始化物件的特殊函式
  • 建構函式在物件建立時會自動被呼叫
  • 建構函式和普通的函式都必須遵守過載的原則
  • 拷貝建構函式是函式正確初始化的重要保障
  • 必要的時候,必須手工的編寫拷貝建構函式來滿足我的需求

4 深拷貝和淺拷貝

為什麼會出現深拷貝和淺拷貝的情況

  • 預設的複製建構函式可以完成的是對於成員變數的值的簡單複製
  • 當物件的資料是指向堆的指標時,預設拷貝建構函式也只是對指標的值進行簡單的值的複製

我做了一個圖可以很好的表示深拷貝的過程

成員2成員2成員1成員1成員指標1成員指標1堆地址A堆地址A物件1物件1成員2成員2成員1成員1成員指標1成員指標1物件2物件2Viewer does not support full SVG 1.1

這兩個物件分指標都指向同一個堆地址,這顯然是不是我們希望的,這樣導致一個物件對指標的內容進行修改,則另外物件也同樣發生改變。

那麼如何解決深拷貝和淺拷貝的問題?

  • 顯式的提供copy建構函式
  • 顯式的提供過載=操作,不使用編譯器提供的淺拷貝建構函式
class Name
{
public:
	Name(const char *pname)
	{
		size = strlen(pname);
		pName = (char *)malloc(size + 1);
		strcpy(pName, pname);
	}
	Name(Name &obj)
	{
		//用obj來初始化自己
		pName = (char *)malloc(obj.size + 1);
		strcpy(pName, obj.pName);
		size = obj.size;
	}
	~Name()
	{
		cout<<"開始析構"<<endl;
		if (pName!=NULL)
		{
			free(pName);
			pName = NULL;
			size = 0;
		}
	}

	void operator=(Name &obj3)
	{
		if (pName != NULL)
		{
			free(pName);
			pName = NULL;
			size = 0;
		}
		cout<<"測試有沒有呼叫我。。。。"<<endl;

		//用obj3來=自己
		pName = (char *)malloc(obj3.size + 1);
		strcpy(pName, obj3.pName);
		size = obj3.size;
	}  

protected:
private:
	char *pName;
	int size;
};

//物件的初始化 和 物件之間=號操作是兩個不同的概念
void playObj()
{
	Name obj1("obj1.....");
	Name obj2 = obj1; //obj2建立並初始化

	Name obj3("obj3...");

	//過載=號操作符
	obj2 = obj3; //=號操作
	cout<<"test"<<endl;

}
void main61()
{
	playObj();
	syste

5 多個物件的構造和析構

5.1 物件的初始化列表

物件初始化列表出現原因

1.必須這樣做:

如果我們有一個類成員,它本身是一個類或者是一個結構,而且這個成員它只有一個帶引數的建構函式,沒有預設建構函式。這時要對這個類成員進行初始化,就必須呼叫這個類成員的帶引數的建構函式,

如果沒有初始化列表,那麼他將無法完成第一步,就會報錯。

2.類成員中若有const修飾,必須在物件初始化的時候,給const int m 賦值

當類成員中含有一個const物件時,或者是一個引用時,他們也必須要通過成員初始化列表進行初始化,

因為這兩種物件要在聲明後馬上初始化,而在建構函式中,做的是對他們的賦值,這樣是不被允許的。

3.C++中提供初始化列表對成員變數進行初始化

語法規則

Constructor::Contructor() : m1(v1), m2(v1,v2), m3(v3)

{

// some other assignment operation

}

3.注意概念

初始化:被初始化的物件正在建立

賦值:被賦值的物件已經存在

4.注意

成員變數的初始化順序與宣告的順序相關,與在初始化列表中的順序無關

初始化列表先於建構函式的函式體執行

6 建構函式和解構函式的呼叫順序研究

建構函式與解構函式的呼叫順序

  1. 當類中有成員變數是其它類的物件時,首先呼叫成員變數的建構函式,呼叫順序與宣告順序相同;之後呼叫自身類的建構函式
  2. 解構函式的呼叫順序與對應的建構函式呼叫順序相反

7 物件的動態建立和釋放

7.1 物件的動態建立和釋放

new和delete基本語法

  1. 在軟體開發過程中,常常需要動態地分配和撤銷記憶體空間,例如對動態連結串列中結點的插入與刪除。在C語言中是利用庫函式mallocfree來分配和撤銷記憶體空間的。C++提供了較簡便而功能較強的運算子new和delete來取代mallocfree函式。

  2. 注意: newdelete是運算子,不是函式,因此執行效率高。

  3. 雖然為了與C語言相容,C++仍保留malloc和free函式,但建議使用者不用malloc和free函式,而用new和delete運算子。new運算子的例子:
    new int; //開闢一個存放整數的儲存空間,返回一個指向該儲存空間的地址(即指標)
    new int(100); //開闢一個存放整數的空間,並指定該整數的初值為100,返回一個指向該儲存空間的地址
    new char[10]; //開闢一個存放字元陣列(包括10個元素)的空間,返回首元素的地址
    new int[5][4]; //開闢一個存放二維整型陣列(大小為5*4)的空間,返回首元素的地址
    float *p=new float (3.14159); //開闢一個存放單精度數的空間,並指定該實數的初值為//3.14159,將返回的該空間的地址賦給指標變數p

  4. newdelete運算子使用的一般格式為:

用new分配陣列空間時不能指定初值。如果由於記憶體不足等原因而無法正常分配空間,則new會返回一個空指標NULL,使用者可以根據該指標的值判斷分配空間是否成功。

7.2 類物件的動態建立和釋放

使用類名定義的物件都是靜態的,在程式執行過程中,物件所佔的空間是不能隨時釋放的。但有時人們希望在需要用到物件時才建立物件,在不需要用該物件時就撤銷它,釋放它所佔的記憶體空間以供別的資料使用。這樣可提高記憶體空間的利用率。

​ C++中,可以用new運算子動態建立物件,用delete運算子撤銷物件

比如:

  • Box *pt; //定義一個指向Box類物件的指標變數pt
  • pt=new Box; //在pt中存放了新建物件的起始地址
    在程式中就可以通過pt訪問這個新建的物件。如
    cout<<pt->height; //輸出該物件的height成員
    cout<<pt->volume( ); //呼叫該物件的volume函式,計算並輸出體積
    C++還允許在執行new時,對新建立的物件進行初始化。如
    Box *pt=new Box(12,15,18);

這種寫法是把上面兩個語句(定義指標變數和用new建立新物件)合併為一個語句,並指定初值。這樣更精煉。

新物件中的heightwidthlength分別獲得初值12,15,18。呼叫物件既可以通過物件名,也可以通過指標。

​ 在執行new運算時,如果記憶體量不足,無法開闢所需的記憶體空間,目前大多數C++編譯系統都使new返回一個0指標值。只要檢測返回值是否為0,就可判斷分配記憶體是否成功。

ANSI C++標準提出,在執行new出現故障時,就“丟擲”一個“異常”,使用者可根據異常進行有關處理。但C++標準仍然允許在出現new故障時返回0指標值