詳解C++之類和對象

一.構造函數

1.構造函數的定義:

構造函數 是一個 特殊的成員函數,名字與類名相同 , 創建類類型對象時由編譯器自動調用 ,保證每個數據成員都有 一個合適的初始值,並且 在對象的生命周期內隻調用一次 。 其實構造函數的作用就是完成成員變量的初始化 ,但不同於c語言的初始化構造函數可以實在創造對象的同時就完成成員變量的初始化。

2.構造函數的特征:

1. 函數名與類名相同。

2. 無返回值。

3. 對象實例化時編譯器 自動調用 對應的構造函數。

4. 構造函數可以重載。

3.構造函數的實現:

構造函數的實現主要有三種,

1.當用戶沒有實現構造函數的話系統會默認創造一個,此時系統會將內置類型的成員變量賦予隨機值,而對於自定義類型的成員變量則會調用他們的構造函數。(註:內置類型一般指的是:int char double float等這些定義好的類型,自定義類型指的是:struct這種類型以及class類這種)。

2.當然用戶也可以自己實現構造函數,一種為無參構造

3.類一種為帶參構造,但是在帶參構造中我們使用全缺省構造。我們用代碼展示一下:

3.1.系統默認的構造函數

我們可以看到當我們沒有在Data類進行函數構造的時系統將會自己默認創建構造函數,對內置類型變量賦予隨機值,自定義類型調用自己的構造函數(若自定義類型也沒有定義構造函數那麼此例子中的_a1和_a2也會被賦予隨機值)

3.2無參構造

3.3 帶參構造

這裡出一個問題對於代碼風格造成的問題:成員變量year最後的結果是多少呢?

class A{public:A(int year){year = year;}private:int year;};int main(){A a(20);}

答案是:隨機值。那麼為什麼是隨機值呢?這裡主要是變量之間它采用瞭就近原則,所以等式左邊的year會直接尋找離他最近的變量所以會將等式右邊的year直接賦值給它自己,所以year最後的值就是隨機值。

我們繼續來說帶參的構造函數,我們一般推薦使用的是全缺省的構造函數(註:

無參的構造函數和全缺省的構造函數都稱為默認構造函數,並且默認構造函數隻能有一個。無參構造函數、全缺省構造函數、我們沒寫編譯器默認生成的構造函數,三者都可以認為是默認成員函數。

二 析構函數

構造函數時完成對象的初始化,那麼一個對象又是怎麼樣被銷毀的呢?

1.析構函數的定義

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

2.析構函數的特征

1. 析構函數名是在類名前加上字符 ~ 。

2. 無參數無返回值。

3. 一個類有且隻有一個析構函數。若未顯式定義,系統會自動生成默認的析構函數 。

4. 對象生命周期結束時, C++ 編譯系統系統自動調用析構函數。

這裡我們用棧的例子來說明析構函數的實現以及作用。

class Stack
	{
	public:
		Stack(int capacity = 4)
		{
			_a = (int*)malloc(sizeof(int)*capacity);
			if (_a == nullptr)
			{
				cout << "malloc fail" << endl;
				exit(-1);
			}
			_top = 0;
			_capacity = capacity;
		}
	//析構函數的實現
		~Stack()
		{
			// 像Stack這樣的類,對象中的資源需要清理工作,就用析構函數
			free(_a);
			_a = nullptr;
			_top = _capacity = 0;
		}
 	private:
		int* _a;
		int _top;
		int _capacity;
	};

這裡是完成構造函數,有自己定義的析構函數的效果。同構造函數一樣對於內置成員變量析構函數會置為隨機值,而自定義類型則會去調用他們的析構函數。

三 拷貝函數

如果某些時候我們需要去復制一個對象,這樣的話我們該怎麼樣去解決呢?

這裡我們就需要引入拷貝函數。那麼什麼叫做拷貝函數呢?我們應該去怎麼實現呢?有什麼註意事項呢?這裡我們一一來說道。

1.拷貝函數定義

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

2.拷貝函數的特性

1. 拷貝構造函數 是構造函數的一個重載形式 。

2. 拷貝構造函數的參數隻有一個且必須使用引用傳參,使用傳值方式會引發無窮遞歸調用 。

3. 若未顯示定義,系統生成默認的拷貝構造函數。 默認的拷貝構造函數對象按內存存儲按字節序完成拷 貝,這種拷貝我們叫做淺拷貝,或者值拷貝。

3.拷貝函數的實現

拷貝函數的實現分為兩種一種是系統默認,一種是自己定義。我們分別來看其效果

class A 
{
public:
	A() 
	{ 
         _a1 = 1;
		 _a2 = 2;
	}
	~A() 
	{
		cout << "A()" << endl;
	}
private:
	int _a1;
	int _a2;
};
 class Data
{
public:
	/*Data() 
	{
		_year = 2021;
		_month = 12;
		_day = 12;
	}*/
	//Data(int year, int month, int day) 
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}
	Data(int year = 2022, 
		int month = 12, 
		int day = 12) 
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
	A a;
};
 int main() 
{
	Data s;
	//拷貝函數的調用
	Data s2(s);
	return 0;
}

調用系統默認生成拷貝函數(註:這裡拷貝函數的拷貝對自定義類型和內置類型的成員變量處理都是一致的完成字節序的值拷貝)

圖1 調用系統默認生成的拷貝函數

圖2 調用用戶自己定義的拷貝函數

在這裡我們順便說一下在自定義拷貝函數的時候一定要使用引用不然會出現無限遞歸例如 Data(Data s){}正確的使用是Data (const Data & s){}其中const是為瞭保護原數據不被輕易改動。

class A 
{
public:
	A() 
	{ 
         _a1 = 1;
		 _a2 = 2;
	}
	~A() 
	{
		cout << "A()" << endl;
	}
private:
	int _a1;
	int _a2;
};
 class Data
{
public:
	/*Data() 
	{
		_year = 2021;
		_month = 12;
		_day = 12;
	}*/
	//Data(int year, int month, int day) 
	//{
	//	_year = year;
	//	_month = month;
	//	_day = day;
	//}
	Data( const Data &s) 
	{
		_year = s._year;
		_month = s._month;
		_day = s._day;
 	}
	Data(int year = 2023, 
		int month = 12, 
		int day = 12) 
	{
		_year = year;
		_month = month;
		_day = day;
	}
private:
	int _year;
	int _month;
	int _day;
	A a;
};
 int main() 
{
	Data s;
	//拷貝函數的調用
	Data s2(s);
	return 0;
}

我們可以發現s2均完整的賦值瞭s的內容,但是這裡真的就沒有問題瞭嗎?如果我們使用系統默認生成的拷貝函數成員變量中含有指針那麼會出現什麼樣的問題呢?

class String
{
public:
String(const char* str = "jack")
 {
_str = (char*)malloc(strlen(str) + 1);
strcpy(_str, str);
 }
~String()
 {
cout << "~String()" << endl;
free(_str);
 }
private:
char* _str;
};
int main()
{
String s;
String s1(s);
}

我們可以看到雖然雖然s1拷貝瞭s的內容但是最後系統還是拋出瞭錯誤那麼這個錯誤來自那裡呢?

我們看這幅圖

這裡就是我們之前說的系統默認生成的拷貝函數是淺拷貝,那麼怎麼去完成深拷貝我們後邊在繼續講解。

總結

本篇文章就到這裡瞭,希望能夠給你帶來幫助,也希望您能夠多多關註WalkonNet的更多內容!

推薦閱讀: