C++構造函數和析構函數的總結


1 前言

創建一個對象時候,常常需要作一些初始的工作,就像買房子的話,售房小姐就會問你是否需要家具,是否要精裝修等等的問題。注意,類的成員不能在聲明類的時候初始化的。
image

image

為了解決這個問題,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;
}

image

結果顯然是不同,使用一個對象初始化和在初始化后使用=號,前一個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的析構函數,輸出的結果如下圖

image

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

#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匿名對象會發生析構,而第二種接編譯器會比較智能,匿名對象會不析構直接轉正,編譯可以知道你后續要用這個對象來拷貝初始化新對象,於是直接用匿名對象取代新對象,這是一個編譯器的優化代碼的功能。

image

image

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
對象2
Viewer 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運算符使用的一般格式為:

image

用new分配數組空間時不能指定初值。如果由於內存不足等原因而無法正常分配空間,則new會返回一個空指針NULL,用戶可以根據該指針的值判斷分配空間是否成功。

image

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指針值


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM