日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

C++類與對象深入之構造函數與析構函數詳解_C 語言

作者:Rookiep ? 更新時間: 2022-08-03 編程語言

對象的初始化和清理

生活中我們買的電子產品都基本會有出廠設置,在某一天我們不用時候也會刪除一些自己信息數據保證安全。C++中的面向對象來源于生活,每個對象也都會有初始設置以及對象銷毀前的清理數據的設置。

一:構造函數

對象的初始化和清理也是兩個非常重要的安全問題,一個對象或者變量沒有初始狀態,對其使用后果是未知。c++利用了構造函數解決上述問題,這兩個函數將會被編譯器自動調用,完成對象初始化和清理工作。對象的初始化和清理工作是編譯器強制要我們做的事情,因此如果我們不提供構造和析構,編譯器也會提供,編譯器提供的構造函數和析構函數是空實現。

構造函數是一個特殊的成員函數,名字與類名相同,實例化類對象時由編譯器自動調用,保證每個數據成員都有一個合適的初始值,并且在對象的生命周期內只調用一次

構造函數語法:類名(){}

1.1:構造函數的特性

構造函數是特殊的成員函數,需要注意的是,構造函數的名字雖然叫構造,但是構造函數的主要任務并不是開空間創建對象,而是初始化對象。

構造函數特征:

1. 構造函數,沒有返回值也不寫void

2. 函數名稱與類名相同

3. 構造函數可以有參數,因此可以發生重載

4. 程序在調用對象時候會自動調用構造,無須手動調用,而且只會調用一次

class Date
{
public:
	Date()
	{
		_year = 1;
		_month = 1;
		_day = 1;
	}
	Date(int year, int month, int day)
	{
		_year = year;
		_month = month;
		_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
int main()
{
	Date d1;                   //調用無參構造
	d1.Print();
	Date d2(2022, 5, 15);      //調用帶參的構造
	d2.Print();
	system("pause");
	return 0;
}

5. 如果類中沒有顯式定義的構造函數,則C++編譯器會自動生成一個無參的默認構造函數,一旦用戶顯式定義編譯器將不再生成。

6. 無參的構造函數和全缺省的構造函數都被稱為默認構造函數,并且默認構造函數只有一個。注意:無參構造函數、全缺省構造函數、以及我們沒顯式寫由編譯器默認生成的構造函數,都可以認為是默認構造函數。即不用傳參就可以調用的函數

class Date
{
public:
	Date(int year = 1, int month = 1, int day = 1)//默認全缺省構造函數
	{
	_year = year;
	_month = month;
	_day = day;
	}
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
};
int main()
{
	Date d1;                   
	d1.Print();
	Date d2(2022, 5, 15);     
	d2.Print();
	Date d3(2022);
	d3.Print();
	Date d4(2022, 10);
	d4.Print();
	system("pause");
	return 0;
}

1-1-1
2022-5-15
2022-1-1
2022-10-1
請按任意鍵繼續. . .

7. 默認生成構造函數對于內置類型成員變量不做處理,因為編譯器默認生成的構造函數都是空實現,對于自定義類型成員變量做出處理,相當于實例化對象自動調用該類的默認構造函數!如下述代碼中Date date和A _aa有什么區別呢?不都是實例化對象自動調用默認構造函數嗎?。?!

代碼示例:

class A
{
public:
	A(){
		cout << " A()" << endl;
		_a = 0;
	}
private:
	int _a;
};
class Date
{
public:
	void Print()
	{
		cout << _year << "-" << _month << "-" << _day << endl;
	}
private:
	int _year; // 年
	int _month; // 月
	int _day; // 日
	A _aa;
};
int main(){
	Date date;
	date.Print();
	system("pause");
	return 0;
}
 A()

?-858993460--858993460--858993460
請按任意鍵繼續. . .

默認構造函數不會對自己的變量初始化,會對自定義類型處理,自定義類型成員會去調用它的默認構造函數!因為這里實例化對象也只能調用默認構造函數!!!(如果自定義類型的構造函數沒有顯示定義,也會是隨機值)。

接下來我們利用代碼詳細看看上面這段話:

示例1:默認生成的默認構造函數

class Stack
{
public:
private:
	int* _a;
	int _top;
	int _capacity;
};
class MyQueue {
public:
	// 默認生成構造函數就可以用了
	void push(int x) {
	}
	int pop() {
	}
private:
	Stack _st1;
	Stack _st2;
};
int main()
{
	MyQueue q;
	q.push(1);
	//Stack st;
	system("pasue");
	return 0;
}

上面這段代碼是可以編譯過的,在Myqueue類中只有自定義類型,所以我們不需要寫構造函數,使用默認生成的即可。然后Myqueue類中聲明Stack類實例化對象時,會去調用Stack類的默認構造函數(這里是自動生成的默認構造函數),編譯通過!

這里咋們看監視界面:

由于Stack的默認構造函數是默認生成的,同樣不會對內置類型成員變量做初始化,所以顯示是隨機值!

示例2:無參默認構造

class Stack
{
public:
	Stack()
	{
	_a = nullptr;
	_top = _capacity = 0;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
class MyQueue {
public:
	// 默認生成構造函數就可以用了
	void push(int x) {
	}
	int pop() {
	}
private:
	Stack _st1;
	Stack _st2;
};
int main()
{
	MyQueue q;
	q.push(1);
	//Stack st;
	system("pasue");
	return 0;
}

這段代碼也是可以編譯過的,在Myqueue類中只有自定義類型,所以我們不需要寫構造函數,使用默認生成的即可。然后Myqueue類中聲明Stack類實例化對象時,會去調用Stack類的默認構造函數(這里是咋們自己提供的默認構造函數),編譯通過!

同樣的,我們看監視界面:

由于Stack的默認構造函數是是我們自己提供的,同時對內置類型做了初始化,所以這里的各個值不再是隨機值!

示例3:全缺省默認構造函數

class Stack
{
public:
	Stack(int capacity = 10)
	{
	_a = (int*)malloc(sizeof(int)*capacity);
	assert(_a);
	_top = 0;
	_capacity = capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
class MyQueue {
public:
	// 默認生成構造函數就可以用了
	void push(int x) {
	}
	int pop() {
	}
private:
	Stack _st1;
	Stack _st2;
};
int main()
{
	MyQueue q;
	q.push(1);
	//Stack st;
	system("pasue");
	return 0;
}

這段代碼也是可以編譯過的,在Myqueue類中只有自定義類型,所以我們不需要寫構造函數,使用默認生成的即可。然后Myqueue類中聲明Stack類實例化對象時,會去調用Stack類的默認構造函數(這里是咋們自己提供的全缺省默認構造函數),編譯通過!

同樣的,觀察監視界面:

我們通過全缺省默認構造函數對各個值做出了初始化,因此不再是隨機值!

錯誤示例:有參構造函數

class Stack
{
public:
	Stack(int capacity)
	{
	_a = (int*)malloc(sizeof(int)*capacity);
	assert(_a);
	_top = 0;
	_capacity = capacity;
	}
private:
	int* _a;
	int _top;
	int _capacity;
};
class MyQueue {
public:
	// 默認生成構造函數就可以用了
	void push(int x) {
	}
	int pop() {
	}
private:
	Stack _st1;
	Stack _st2;
};
int main()
{
	MyQueue q;
	q.push(1);
	//Stack st;
	system("pasue");
	return 0;
}

程序報錯!

因為我們在Stack類中提供了一個有參構造函數,這時Stack類中不再有默認構造函數,因此Myqueue的默認構造函數無法調用Stack的默認構造函數,編譯不通過!

C++11還支持在聲明的時候給自定義類型變量賦一個缺省值:

class MyQueue
{
private:
	int _size = 0;
	Stack _st1;
	Stack _st2;
};

總結:如果一個類中的成員全是自定義類型,我們就可以不寫構造函數,就用默認生成的構造函數。如果有內置類型的成員,或者需要顯示傳參初始化,那么都要自己實現構造函數。

1.2:構造函數的分類

兩種分類方式:

  • 按參數分為: 有參構造和無參構造
  • 按類型分為: 普通構造和拷貝構造

二:析構函數

2.1:概念

與構造函數功能相反,析構函數不是完成對象的銷毀,局部對象銷毀工作由編譯器完成。而對象在銷毀時會自動調用析構函數,完成類的一些資源清理工作

2.2:特性

語法:~類名(){}

1. 析構函數,沒有返回值也不寫void

2. 函數名稱與類名相同,在名稱前加上符號 ~

3. 析構函數不可以有參數,因此不可以發生重載

4. 程序在對象銷毀前會自動調用析構,無須手動調用,而且只會調用一次

class Person
{
public:
	//構造函數
	Person(){
		cout << "Person的構造函數調用" << endl;
	}
	//析構函數
	~Person(){
		cout << "Person的析構函數調用" << endl;
	}
};
void test01(){
	Person p;
}
int main() {
	test01();
	system("pause");
	return 0;
}

Person的構造函數調用
Person的析構函數調用
請按任意鍵繼續. . .

如結果表示,在對象創建之前編譯器自動調用了析構函數。

??同樣的,我們也可以自己提供析構函數來對類上的一些資源完成清理工作。

示例:

typedef int DataType;
class SeqList{
public:
	SeqList(int capacity = 10){
		_pData = (DataType*)malloc(capacity * sizeof(DataType));
		assert(_pData);
		_size = 0;
		_capacity = capacity;
	}
	~SeqList(){
		if (_pData){
			free(_pData);
			_pData = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	int * _pData;
	size_t _size;
	size_t _capacity;
};
int main(){
	SeqList Sq;
	system("pause");
	return 0;
}

如上述代碼,在構造函數中在堆區開辟了空間,這時就需要我們自己提供析構函數來釋放對應的空間。

注意:默認生成的析構函數,內置類型成員不做處理,自定義類型成員會去調用它的析構函數

三:拷貝構造函數

3.1:概念

在現實生活中我們會遇到兩個小孩長得一摸一樣,我們稱其為雙胞胎。

拷貝構造,顧名思義就是在創建對象的時候,創建一個與原對象一摸一樣的新對象。

構造函數:參數列表只有單個形參,該形參是對本類類型對象的引用(一般用const修飾),在用已存在的類類型對象創建新對象時由編譯器自動調用。

如下列代碼:

class Person {
public:
	//有參構造函數
	Person(int a) {
		age = a;
		cout << "有參構造函數!" << endl;
	}
	//拷貝構造函數
	Person(const Person& p) {      //<看這里,形參是對本類類型對象的引用
		age = p.age;
		cout << "拷貝構造函數!" << endl;
	}
	//析構函數
	~Person() {
		cout << "析構函數!" << endl;
	}
public:
	int age;
};
void test01()
{
	Person p1(18);
	//如果不寫拷貝構造,編譯器會自動添加拷貝構造,并且做淺拷貝操作
	Person p2(p1);  //Person p2 = Person(p1);
	cout << "p2的年齡為: " << p2.age << endl;
}
int main() {
	test01();
	system("pause");
	return 0;
}

3.2:特性

拷貝構造函數也是特殊的成員函數,其有如下特征:

  • 拷貝構造函數是構造函數的一個重載形式。
  • 拷貝構造函數的參數只有一個,且必須使用引用傳參,使用傳值方式會引發無窮遞歸調用(因為傳值傳參也會調用拷貝構造函數)。
  • 如果沒有顯示定義拷貝構造函數,系統生成默認的拷貝構造函數。默認的拷貝構造函數對象按內存存儲按字節序完成拷貝,這種通常稱為淺拷貝,或者值拷貝。

??淺拷貝:

  • 指向一塊空間,修改數據會相互影響。
  • 這塊空間析構時會釋放兩次,導致程序崩潰。

編譯器生成的默認拷貝函數已經完成了字節序的值拷貝了,那我們還需要自己實現嗎?我們看一段代碼實例:

typedef int DataType;
class SeqList{
public:
	SeqList(int capacity = 10){
		_pData = (DataType*)malloc(capacity * sizeof(DataType));
		assert(_pData);
		_size = 0;
		_capacity = capacity;
	}
	~SeqList(){
		if (_pData){
			free(_pData);
			_pData = nullptr;
			_capacity = 0;
			_size = 0;
		}
	}
private:
	int * _pData;
	size_t _size;
	size_t _capacity;
};
int main(){
	SeqList Sq1;//調用默認構造函數初始化Sq1
	SeqList Sq2(Sq1);
	system("pause");
	return 0;
}

代碼解釋:編譯器先調用默認構造函數初始化Sq1,然后用類對象Sq1初始化類對象Sq2,我們使用編譯器提供的默認拷貝構造函數實現淺拷貝,這時程序就會出現問題了!

雖然語法編譯能通過:

但是運行程序終究會是報錯的!

我們注意到在初始化Sq1的時候我們在堆區開辟了地址,如果我們這時淺拷貝初始化Sq2,那么在調用析構函數的時候會造成對同一塊空間重復釋放,所以造成程序崩潰!

3.3:拷貝構造函數調用時機

C++中拷貝構造函數調用時機通常有三種情況:

1. 使用一個已經創建完畢的對象來初始化一個新對象

class Person {
public:
	Person() {
		cout << "無參構造函數!" << endl;
		mAge = 0;
	}
	Person(int age) {
		cout << "有參構造函數!" << endl;
		mAge = age;
	}
	Person(const Person& p) {
		cout << "拷貝構造函數!" << endl;
		mAge = p.mAge;
	}
	//析構函數在釋放內存之前調用
	~Person() {
		cout << "析構函數!" << endl;
	}
public:
	int mAge;
};
//1. 使用一個已經創建完畢的對象來初始化一個新對象
void test01() {
	Person man(100); //p對象已經創建完畢
	Person newman(man); //調用拷貝構造函數
}
int main() {
	test01();
	system("pause");
	return 0;
}

2. 值傳遞的方式給函數參數傳值

//這里代碼都旨在說明目的,代碼不全!
void doWork(Person p1) {//相當于Person p1 = p;
	//
}
void test02() {
	Person p; //無參構造函數
	doWork(p);
}

3. 以值方式返回局部對象

//這里代碼都旨在說明目的,代碼不全!
Person doWork2(){
	Person p1;
	cout << (int *)&p1 << endl;
	return p1;
}
void test03(){
	Person p = doWork2();
	cout << (int *)&p << endl;
}
int main() {
	test03();
	system("pause");
	return 0;
}

這里可以看作Person p = p1;也相當于調用拷貝構造函數。

3.4:構造函數調用規則

默認情況下,c++編譯器至少給一個類添加3個函數:

  • 默認構造函數(無參,函數體為空)
  • 默認析構函數(無參,函數體為空)
  • 默認拷貝構造函數,對屬性進行值拷貝

構造函數調用規則如下:

  • 如果用戶定義有參構造函數,c++不在提供默認無參構造,但是會提供默認拷貝構造。
  • 如果用戶定義拷貝構造函數,c++不會再提供其他構造函數。

原文鏈接:https://blog.csdn.net/qq_43727529/article/details/124808402

欄目分類
最近更新