C++三大特性之多型
多型
編譯環境:VS2013
一、物件型別
在引入多型之前,我們先來看一下物件型別
二、多型性的概念
多型一詞最初來源於希臘語,意思是具有多種形式或形態的情形,在C++中是指同樣的訊息被不同型別的物件接收時導致不同的行為,這裡講的訊息就是指物件的成員函式的呼叫,而不同的行為是指不同的實現。也就是呼叫了不同的函式。
概念的給出總是那麼的抽象,我們來通過一個具體的例子來看看什麼是多型:
#include<iostream>
using namespace std;
int Add(int left
{
return left + right;
}
char Add(char left, char right)
{
return left + right;
}
double Add(double left, double right)
{
return left + right;
}
int main()
{
Add(1, 2);
Add('1', '2');
Add(1.2, 2.3);
system("pause");
return 0;
}
在此函式中,同樣的訊息加法運算,由於執行時傳入的引數型別不同,就會呼叫不同型別的函式,也就是會有不同的行為響應。
多型性從系統實現的角度來講可以劃分為兩類:靜態多型(也叫編譯時多型性)和動態多型(又稱執行時多型性),以前學過的函式過載和運算子的過載屬於靜態多型性,在程式編譯時就能決定呼叫的是哪一個函式,靜態多型是通過函式的過載來實現的(運算子過載實際上也屬於函式的過載)。動態多型性是程式執行過程中才動態地確定操作所針對的物件,執行時多型性是通過虛擬函式來實現的。下面我們就分別看一下靜態多型和動態多型。
三、靜態多型
靜態多型:編譯器在編譯期間完成的,編譯器根據函式實參的型別(可能會進行隱式型別轉換),可推
斷出要呼叫那個函式,如果有對應的函式就呼叫該函式,否則出現編譯錯誤。
四、動態多型
動態繫結:在程式執行期間(非編譯期)判斷所引用物件的實際型別,根據其實際型別呼叫相應的方
法。
使用virtual關鍵字修飾類的成員函式時,指明該函式為虛擬函式,派生類需要重新實現,編譯器將實現動態繫結。
下面我們通過一個例子來實現動態多型:
#include<iostream>
#include<windows.h>
using namespace std;
class CWashRoom //基類
{
public:
void GoToManWashRoom() //去男廁
{
cout << "Man--->Please Left" << endl;
}
void GoToWomanWashRoom() //去女廁
{
cout << "Woman--->Please Right" << endl;
}
};
class CPerson //基類
{
public:
virtual void GoToWashRoom(CWashRoom & _washRoom) = 0; //純虛擬函式
};
class CMan :public CPerson
{
public:
virtual void GoToWashRoom(CWashRoom & _washRoom)
{
_washRoom.GoToManWashRoom();
}
};
class CWoman :public CPerson //CPerson的派生類
{
public:
virtual void GoToWashRoom(CWashRoom & _washRoom) //對CPerson中的虛擬函式進行重寫
{
_washRoom.GoToWomanWashRoom();
}
};
void FunTest()
{
CWashRoom washRoom;
//隨機生成一個數,若此數的二進位制最後一位是一,則生成一個男人物件,否則生成女人物件
for (int iIdx = 1; iIdx <= 10; ++iIdx)
{
CPerson* pPerson;
int iPerson = rand() % iIdx;
if (iPerson & 0x01)
{
pPerson = new CMan;
}
else
{
pPerson = new CWoman;
}
pPerson->GoToWashRoom(washRoom);
delete pPerson;
pPerson = NULL;
Sleep(1000);
}
}
此函式就是隻有在執行時編譯器才能知道生成的物件型別,為動態型多型。
動態繫結的條件:
下面程式能通過編譯嗎?會列印什麼?
#include<iostream>
using namespace std;
class CBase
{
public:
virtual void FunTest1(int _iTest) { cout << "CBase::FunTest1()" << endl; }
void FunTest2(int _iTest) { cout << "CBase::FunTest2()" << endl; }
virtual void FunTest3(int _iTest1){ cout << "CBase::FunTest3()" << endl; }
virtual void FunTest4(int _iTest) { cout << "CBase::FunTest4()" << endl; }
};
class CDerived :public CBase
{
public:
virtual void FunTest1(int _iTest){ cout << "CDerived::FunTest1()" << endl; }
virtual void FunTest2(int _iTest){ cout << "CDerived::FunTest2()" << endl; }
void FunTest3(int _iTest1) { cout << "CDerived::FunTest3()" << endl; }
virtual void FunTest4(int _iTest1, int _iTest2)
{
cout << "CDerived::FunTest4()" << endl;
}
};
int main()
{
CBase* pBase = new CDerived;
pBase->FunTest1(0);
pBase->FunTest2(0);
pBase->FunTest3(0);
pBase->FunTest4(0);
pBase->FunTest4(0, 0);
system("pause");
return 0;
}
很顯然,此程式是通不過編譯的,但是,如果我們把main函式中最後一個函式呼叫遮蔽掉就可以通過編譯了,執行後結果如下圖所示:
下面我們來分析一下:
在第列印的第一行中列印了派生類的函式體內容,因為FunTest1在基類中是虛擬函式,在派生類中也對其進行了重寫。
在列印的第二行中列印了基類的函式體內容,因為FunTest2沒有在基類中被寫為虛擬函式。
在第三行中列印了派生類的函式體內容,說明了在基類中為虛擬函式,在派生類中重寫基類的虛擬函式時可以不用寫virtual關鍵字。
第四行列印了基類的函式體內容,因為派生類中沒有對基類的虛擬函式進行重寫。
最後說說出錯的那行程式碼,因為建立的是基類型別的物件,所以根本就不能訪問派生類自己的成員函式。
最後,我們來總結一下動態繫結條件:
1、派生類必須重寫基類的虛擬函式。
2、通過基類指標或引用呼叫基類的虛擬函式(該虛擬函式派生類必須要重寫)
在上面的例項中,我們多次提到了重寫這個概念,那重寫與我們以前接觸到的過載、重定義有什麼區別呢?我們來通過下面這張圖簡單看一下:
五、純虛擬函式
許多情況下,在基類中不能對虛擬函式給出有意義的實現,就把他說明為純虛擬函式,她的實現留給該基類的派生類去做。純虛擬函式在宣告虛擬函式時被“初始化”為0的函式。在成員函式的形參後面寫上=0,包含純虛擬函式的類叫做抽象類(也叫介面類),抽象類不能例項化出物件。純虛擬函式在派生類中重新定義以後,派生類才能例項化出物件。
純虛擬函式沒有函式體,後面的“=0”並不表示函式返回值為0,他只是形式上的作用,目的是告訴編譯系統“這是純虛擬函式”。
純虛擬函式的作用是在基類中為其派生類保留一個函式的名字,以便派生類根據需要對他進行定義。如果在基類中沒有保留函式的名字,則無法實現多型性。
class Person
{
virtual void Display () = 0; // 純虛擬函式
protected :
string _name ; // 姓名
};
class Student : public Person
{};
總結:
1、派生類重寫基類的虛擬函式實現多型,要求函式名、引數列表、返回值完全相同。(協變除外)
協變:函式返回值不同,基類返回基類物件的引用,派生類返回派生類物件的引用。
2、基類中定義了虛擬函式,在派生類中該函式始終保持虛擬函式的特性。
3、只有類的非靜態成員函式才能定義為虛擬函式,靜態成員函式不能定義為虛擬函式。
4、如果在類外定義虛擬函式,只能在宣告函式時加virtual關鍵字,定義時不用加。
5、建構函式不能定義為虛擬函式,雖然可以將operator=定義為虛擬函式,但最好不要這麼做,使用時容
易混淆
6、不要在建構函式和解構函式中呼叫虛擬函式,在建構函式和解構函式中,物件是不完整的,可能會
出現未定義的行為。
7、最好將基類的解構函式宣告為虛擬函式。(解構函式比較特殊,因為派生類的解構函式跟基類的析構
函式名稱不一樣,但是構成覆蓋,這裡編譯器做了特殊處理)
8、虛表是所有類物件例項共用的
六、虛表剖析
在上面我們多次提到虛表,對於有虛擬函式的類,編譯器都會維護一張虛表,那麼到底虛表在記憶體中是以什麼樣的方式存在呢?下面我們就來對不同情況下的虛表進行詳細剖析。
在分類剖析之前,我們先來看看虛表對物件在記憶體中的影響,我們先來看看下面這段程式碼:
#include<iostream>
using namespace std;
class Base
{
public:
Base()
{
_data = 10;
cout << "this=" << this << endl;
}
virtual ~Base()
{}
private:
int _data;
};
int main()
{
Base b;
cout << sizeof(b) << endl;
system("pause");
return 0;
}
我們先來看看這個Base類的大小是多少?
我們可以看到,sizeof(b)的值是8,比我們以前見過的類多出了四個位元組,這到底是怎麼回事呢?為了弄清楚這個問題,我們在上面也打印出了這個類在記憶體中的地址,我們來看看記憶體中那四個位元組到底是什麼。
在圖中我們可以觀察到我們給成員變數賦的值被放在了給類分配記憶體的下四個位元組,而在前四個位元組放了一個地址,我們根據這個地址在記憶體2追蹤到他裡面存放的內容,我們發現裡面存放的又是一個地址,
並且這個虛表最後以0結尾,至於這個地址又是什麼呢?我們在這裡不多做解釋,後面我們會詳細介紹,至少我們現在可以得出的是有虛擬函式的類大小多了四個位元組,這四個位元組裡面存了一個地址,指向了一張表,我們把這張表就叫做虛表。
在認識了虛表之後,下面我們就對不同型別下的虛表進行詳細介紹:
①沒有覆蓋(單繼承)
在上面一個例子中我們已經知道,在類類記憶體的前四個位元組存了一個地址,指向一張表,這張表中也是一些地址,現在我們就去驗證一下這些地址到底指向了哪裡?現在我們就用一段程式碼來實現以上功能。
#include<iostream>
using namespace std;
class Base
{
public:
Base()
{
_data = 10;
}
virtual void FunTest1()
{
cout << "Base::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "Base::FunTest2()" << endl;
}
virtual void FunTest3()
{
cout << "Base::FunTest3()" << endl;
}
private:
int _data;
};
class Derived : public Base
{
public:
virtual void FunTest4()
{
cout << "Derived::FunTest4()" << endl;
}
virtual void FunTest5()
{
cout << "Derived::FunTest5()" << endl;
}
virtual void FunTest6()
{
cout << "Derived::FunTest6()" << endl;
}
};
typedef void(*VFP)();
void Printvfp()
{
Base b;
VFP* vfp = (VFP*)(*(int*)&b); //取虛表內的指標賦給一個函式指標變數
while (*vfp)
{
(*vfp)();
cout << (int*)vfp << endl; //列印地址
++vfp; //函式指標向後偏移
}
cout << endl;
Derived d;
VFP* vfp2 = (VFP*)(*(int*)&d);
while (*vfp2)
{
(*vfp2)();
cout << (int*)vfp << endl;
++vfp2;
}
}
int main()
{
Printvfp();
system("pause");
return 0;
}
首先我對部分程式的功能實現做簡單的說明,其他的程式碼都不叫好理解,最不容易懂得地方估計就是我註釋的那幾行程式碼了,我們一起來看看到底表示什麼意思。
在這段程式碼中首先我們建立了一個Base類的物件b,上面我們已經說了,他的前四個位元組是一個虛表指標,下面才是自己的成員變數,我們先給出它的模型圖(圖一),而虛表指標指向了一個以00000000結尾的表的首地址,前面都是一些指標(圖二)
我們可以看到,我在函式的外部定義了一個函式指標,而建立完類的物件後的一句比較難理解,我們來分步解析:
我們在此程式中列印了虛表指標所指向的地址的內容及地址,我們來看看執行結果吧。
以紅色的線為分割線,上面的是基類的虛表指標所指向的內容,下面的是派生類中的虛表所指向的內容。因為派生類中的函式對基類中的函式沒有重寫,因此,這種情況被稱為沒有覆蓋。那麼有覆蓋又是哪種情況呢?我們一起來看看。
②有覆蓋(單繼承)
同樣,我們先把有覆蓋的這種情況程式碼話,然後再通過監視視窗和記憶體來分析結果
#include<iostream>
using namespace std;
class Base
{
public:
Base()
{
_data = 10;
}
virtual void FunTest1()
{
cout << "Base::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "Base::FunTest2()" << endl;
}
virtual void FunTest3()
{
cout << "Base::FunTest3()" << endl;
}
private:
int _data;
};
class Derived : public Base
{
public:
virtual void FunTest1()
{
cout << "Derived::FunTest1()" << endl;
}
virtual void FunTest5()
{
cout << "Derived::FunTest2()" << endl;
}
virtual void FunTest6()
{
cout << "Derived::FunTest4()" << endl;
}
};
typedef void(*VFP)();
void Printvfp()
{
Base b;
VFP* vfp = (VFP*)(*(int*)&b); //取虛表內的指標賦給一個函式指標變數
while (*vfp)
{
(*vfp)();
cout << (int*)vfp << endl; //列印地址
++vfp; //函式指標向後偏移
}
cout << endl;
Derived d;
VFP* vfp2 = (VFP*)(*(int*)&d);
while (*vfp2)
{
(*vfp2)();
cout << (int*)vfp << endl;
++vfp2;
}
}
int main()
{
Printvfp();
system("pause");
return 0;
}
這段程式碼和上一個例子變化不大,不同之處就是在派生類Derived中對基類的FunTest1和FunTest2進行了重寫。
下面我們來分析一下結果
基類中的虛表:
在除錯過程中,我通過監視視窗得到基類物件b的地址,然後輸入記憶體檢視視窗找到指向虛表的地址,然後根據虛表地址在記憶體2視窗中得到虛表內容,最後結合執行結果看,虛表中的地址是與基類中的虛擬函式的地址一一對應的。所以我們現在就可以得出虛表中存放的其實就是虛擬函式的地址。既然已經弄明白了虛表的指向內容,那麼我們就繼續用同樣的方法看看派生類中的虛表內容和基類有何不同呢?
檢視步驟和看基類虛表的一樣,我們就不多說了,主要看看不同的地方,我們發現在執行結果中被派生類重寫了的FunTest1和FunTest2呼叫了派生類的,而沒重寫的按原樣寫下來然後再把派生類自己的虛擬函式新增在後面。
下面我們就來總結一下派生類虛表的生成方式:
⑴先拷貝基類的虛擬函式表
⑵如果派生類重寫了基類的某個虛擬函式,就覆蓋同位置的基類虛擬函式
⑶跟上自己新定義的虛擬函式
在這裡還要提醒的一點就是:通過基類的引用或指標呼叫虛擬函式時,是要呼叫基類還是派生類的虛擬函式,要根據執行時引用或指標實際引用(指向)的型別確定,呼叫非虛擬函式則無論指向何種型別,都呼叫的是基類的函式。
③有覆蓋(多繼承)
首先,像上面一樣,我們先把這種情況用程式碼實現出來:
#include<iostream>
using namespace std;
class Base
{
public:
Base()
{
_data1 = 1;
}
virtual void FunTest1()
{
cout << "Base::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "Base::FunTest2()" << endl;
}
virtual void FunTest3()
{
cout << "Base::FunTest3()" << endl;
}
private:
int _data1;
};
class Base1
{
public:
Base1()
{
_data2 = 2;
}
virtual void FunTest4()
{
cout << "Base1::FunTest4()" << endl;
}
virtual void FunTest5()
{
cout << "Base1::FunTest5()" << endl;
}
virtual void FunTest6()
{
cout << "Base1::FunTest6()" << endl;
}
private:
int _data2;
};
class Derived : public Base,public Base1
{
public:
Derived()
{
_data3 = 3;
}
virtual void FunTest7()
{
cout << "Derived::FunTest7()" << endl;
}
int _data3;
};
typedef void(*VFP)();
void Printvfp()
{
Derived d;
Base& b = d;
VFP* vfp = (VFP*)(*(int*)&b);
cout << "Base vir Tab" << endl;
while (*vfp)
{
(*vfp)();
++vfp;
}
cout << endl;
Base1& b1 = d;
VFP* vfp2 = (VFP*)(*(int*)&b1);
cout << "Base1 vir Tab" << endl;
while (*vfp2)
{
(*vfp2)();
++vfp2;
}
cout << endl;
VFP* vfp3 = (VFP*)(*(int*)&d);
cout << "Derived vir Tab" << endl;
while (*vfp3)
{
(*vfp3)();
++vfp3;
}
}
int main()
{
Printvfp();
system("pause");
return 0;
}
我們先來通過除錯視窗和記憶體視窗看一下類在多繼承無覆蓋情況下虛表在記憶體中的儲存和派生類Derived的模型圖:
正如圖中所示,圖三為Derived在記憶體中儲存的模型,紅色為Base基類部分,綠色為Base1基類部分,最後的藍色部分為Derived的資料成員儲存。在多繼承無覆蓋的情況下,我們發現第一個基類的虛表中多了一個地址(圖四中用綠線畫出來部分),而這個地址到底是誰的地址呢?我們來看看執行結果
從結果中我們可以清晰的看出多出來的地址是派生類自己的虛擬函式的地址。
看到這裡我們會發現一個小小的問題,在監視視窗中也可以看這些虛表中函式的地址,為什麼還要到記憶體中去看呢?其實看過上面這個例子我們就會發現,多繼承中派生類的虛擬函式地址在監視視窗是看不見的,所以說監視視窗有時候提供給我們的資訊是不全面的。所以建議大家還是儘量在記憶體視窗檢視,避免被監視視窗的假象欺騙
④有覆蓋(多繼承)
看完多繼承的無覆蓋情況,我們來繼續分析多繼承有覆蓋的情況,原始碼如下:
#include<iostream>
using namespace std;
class Base
{
public:
Base()
{
_data1 = 1;
}
virtual void FunTest1()
{
cout << "Base::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "Base::FunTest2()" << endl;
}
virtual void FunTest3()
{
cout << "Base::FunTest3()" << endl;
}
private:
int _data1;
};
class Base1
{
public:
Base1()
{
_data2 = 2;
}
virtual void FunTest4()
{
cout << "Base1::FunTest4()" << endl;
}
virtual void FunTest5()
{
cout << "Base1::FunTest5()" << endl;
}
virtual void FunTest6()
{
cout << "Base1::FunTest6()" << endl;
}
private:
int _data2;
};
class Derived : public Base, public Base1
{
public:
Derived()
{
_data3 = 3;
}
virtual void FunTest1()
{
cout << "Derived::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "Derived::FunTest2()" << endl;
}
virtual void FunTest4()
{
cout << "Derived::FunTest4()" << endl;
}
virtual void FunTest7()
{
cout << "Derived::FunTest7()" << endl;
}
int _data3;
};
typedef void(*VFP)();
void Printvfp()
{
Derived d;
Base& b = d;
VFP* vfp = (VFP*)(*(int*)&b);
cout << "Base vir Tab" << endl;
while (*vfp)
{
(*vfp)();
++vfp;
}
cout << endl;
Base1& b1 = d;
VFP* vfp2 = (VFP*)(*(int*)&b1);
cout << "Base1 vir Tab" << endl;
while (*vfp2)
{
(*vfp2)();
++vfp2;
}
cout << endl;
VFP* vfp3 = (VFP*)(*(int*)&d);
cout << "Derived vir Tab" << endl;
while (*vfp3)
{
(*vfp3)();
++vfp3;
}
}
int main()
{
Printvfp();
system("pause");
return 0;
}
這個程式與上一個程式的區別就是在派生類中對基類的部分函式進行了重寫。我們來看看虛表有何變化?
大體看去和多繼承無覆蓋的查不了多少,區別就在於執行結果中用藍線畫出來的部分,由此可以看出派生類的虛表和上面單繼承有覆蓋中的一樣,都是先把基類中的虛表先複製一份,然後將重寫的函式把原來的覆蓋掉,再將派生類自己的虛擬函式加在第一個基類的虛表的後面。
⑤菱形虛擬繼承(無覆蓋)
原始碼:
#include<iostream>
using namespace std;
class Base
{
public:
Base()
{
_data1 = 1;
}
virtual void FunTest1()
{
cout << "Base::FunTest1()" << endl;
}
private:
int _data1;
};
class C1 :virtual public Base
{
public:
C1()
{
_data2 = 2;
}
virtual void FunTest2()
{
cout << "C1::FunTest2()" << endl;
}
private:
int _data2;
};
class C2 : virtual public Base
{
public:
C2()
{
_data3 = 3;
}
virtual void FunTest3()
{
cout << "C2::FunTest3()" << endl;
}
int _data3;
};
class Derived :public C1, public C2
{
public:
Derived()
{
_data4 = 4;
}
virtual void FunTest4()
{
cout << "Derived::FunTest4()" << endl;
}
int _data4;
};
typedef void(*VFP)();
void Printvfp()
{
Derived d;
cout << sizeof(d) << endl;
C1& c1 = d;
VFP* vfp = (VFP*)(*(int*)&c1);
cout << "Base vir Tab" << endl;
while (*vfp)
{
(*vfp)();
++vfp;
}
cout << endl;
C2& c2 = d;
VFP* vfp2 = (VFP*)(*(int*)&c2);
cout << "Base1 vir Tab" << endl;
while (*vfp2)
{
(*vfp2)();
++vfp2;
}
cout << endl;
VFP* vfp3 = (VFP*)(*(int*)&d);
cout << "Derived vir Tab" << endl;
while (*vfp3)
{
(*vfp3)();
++vfp3;
}
Base& b = d;
VFP* vfp4 = (VFP*)(*(int*)&b);
cout << "Base1 vir Tab" << endl;
while (*vfp4)
{
(*vfp4)();
++vfp4;
}
}
int main()
{
Printvfp();
system("pause");
return 0;
}
在這個程式中我先計算了派生類Derived的大小sizeof(d)=36。因為是虛擬繼承,所以在有虛擬表地址的情況下多了兩個偏移量地址,在記憶體視窗中我只分析了C1的情況,C2的與之相似。同時畫出了派生類的記憶體分配模型圖如下圖:
這個例子主要就是想說明虛擬繼承比其他的繼承在記憶體中多了偏移量表。
⑥虛繼承
最後我們來討論一組特殊的繼承,虛繼承。
原始碼實現:
class Base
{
public:
virtual void FunTest1()
{
cout << "Base::FunTest1()" << endl;
}
public:
int _data1;
};
class Derived :virtual public Base
{
public:
virtual void FunTest1()
{
cout << "Derived::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "Derived::FunTest2()" << endl;
}
int _data2;
};
typedef void(*VFP)();
void Printvfp()
{
Derived d;
d._data1 = 1;
d._data2 = 2;
Base& b1 = d;
VFP* vfp = (VFP*)(*(int*)&b1);
cout << "Base vir Tab" << endl;
while (*vfp)
{
(*vfp)();
++vfp;
}
cout << endl;
VFP* vfp2 = (VFP*)(*(int*)&d);
cout << "Derived vir Tab" << endl;
while (*vfp2)
{
(*vfp2)();
++vfp2;
}
}
int main()
{
cout << sizeof(Derived) << endl;
Printvfp();
system("pause");
return 0;
}
在這個程式中,我們主要看一下派生類Derived的大小,
虛繼承的特點就是把基類放在了下面,但我們也可以看到派生類的大小是20,我們姑且記住這個大小,再來看看下面這個程式。
#include<iostream>
using namespace std;
class Base
{
public:
virtual void FunTest1()
{
cout << "Base::FunTest1()" << endl;
}
public:
int _data1;
};
class Derived :virtual public Base
{
public:
Derived()
{}
~Derived()
{}
virtual void FunTest1()
{
cout << "Derived::FunTest1()" << endl;
}
virtual void FunTest2()
{
cout << "Derived::FunTest2()" << endl;
}
int _data2;
};
typedef void(*VFP)();
void Printvfp()
{
Derived d;
d._data1 = 1;
d._data2 = 2;
Base& b1 = d;
VFP* vfp = (VFP*)(*(int*)&b1);
cout << "Base vir Tab" << endl;
while (*vfp)
{
(*vfp)();
++vfp;
}
cout << endl;
VFP* vfp2 = (VFP*)(*(int*)&d);
cout << "Derived vir Tab" << endl;
while (*vfp2)
{
(*vfp2)();
++vfp2;
}
}
int main()
{
cout << sizeof(Derived) << endl;
Printvfp();
system("pause");
return 0;
}
在這個程式中,與上一個程式不同的是在派生類中多了構造和解構函式。我們再來分析一下記憶體儲存中有什麼不同。
可以看到,派生類的大小多了4個位元組,從記憶體中可以看到這四個位元組存了0,這個0其實也沒什麼作用,因為從底層實現(彙編)沒看到這個0做了什麼工作,我們只能理解為起分隔作用。現在我們就要討論什麼原因導致加了這個0,很明顯,這個程式比上面一個程式就只多了構造和解構函式,所以肯定和這兩個函式有關,但要提醒大家的是只要構造和解構函式有一個(當然也包括六個都有)大小就會加4,也就是會加起分隔作用的0.
最後我們再來看一下建構函式在底部到底做了哪些工作?我們來通過反彙編來分析一下:
003937C0 push ebp
003937C1 mov ebp,esp
003937C3 sub esp,0CCh
003937C9 push ebx
003937CA push esi
003937CB push edi
003937CC push ecx
003937CD lea edi,[ebp-0CCh]
003937D3 mov ecx,33h
003937D8 mov eax,0CCCCCCCCh
003937DD rep stos dword ptr es:[edi]
003937DF pop ecx
003937E0 mov dword ptr [this],ecx
003937E3 cmp dword ptr [ebp+8],0
003937E7 je Derived::Derived+3Eh (03937FEh)
003937E9 mov eax,dword ptr [this]