C++中類的默認成員函數詳解

C++中,對於任意一個類,都會為我們提供4個默認的成員函數(如果我們不顯示的去聲明)——構造函數、析構函數、拷貝構造函數、賦值函數。這些函數在特定的情況下會被自動調用,但自動調用並不意味著它們能像用戶所期望的那樣能實現特定的功能或者完成特定的任務,更多的時候需要我們自己實現這些函數的功能

A();                         //默認的構造函數
~A();                        //析構函數
A(const A&);                 //默認的拷貝函數
A& operator = (const A& a);  //默認賦值函數

一、構造函數

構造函數是一個特殊的成員函數,名字與類名相同,通過類創建對象時由編譯器自動調用,保證每個數據成員都有 一個合適的初始值,並且在對象的生命周期內隻調用一次。構造函數的功能是由類的實現者實現,根據實際情況設計函數體和函數參數,構造函數必須有一個,或者可以有多個。

class Person
{
public:
   //無參的構造函數和全缺省的構造函數都稱為默認構造函數,並且默認構造函數隻能有一個。
   //註意:無參構造函數、全缺省構造函數、我們沒寫編譯器默認生成的構造函數,都可以認為是默認成員函數。
   Person(string name = "馮同學", int age = 18)
   {
     _name = name;
     _age = age;
   }
   //打印信息
   void Print() const
   {
     cout << "姓名:" << _name << "——年齡:" << _age << endl;
   }
   
private:
  string _name;
  int _age;
};

int main()
{
	Person A;//調用全缺省的構造函數
	Person B("風同學");//調用半缺省的構造函數
	Person C("瘋同學",20);
	A.Print();
	B.Print();
	C.Print();
}

在這裡插入圖片描述

關於編譯器生成的默認成員函數,很多人會有疑惑:在我們不實現構造函數的情況下,編譯器會生成默認的構造函數。但是看起來默認構造函數又沒什麼用?A對象調用瞭編譯器生成的默認構造函數,但是A對象_name是空字符串,_age依舊是隨機值。也就說在這裡編譯器生成的默認構造函數並沒有什麼用??

class Person
{
public:
	void Print() const
	{
		cout << "姓名:" << _name << "——年齡:" << _age << endl;
	}
private:
	string _name;
	int _age;
};

int main()
{
	Person A;
	A.Print();
}

在這裡插入圖片描述

解答:C++把類型分成內置類型(基本類型)和自定義類型。內置類型就是語法已經定義好的類型:如
int/char…,自定義類型就是我們使用class/struct/union自己定義的類型,看看下面的程序,就會發現
編譯器生成默認的構造函數會對自定類型成員B調用的它的默認成員函數

class B
{
public:
	B(int b)
	{
		_b = b;
	}
private:
	int _b = 0;
};

class A
{
private:
	int _a;
	B bb;
};
int main()
{
	A aa;
	return 0;
}

在這裡插入圖片描述

在A類中,用B類創建瞭一個bb對象,bb對象就會調用構造函數,因為是自定義的構造函數,編譯器就不會給出默認的構造函數,所以就會報錯。如果將B的構造函數改為無參的構造函數和全缺省的構造函數,那麼程序就對瞭,這樣也就證明瞭自定類型成員會調用的它的默認成員函數

構造函數的特點

函數名與類名相同。無返回值。對象實例化時編譯器自動調用對應的構造函數。構造函數可以重載。在定義類時,如果沒有定義構造函數,則C++編譯器會自動提供一個默認構造函數(沒有參數),一旦我們定義構造函數,C++編譯器就不會提供默認構造函數無參構造函數、全缺省構造函數、我們沒寫編譯器默認生成的構造函數,都可以認為是默認成員函數。並且默認構造函數隻能有一個(如果默認構造函數出現多個,在創建對象調用構造函數時,可能會出現二義性)

二、析構函數

與構造函數相反的是析構函數,析構函數不是完成對象的銷毀,局部對象銷毀工作是由編譯器完成的。而對象在銷毀時會自動調用析構函數,完成類的一些資源清理工作,例如在構造函數中,我們為成員變量申請瞭內存,我們就可以在析構函數中將申請的內存釋放

class Person
{
public:
	Person(string name = "馮同學", int age = 18)
	{
		_name = name;
		_age = age;
	}

	void Print() const
	{
		cout << "姓名:" << _name << "——年齡:" << _age << endl;
	}
	
	~Person()
	{
		cout << _name << "正在調用~Person()" << endl;
	}

private:
	string _name;
	int _age;
};

int main()
{
	Person A;
	Person B("風同學");
	Person C("瘋同學", 20);
	A.Print();
	B.Print();
	C.Print();
}

在這裡插入圖片描述

關於編譯器自動生成的析構函數,是否會完成一些事情呢?下面的程序我們會看到,編譯器生成的默認析構函數,對會自定類型成員調用它的析構函數。

class B
{
public:
	B(int b = 0)
	{
		_b = b;
	}

	~B()
	{
		cout << "正在調用~B()" << endl;
	}

private:
	int _b = 0;
};

class A
{

private:
	int _a;
	B bb;
};

int main()
{
	A aa;
	return 0;
}

在這裡插入圖片描述

析構函數的特點

析構函數名是在類名前加上字符 ~。無參數無返回值。一個類有且隻有一個析構函數。若未顯式定義,系統會自動生成默認的析構函數。對象生命周期結束時,C++編譯系統系統自動調用析構函數。

三、拷貝構造函數

拷貝構造函數是一個特殊的構造函數(構造函數的重載形式)。用基於同一類的已經存在的一個對象拷貝初始化另一個馬上創建的對象。

class Person
{
public:
	Person(string name = "馮同學", int age = 18)
	{
		_name = name;
		_age = age;
	}

	void Print() const
	{
		cout << "姓名:" << _name << "——年齡:" << _age << endl;
	}
	
	Person(const Person& p)
	{
		_name = p._name;
		_age = p._age;
	}
	
private:
	string _name;
	int _age;
};

int main()
{
	Person f("風同學", 20);
	Person l(f);
	f.Print();
	l.Print();
}

在這裡插入圖片描述

如果沒有定義拷貝構造函數,C++編譯器也會提供一個默認的拷貝構造函數,不過該函數實現的是一個淺拷貝功能(將拷貝源按字節序賦值給拷貝對象)。淺拷貝對內置類型基本存在什麼影響,但對於在堆上開辟的對象會存在安全隱患,來看看一下程序

class Person
{
public:
	Person(int age = 18)
	{
		_name = new string("馮同學");
		_age = age;
	}

	~Person()
	{
		cout << "正在調用~Person()——" << _name << endl;
		delete _name;
		_name = nullptr;
	}
	
private:
	string* _name;
	int _age;
};

int main()
{
	Person f(20);
	Person l(f);
}

在這裡插入圖片描述

在這裡插入圖片描述

通過打印的結果我們可以發現用對象 f 去拷貝構造對象 l 時,f 中的_name和 l 中的_name指向同一塊內存空間(010C5440),並且在調用析構函數時,對同一塊內存空間進行瞭兩次釋放,最終導致瞭程序崩潰,這就是淺拷貝帶來的程序安全隱患。
不過我們可以將淺拷貝轉換為深拷貝從而解決問題

//自己實現拷貝構造函數
Person(const Person& p)
	{
		_name = new string;
		*_name = *p._name;
		_age = p._age;
	}

在這裡插入圖片描述

先申請內存,在進行賦值就很好解決瞭淺拷貝問題

拷貝構造函數的特點

拷貝構造函數是構造函數的一個重載形式。拷貝構造函數的參數隻有一個且必須使用引用傳參,使用傳值方式會引發無窮遞歸調用。若未顯示定義,系統生成默認的拷貝構造函數。 默認的拷貝構造函數對象按內存存儲按字節序完成拷貝,這種拷貝我們叫做淺拷貝,或者值拷貝。

四、賦值函數(賦值運算符重載)

賦值函數和拷貝構造函數有點類似,不過賦值函數隻是把一個已存在的對象賦值給另一個已存在的對象,使得那個已存在的對象具有和原對象相同的狀態。

class Person
{
public:
	Person(string name = "馮同學", int age = 18)
	{
		_name = name;
		_age = age;
	}

	Person& operator=(const Person& p)
	{
		_name = p._name;
		_age = p._age;
	}
	
	void Print() const
	{
		cout << "姓名:" << _name << "——年齡:" << _age << endl;
	}
	
private:
	string _name;
	int _age;
};

int main()
{
	Person f("風同學", 21);
	Person l("鳳同學", 20);
	f = l;
	f.Print();
	l.Print();
}

在這裡插入圖片描述

賦值函數的特點

使用關鍵字operator(所有的運算符重載都會使用這個關鍵字)返回值為類的引用(返回*this)不能改變運算符的優先級/結合性/操作數個數一個類如果沒有顯式定義賦值運算符重載,編譯器也會生成一個,完成對象按字節序的值拷貝。

總結

到此這篇關於C++中類的默認成員函數詳解的文章就介紹到這瞭,更多相關C++成員函數內容請搜索WalkonNet以前的文章或繼續瀏覽下面的相關文章希望大傢以後多多支持WalkonNet!

推薦閱讀: