C++幕後故事(八)--給我來一打對象

這節我們的知識點就兩個:

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的內存視圖,會看到如下的所示。

image

紅色部分就是對象真實佔用的內存。仔細再看黃色框的地方一個地址爲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的內存視圖:

image

在內存釋放的過程中,遠遠不止52個字節被釋放,實際分配的內存比我們預計的還要多。

3.總結

在對象數組構造時,先是分配內存,然後再去循環調用每個對象的構造函數

在對象數組析構時,先是調用最後一個對象的析構函數直到第一個對象的析構函數,最後再去釋放內存

這裏畫一張簡陋點的內存圖,這個章節關於內存的分配我就淺嘗輒止,後面有機會我在詳細的寫寫內存分配的內容。

image

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