C++幕後故事(七)–一個對象的生與死
這節裏面我們會學習到以下四點:
1.對象的生成時機
2.對象構造過程和POD類型
3.對象的複製語意
4.析構語意
1.對象生成的時機
根據對象的控制力度不同,對象的生成時機也是不一樣的。
我們可以把它分爲兩類:
1.new操作符用戶手動控制時機,隨時new,隨時生成。
2.編譯器控制下也是有細微的差別,請看下面的表格。
全局對象/全局靜態對象 | 構造先於main函數的,在main之前還有很多的準備工作 |
局部靜態對象 | 第一次調用的時候生成,第二次時不會在構造 |
局部對象 | 每次調用的時候都會生成 |
編譯器爲VS2013 x86,下面是代碼驗證:
/*
測試:對象的構造、析構、拷貝語意
*/
namespace object_ctor_dtor_copy_semantic
{
class Cat
{
public:
explicit Cat(const string &name) : mName(name) { cout << mName << endl; }
~Cat() { cout << "~" << mName << endl; }
private:
string mName;
};
// 全局對象
Cat g_Cat("global cat");
// 全局靜態對象
Cat g_s_Cat("global static cat");
void test_obj_ctor()
{
// 局部變量
Cat local_cat("local cat");
// 局部靜態對象
static Cat local_s_cat("local static cat");
}
};
int main(int argc, char *argv[])
{
cout << "------------start main------------" << endl;
object_ctor_dtor_copy_semantic::test_obj_ctor();
cout << "------------end main------------" << endl;
return 0;
}
// 打印的結果
// global cat
// global static cat
// ------------start main------------
// local cat
// local static cat
// ~local cat
// ------------end main------------
// ~local static cat
// ~global static cat
// ~global cat
關於局部對象這裏有個小技巧跟大家分享下:類的實例只有在真正需要的時候再初始化
void test_local_useless(bool find)
{
Dog dog;
// 返回操作,而這裏初始化的dog對象沒有任何的作用,平白無故的增加dog的構造函數調用降低效率
if (find) { return; }
// 應該將對象的初始化延遲到真正需要的時候在初始化
// 對dog的一系列操作
// ...
}
2.對象構造
2.1 構造函數做了什麼?
我們已經知道對象在什麼時候生成,但是對象在生成過程除了我們自己寫的構造函數裏面的動作,編譯器在幕後也幫我們做了很多的工作,這節我們就要搞清楚編譯器做了什麼。
這一節既然要分析,我們就來分析最複雜的模型,虛繼承+虛函數模型。因爲最難的搞懂了,那簡單的還不是毛毛雨。
看如下代碼:
class Point
{
public:
Point() : mX(1), mY(2) { cout << "point" << endl; }
virtual ~Point() { cout << "~point" << endl; }
protected:
int mX;
int mY;
};
class Point3D : public virtual Point
{
public:
Point3D() : mZ(3) { cout << "point3d" << endl; }
virtual ~Point3D() { cout << "~point3d" << endl; }
virtual void VirFun1() { cout << "~VirFun1" << endl; }
protected:
int mZ;
};
class Vertex : public virtual Point
{
public:
Vertex() : mAngle(4) { cout << "vertex" << endl; }
virtual ~Vertex() { cout << "~vertex" << endl; }
virtual void VirFun2() { cout << "~VirFun2" << endl; }
protected:
int mAngle;
};
class Vertex3D : public Point3D, public Vertex
{
public:
Vertex3D() { cout << "vertex3D" << endl; }
virtual ~Vertex3D() { cout << "~vertex3D" << endl; }
virtual void VirFun3() { cout << "~VirFun3" << endl; }
};
class PVertex : public Vertex3D
{
public:
PVertex() : mCount(5) { cout << "PVertex3D" << endl; }
virtual ~PVertex() { cout << "~PVertex3D" << endl; }
virtual void VirFun4() { cout << "~VirFun4" << endl; }
void setvalue(int value) { mY = value; }
protected:
int mCount;
};
void test_virtual_inherit_ctor()
{
PVertex pvertex;
// pvertex.PVertex::~PVertex();
// pvertex.setvalue(10);
// 露出海面的表象
// point
// point3d
// vertex
// vertex3D
// PVertex3D
// ~PVertex3D
// ~vertex3D
// ~vertex
// ~point3d
// ~point
}
int main()
{
test_virtual_inherit_ctor();
return 0;
}
調用函數,打印的結果如上面代碼中註釋的那樣,其實那只是冰山一角。我們先看看PVertex佈局是啥樣的。老規矩將上面的代碼保存爲main.cpp。
1.藉助VS2013開發人員命令提示,進入到main.cpp所在目錄。
2.運行命令cl /d1 reportSingleClassLayoutPVertex main.cpp
3.拿出重要的部分我們看看的
class PVertex size(40):
+---
| +--- (base class Vertex3D)
| | +--- (base class Point3D)
0 | | | {vfptr}
4 | | | {vbptr}
8 | | | mZ
| | +---
| | +--- (base class Vertex)
12 | | | {vfptr}
16 | | | {vbptr}
20 | | | mAngle
| | +---
| +---
24 | mCount
+---
+--- (virtual base Point)
28 | {vfptr}
32 | mX
36 | mY
+---
PVertex::$vftable@Point3D@:
| &PVertex_meta
| 0
0 | &Point3D::VirFun1
1 | &Vertex3D::VirFun3
2 | &PVertex::VirFun4
PVertex::$vftable@Vertex@:
| -12
0 | &Vertex::VirFun2
PVertex::$vbtable@Point3D@:
0 | -4
1 | 24 (PVertexd(Point3D+4)Point)
PVertex::$vbtable@Vertex@:
0 | -4
1 | 12 (PVertexd(Vertex+4)Point)
PVertex::$vftable@Point@:
| -28
0 | &PVertex::{dtor}
從導出的結構中看出,這個內存模型真是相當複雜,看着都有點頭暈目眩。當對象之間的關係複雜之後,甚至連對象的大小都有膨脹的感覺。
我們關係整理下:
從上面可以看出,PVertex虛函數(除了虛析構函數)是追加在Point3D vfptr表中,而析構函數則是放在Point vfptr表中。
我們把內存模型搞清楚了,剩下的簡單多了,我們下圖所示:
看了半天發現,其實還是很複雜,複雜到一頁word裝不下。整個的調用流程,感覺都是在不斷的設置虛表,設置虛基類表,不斷的重複。而我們寫的代碼只是其中的一小部分。
好,我們再簡化下這張圖。(紅色的線表示調用過程,藍色線表示回溯過程)
這樣看就簡潔多了,整個調用的流程也是一目瞭然。
問題1:但是是不是覺得有點奇怪,PVertex怎麼直接調用Point構造函數,不是應該下面這樣圖?
但是這樣的調用流程會造成將Point構造兩次,大大的降低效率。所以編譯器會決定由誰構造Point。關於virtual base class constructor如何被調用有着明確的定義:只有當一個完成的class object被定義出來(PVertex)時,它纔會被調用;如果object只是某個完整object的subobject(Point3D),它就不會被調用(摘自《深入探索C++對象模型》)。
舉個例子:
1.我們定義了一個Vertex3D對象,這時Point3D就是Vertex3D是個subobject對象,所以此時Point3D就不會調用Point構造函數。
2.定義了一個Point3D,它就是個完整的object,所以會直接調用Point構造函數。
問題2:在構造函數調用鏈中,我們發現整個過程都是在不斷的設置虛表地址和虛基類表地址。爲什麼要來回不斷的設置呢,在最開始的時候一次性搞定不就行了。
舉個例子:
在不同的對象域中不停的修改虛表和虛基類表地址做法我稱之爲入鄉隨俗
我們在構造PVertex時,PVertex先去構造Point。此時Point對象已經構造完畢是個完整的對象,但是PVertex還是殘缺對象。如果這個時候我們Point虛表地址還是PVertex的虛表地址。此時我們在Point構造函數間接調用到PVertex虛函數,而此時PVertex還未完全構造完畢(比如一些成員變量還未初始化),這時調用PVertex虛函數就存在安全風險。說的簡單點,在父類構造函數中就要把虛表地址設置爲父類自己的而不是子類的。虛基類表也是同樣的道理。同時會聯想到在對象析構的時候也是類似的。
如果感興趣的同學可以再看下彙編代碼,其實這裏的彙編代碼的思路就是非常的清晰,就是如何把內存給填滿的。我把代碼就放在最後面了。附錄1.1彙編代碼填充內存結構。
2.2 POD類型
所謂的POD全稱是Plain Old Data。基本數據類型、指針、union、數組、構造函數是 trivial 的 struct 或者 class。其實C的struct極其的相似。
看下面代碼:
class Dog
{
public:
int mSize;
int mAge;
};
void test_pod_type()
{
// 1.沒有加上括號,注意這裏的成員值都是隨機值
Dog *dog = new Dog;
// 2.加上括號,注意這裏的成員值都爲0
Dog *dog1 = new Dog();
}
但是結果卻大不相同。加上括號初始化,會將對象中的成員變量做初始化。但是沒有加上括號的對象中成員變量卻是個隨機值。
但是如果Dog有構造函數,但是裏面什麼都不做。上面的兩行初始化的結果卻是一樣的,對象中成員變量的值都是隨機值。
針對上面的代碼,做個表格更直觀點。
無構造函數 | 存在構造函數(未初始化) | 存在構造函數(初始化) | |
不帶()初始化 | 隨機值 | 隨機值 | 初始化爲0 |
帶()初始化 | 初始化爲0 | 隨機值 | 初始化爲0 |
所以最佳的實踐方式:給類加上構造函數同時給類中的成員變量賦初值,在構造對象的時候採用正規的做法加上括號。
3.對象的複製語意
一說到複製語意,我就想到了build設計模式,當然這兩者沒有強相關性,硬要說關聯那就是它們都是和對象的構造有關。
對象的複製語意分爲兩種,一種就是拷貝構造,還有一種就是賦值構造****(operator=)。但是有的同學,這兩種方式不能很好的區分。其實很簡單,拷貝構造是從無到有的過程,賦值構造重新賦值過程,用已經存在的對象去重新賦值另外一個已經存在的對象。(這裏不提及std::move構造)。
在複製過程中,編譯器也會爲我們提供默認的複製構造語意,我們把編譯器提供的叫做淺拷貝(bitwise copy)。在拷貝的時候,每個對象都擁有自己獨立的一份資源而不是共享資源,這種方式叫做深拷貝(memberwise copy)。
爲什麼會有兩種方式?
編譯器提供兩種方式,是因爲兩種方式各有優缺點。淺拷貝效率略高於深拷貝,但是存在資源釋放問題。深拷貝是把資源也會對應的拷貝一份,這樣就會造成效率的下降。當類中不含有任何的資源,那麼編譯器提供的淺拷貝就已經勝任任務。
最後如果我們不想要複製語意,可以將拷貝構造函數或者operator=設置爲private屬性。還可以使用c++11 delete語法禁止複製語意。
4.析構語意
對象的析構可以看成對象構造的逆向過程。對象的析構函數是個非常重要的函數,因爲在對象消失的那一刻對釋放資源,做一些清理的工作。
我們接着第二節對象構造裏面的代碼,畫下析構的流程。
這裏我就畫了簡易的示意圖,其實它裏面設置虛表和虛基類表地址的套路和它的構造流程是十分的相似,我就不再重複了。
5.總結
這一節提到的拷貝構造,賦值構造,析構函數被稱爲C++的big three,這三個函數十分的重要一定要時刻小心。看一個人寫的類文件,首先就要看從這三個函數開始,寫了也不能代表水平很高,但是不寫水平肯定不高。
附錄1:
1.彙編代碼填充內存結構
; 調用PVertex的構造函數
00983181 lea ecx,[pvertex]
00983184 call object_ctor_dtor_copy_semantic::PVertex::PVertex (09712CBh)
; 設置Point3D域的虛基類表
009767D2 mov eax,dword ptr [this]
009767D5 mov dword ptr [eax+4],98D7D4h
; 設置Vertex域的虛基類表
009767DC mov eax,dword ptr [this]
009767DF mov dword ptr [eax+10h],98D7E4h
; 調整this指針,指向Point域
009767E9 add ecx,1Ch
; 調用Point夠着函數
009767EC call object_ctor_dtor_copy_semantic::Point::Point (097193Dh)
; 設置point虛表地址
00976D33 mov eax,dword ptr [this]
00976D36 mov dword ptr [eax],98D68Ch
; 初始化成員變量的值
00976D3C mov eax,dword ptr [this]
00976D3F mov dword ptr [eax+4],1
00976D46 mov eax,dword ptr [this]
00976D49 mov dword ptr [eax+8],2
; 再將this指針調回爲pvertex的首地址
00976809 mov ecx,dword ptr [this]
; 調用Vertex3D
0097680C call object_ctor_dtor_copy_semantic::Vertex3D::Vertex3D (09713B1h)
00976F29 mov ecx,dword ptr [this]
00976F2C call object_ctor_dtor_copy_semantic::Point3D::Point3D (0971311h)
; 設置Point3D虛表
00976C5D mov eax,dword ptr [this]
00976C60 mov dword ptr [eax],98D6B8h
; 根據虛基類表找到偏移值
00976C66 mov eax,dword ptr [this]
00976C69 mov ecx,dword ptr [eax+4]
00976C6C mov edx,dword ptr [ecx+4]
00976C6F mov eax,dword ptr [this]
; 設置析構函數的虛表地址
00976C72 mov dword ptr [eax+edx+4],98D6C0h
; 根據上面找到的偏移值,初始化成員變量
00976C7A mov eax,dword ptr [this]
00976C7D mov dword ptr [eax+8],3
; 調整this指針,指向Vertex的首地址
00976F3A mov ecx,dword ptr [this]
00976F3D add ecx,0Ch
00976F40 call object_ctor_dtor_copy_semantic::Vertex::Vertex (0971429h)
; 設置Vertex虛表
0097707D mov eax,dword ptr [this]
00977080 mov dword ptr [eax],98D6F8h
; 根據虛基類表找到偏移值
00977086 mov eax,dword ptr [this]
00977089 mov ecx,dword ptr [eax+4]
0097708C mov edx,dword ptr [ecx+4]
0097708F mov eax,dword ptr [this]
; 設置析構函數的虛表地址
00977092 mov dword ptr [eax+edx+4],98D704h
; 根據上面找到的偏移值,初始化成員變量
0097709A mov eax,dword ptr [this]
0097709D mov dword ptr [eax+8],4
; 設置Vertex3D虛表地址
00976F49 mov eax,dword ptr [this]
00976F4C mov dword ptr [eax],98D73Ch
00976F52 mov eax,dword ptr [this]
; 設置Vertex3D虛基類表地址
00976F55 mov dword ptr [eax+0Ch],98D748h
00976F5C mov eax,dword ptr [this]
00976F5F mov ecx,dword ptr [eax+4]
00976F62 mov edx,dword ptr [ecx+4]
00976F65 mov eax,dword ptr [this]
; 設置Vertex3D析構函數的虛表地址
00976F68 mov dword ptr [eax+edx+4],98D754h
; 設置PVertex繼承Point3D的虛表地址
00976818 mov eax,dword ptr [this]
0097681B mov dword ptr [eax],98D7A0h
; 設置PVertex繼承的Vertex的虛表地址
00976821 mov eax,dword ptr [this]
00976824 mov dword ptr [eax+0Ch],98D7B0h
; 設置PVertex繼承的Point的虛表地址
0097682B mov eax,dword ptr [this]
0097682E mov ecx,dword ptr [eax+4]
00976831 mov edx,dword ptr [ecx+4]
00976834 mov eax,dword ptr [this]
00976837 mov dword ptr [eax+edx+4],98D7C4h
; 成員變量的初始化
0097683F mov eax,dword ptr [this]
00976842 mov dword ptr [eax+18h],5
eax,dword ptr [this]
00976824 mov dword ptr [eax+0Ch],98D7B0h
; 設置PVertex繼承的Point的虛表地址
0097682B mov eax,dword ptr [this]
0097682E mov ecx,dword ptr [eax+4]
00976831 mov edx,dword ptr [ecx+4]
00976834 mov eax,dword ptr [this]
00976837 mov dword ptr [eax+edx+4],98D7C4h
; 成員變量的初始化
0097683F mov eax,dword ptr [this]
00976842 mov dword ptr [eax+18h],5