這節我們的知識點就兩個:
1.對象數組是如何構造的。
2.對象數組是如何析構的。
在C++幕後故事(七)中我們詳細的解析了一個對象的生與死,在瞭解了一個對象的生與死的過程中基礎上,這一次我們要一次性搞清楚多個對象的是如何構造和析構的。
1.對象數組是怎麼構造
看代碼:
int g_number = 0;
class ObjClass
{
public:
explicit ObjClass() : mCount(g_number++)
{ cout << "ObjClass ctor" << endl; }
~ObjClass()
{
cout << "~ObjClass ctor" << endl;
mCount = g_number++;
}
private:
int mCount;
};
void test_object_array_ctor_dtor()
{
ObjClass *objarr = new ObjClass[12];
// 0x001D59CC ObjClass ctor:0
// 0x001D59D0 ObjClass ctor:1
// 0x001D59D4 ObjClass ctor:2
// 0x001D59D8 ObjClass ctor:3
// 0x001D59DC ObjClass ctor:4
// 0x001D59E0 ObjClass ctor:5
// 0x001D59E4 ObjClass ctor:6
// 0x001D59E8 ObjClass ctor:7
// 0x001D59EC ObjClass ctor:8
// 0x001D59F0 ObjClass ctor:9
// 0x001D59F4 ObjClass ctor:a
// 0x001D59F8 ObjClass ctor:b
delete[] objarr;
// 0x001D59F8 ~ObjClass ctor:c
// 0x001D59F4 ~ObjClass ctor:d
// 0x001D59F0 ~ObjClass ctor:e
// 0x001D59EC ~ObjClass ctor:f
// 0x001D59E8 ~ObjClass ctor:10
// 0x001D59E4 ~ObjClass ctor:11
// 0x001D59E0 ~ObjClass ctor:12
// 0x001D59DC ~ObjClass ctor:13
// 0x001D59D8 ~ObjClass ctor:14
// 0x001D59D4 ~ObjClass ctor:15
// 0x001D59D0 ~ObjClass ctor:16
// 0x001D59CC ~ObjClass ctor:17
}
從打印的結果可以看出來,構造的時候地址都在遞增的過程。但是析構的過程卻是遞減的過程。構造的時候是從第一個對象到最後一個對象,但是析構的卻是從最後一個對象開始析構再到第一個對象。這個過程是不是十分像出棧和入棧一樣。同時我又聯想到存在繼承關係對象的構造和析構也是這樣的過程(先是構造父類,在構造自己。析構時先是析構自己,再去析構父類)。感覺棧的概念在整個計算機中真的是隨處可見。
好,我們看下彙編代碼一窺究竟。
我節選下重要的代碼我們一起學習下。
; 申請分配的內存大小
00DE309D push 34h
00DE309F call operator new[] (0DD145Bh)
; 設置多少個對象
00DE30D3 push 0Ch
; 設置每個對象的大小
00DE30D5 push 4
00DE30D7 mov ecx,dword ptr [ebp-0F8h]
; 跳過前四個字節
00DE30DD add ecx,4
00DE30E0 push ecx
00DE30E1 call `eh vector constructor iterator' (0DD1780h)
00DD1780 jmp `eh vector constructor iterator' (0DE4B90h)
00DE4BD7 mov eax,dword ptr [i]
00DE4BDA add eax,1
00DE4BDD mov dword ptr [i],eax
00DE4BE0 mov ecx,dword ptr [i]
00DE4BE3 cmp ecx,dword ptr [count]
; 大於[count]跳出循環
00DE4BE6 jge `eh vector constructor iterator'+69h (0DE4BF9h)
00DE4BE8 mov ecx,dword ptr [ptr]
; 調用ObjClass構造函數
00DE4BEB call dword ptr [pCtor]
00DE4BEE mov edx,dword ptr [ptr]
; 將指針指向下一個對象的首地址
00DE4BF1 add edx,dword ptr [size]
00DE4BF4 mov dword ptr [ptr],edx
; 循環構造對象
00DE4BF7 jmp `eh vector constructor iterator'+47h (0DE4BD7h)
這個看起來還是有點不直觀,我翻譯成C++僞代碼看看。
; 分配內存
char *ptr = reinterpret_cast <char *>(operator new[](0x34));
if (ptr) {
*(reinterpret_cast<int *>(ptr)) = 0x0C;
; 跳過前4個字節
ptr += 4;
; 循環調用構造函數
for (int i = 0; i < 12; ++i) {
(*(reinterpret_cast<ObjClass *>(ptr))).ObjClass::ObjClass();
ptr += 4;
}
}
翻譯成僞代碼就好看多了,就順便解決了我們的幾個小疑問。
1.原來對象的大小在編譯期間就已經確定了,所以我們知道了第一個對象的地址就能夠知道後面的對象的地址,比如上面的對象是4byte,上面的彙編代碼push 4。
2.構造多少個對象的,也是編譯期間確定的,比如上面的初始化12個對象,上面的彙編代碼push 0Ch。
3.還有個疑問就是爲什麼我申請的對象數組大小應該爲4*12=48byte,但是實際上卻0x34=52字節。
打開VS的內存視圖,會看到如下的所示。
紅色部分就是對象真實佔用的內存。仔細再看黃色框的地方一個地址爲0x00EC59C8對應的值是0x0000000C,這時候我大概明白了怎麼回事。
原來編譯器背後幫我們多分配了4字節,這4個字節是爲了保存了對象的個數(這裏爲12),這樣做編譯器就知道需要調用多少次構造函數。
其實在分配內存不僅僅分配我們需要的內存,還會額外分配更多的內存,用來保存這塊內存的基本信息,比如上面的有個0x00000034標誌這塊內存的大小。
2.對象數組是怎麼析構
我們接着上面的代碼,接着看反彙編的代碼:
01352F83 call object_ctor_dtor_copy_semantic::ObjClass::`vector deleting destructor' (01341BC2h)
01341BC2 jmp object_ctor_dtor_copy_semantic::ObjClass::`vector deleting destructor' (013435E0h)
; 數組的首地址
01343616 push ecx
; 對象的大小
01343617 push 4
01343619 mov edx,dword ptr [this]
0134361C push edx
0134361D call `eh vector destructor iterator' (01341B77h)
01341B77 jmp `eh vector destructor iterator' (01354C70h)
; 這裏size就是對象的大小爲4byte,
; 下面的三行代碼就是數組指針移動最後一個對象地址的末尾
01354CA7 mov eax,dword ptr [size]
01354CAA imul eax,dword ptr [count]
01354CAE add eax,dword ptr [ptr]
; 對象的個數,這裏爲12
01354CBB mov ecx,dword ptr [count]
; 每循環一次ecx減一
01354CBE sub ecx,1
01354CC1 mov dword ptr [count],ecx
; ecx小於0結束跳出循環
01354CC4 js `eh vector destructor iterator'+67h (01354CD7h)
01354CC6 mov edx,dword ptr [ptr]
; 因爲是從末尾處開始析構的,所以每次循環地址-4表示移動下一個對象的地址
01354CC9 sub edx,dword ptr [size]
01354CCC mov dword ptr [ptr],edx
; 傳遞每個對象對應的地址
01354CCF mov ecx,dword ptr [ptr]
; 調用對象的析構函數
01354CD2 call dword ptr [pDtor]
; 循環跳轉到0x01354CBB
01354CD5 jmp `eh vector destructor iterator'+4Bh (01354CBBh)
; 經過循環之後this指針指向的是第一個對象的地址
0134362A mov eax,dword ptr [this]
; 需要-4調整到保存0x0C的地址
0134362D sub eax,4
01343630 push eax
; 最後釋放內存
01343631 call operator delete[] (0134113Bh)
好,老規矩翻譯成僞代碼我們再看看。
// 對象數組析構僞代碼
char *delete_ptr = reinterpret_cast<char *>(operator new[](0x34));
if (delete_ptr) {
char *tempptr = delete_ptr;
// 跳過前面保存數組大小的4byte
tempptr += 4;
// 移動最後一個對象的位置
tempptr += 0x30;
for (int i = 12; i >= 0; --i) {
(*(reinterpret_cast<ObjClass *>(tempptr))).ObjClass::~ObjClass();
tempptr -= 4;
}
// 注意這裏是我們自己的模擬過程,直接這樣調用會造成崩潰,畢竟內存模型和真正的
// operatr new[]是不一樣的
operator delete[](delete_ptr);
}
我們再看下VS的內存視圖:
在內存釋放的過程中,遠遠不止52個字節被釋放,實際分配的內存比我們預計的還要多。
3.總結
在對象數組構造時,先是分配內存,然後再去循環調用每個對象的構造函數。
在對象數組析構時,先是調用最後一個對象的析構函數直到第一個對象的析構函數,最後再去釋放內存。
這裏畫一張簡陋點的內存圖,這個章節關於內存的分配我就淺嘗輒止,後面有機會我在詳細的寫寫內存分配的內容。