coding之痛:C++中編譯器爲類生成的幾個默認的函數

        當你定義一個空類的時候,c++編譯器會默認爲這個空類定義幾個函數:1. 空的構造函數 2. 拷貝構造函數 3. 析構函數 4. 賦值操作符函數(operator=)。

        在這裏,我想討論兩個問題,第一問題是這幾個默認定義的函數裏面有無虛擬函數,第二個問題是拷貝構造函數和賦值操作符函數的缺陷所在。

1. 默認定義的函數裏面有無虛擬函數?

        答案是沒有的。

        先補充一個知識點,定義了虛函數的類在分配內存時要多出4個字節的空間來存儲指向虛函數表頭的地址,可參見VC++,掀起你的蓋頭來

        構造函數不存在虛擬這一說法。析構函數在我們平時用的時候一般都會將其定義爲虛擬的,原因就是保證在將創建的子類對象空間賦給父類指針時,在使用delete方式釋放該父類指針指向的空間時,能夠先調用子類的析構函數來釋放資源;如果我們把析構函數定義爲非虛擬的,那麼在上面這種情況下,只會去調用父類的析構函數。那麼基於這種原因,按理來說,c++編譯器應該在默認定義析構函數時將其定義爲虛擬的。而c++編譯器並沒有這麼做,原因可能就是它無法預知開發人員在定義類時會賦予類什麼樣的行爲,而且定義虛方法會爲類空間多增加4個字節的內存PS: 這只是我的猜測,具體原因不詳。)。

        將賦值操作符函數定義爲虛擬的是沒有意義的!爲什麼這麼說,下面我舉個例子:

#include <iostream>
using namespace std;

class Parent
{
public:
	Parent(const int a) :m_a(a){}
	void			setA(const int a) { m_a = a; }
	int				a() const { return m_a; }
	virtual Parent& operator= (const Parent& other)
	{
		m_a = other.m_a;
		return *this;
	}

protected:
	int m_a;
};

class Son : public Parent
{
public:
	Son(const int a = 0) :Parent(a) {}
	virtual Son& operator= (const Son& other)
	{
		m_a = other.m_a+1;
		return *this;
	}
};

int _tmain(int argc, _TCHAR* argv[])
{
	Parent *s1 = new Son(5);
	Parent *s2 = new Son(10);
	cout<<"s1::m_a = "<<s1->a()<<endl;
	*s1 = *s2;
	cout<<"s1::m_a = "<<s1->a()<<endl;

	return 0;
}

在這個示例代碼裏面,我爲了區分是調用Parent::operator=方法還是調用Son::operator=方法,我在Son::operator=的賦值方法中賦m_a+1。我將這兩個operator=方法定義爲虛擬的一個目的是讓語句

*s1 = *s2;
執行的是Son::operator=方法,如果執行的是此方法,那麼第二個打印語句打印的數據應該爲11,如果執行的方法是Parent::operator=方法,那麼打印的數據應該爲10。看下VS運行的結果:


這個結果表明調用的是Parent::operator=方法,其實這個語句的另一個寫法就是:

s1->operator= (*s2);
這是爲什麼呢?C++類的轉換有兩種,一種是向上轉換,即將子類型轉換爲父類型,這種轉換可以隱式直接轉換即可;另一種是向下轉換,即將父類型轉換爲子類型,這種轉換需要使用dynamic_cast語法進行轉換。如果上面的語句是調用的Son::operator=方法,那麼可以繼續將這條語句補齊:

s1->operator= ((const Son&) *s2);
這個語句是錯誤的,父類轉自類需要使用dynamic_cast,所以編譯器執行的效果是調用的Parent::operator=方法。如果要讓編譯器調用Son::operator=方法,必須先將這兩個指針轉換爲子類指針,如下代碼:

dynamic_cast<Son&>(*s1) = dynamic_cast<Son&>(*s2);

所以將賦值操作符函數定義爲虛擬的是沒有意義的,最終還是需要轉換爲子類型來調用子類中重載的賦值操作符函數,將這個結果衍生一下就是,將任何帶有以當前子類作爲參數的函數定義爲虛擬的是沒有意義的,就比如operator==,operator+等等。所以上面的代碼應改爲:

#include <iostream>
using namespace std;

class Parent
{
public:
	Parent(const int a) :m_a(a){}
	virtual	~Parent() {}
	void	setA(const int a) { m_a = a; }
	int		a() const { return m_a; }
	Parent& operator= (const Parent& other)
	{
		m_a = other.m_a;
		return *this;
	}

protected:
	int m_a;
};

class Son : public Parent
{
public:
	Son(const int a = 0) :Parent(a) {}
	virtual	~Son() {}
	Son&	operator= (const Son& other)
	{
		m_a = other.m_a+1;
		return *this;
	}
};

int _tmain(int argc, _TCHAR* argv[])
{
	Parent *s1 = new Son(5);
	Parent *s2 = new Son(10);
	cout<<"s1::m_a = "<<s1->a()<<endl;
	dynamic_cast<Son&>(*s1) = dynamic_cast<Son&>(*s2);
	cout<<"s1::m_a = "<<s1->a()<<endl;

	system("pause");
	return 0;
}
注意:dynamic_cast只能轉換多態類型。聲明或繼承了虛函數的類叫做多態類。可參見Why does dynamic_cast only work if a class has at least 1 virtual method?

        另外關於一個在類的哪一層定義虛方法的問題,就比如析構函數,最好是在基類就定義析構函數爲虛函數,函數的虛擬性是存在繼承的特徵的,這樣在子類中即使不顯示將析構函數定義爲虛擬的,它還是虛擬函數。這個函數虛擬的繼承特性在普通的函數重載中也是一樣的,在父類中定義了一個普通的虛擬函數,那麼在子類中重實現這個函數時,即使不顯示將其定義爲虛擬的,那它還是虛擬的

2. 拷貝構造函數和賦值操作符函數的缺陷

        在實際的工程編碼當中,默認的拷貝構造函數和賦值操作符函數會給你帶來很多的麻煩!!

        默認的拷貝構造函數和賦值操作符函數均是採用的位拷貝(也稱“淺拷貝”)方式來給類的屬性變量進行賦值。拷貝方式分兩種,位拷貝(也稱“淺拷貝”)和值拷貝(也稱“深拷貝”),位拷貝是指將一個對象的內存映像原封不動地賦值給另一個對象,值拷貝則是將一個對象的內存映像所指向的值賦值給另一個對象。編譯器默認採用了一種共享的方式來定義拷貝的行爲,而實際上編譯器也只能做到如此,但是這樣一個特性飽受詬病,那爲何又要保留這個特性呢?C++語言之父Bjarne Strousrup在他寫的《The design and evolution of C++》中寫到這樣一句話:

        I personally consider it unfortunate that copy operations are defined by default and I prohibit copying of objects of many of my classes. However, C++ inherited its default assignment and copy constructors from C, and they are frequently used.

        首先這個特性是繼承自C語言的,其主要的一個目的主要是爲了與C語言兼容。這個特性既是C++的一個大的缺陷,也正式C++流形的一個主要原因。

        使用面向對象,大家就一定會使用設計模式,而設計模式可分爲兩大類:繼承的體系結構,聚合/組合的體系結構。在使用第二類設計模式時,相信大家都很能體會到這個特性的缺陷,爲了防止發生淺拷貝,我們“必須”去重載拷貝構造函數和賦值操作符函數。而除了重載這兩個默認函數外,另外一個途徑就是禁用這兩個函數,將這兩個函數定義爲私有的訪問類型。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章