C++幕后故事(八)--给我来一打对象

  1. C++幕后故事(八)–给我来一打对象
    1. 1.对象数组是怎么构造
    2. 2.对象数组是怎么析构
    3. 3.总结

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


转载请注明来源,欢迎对文章中的引用来源进行考证,欢迎指出任何有错误或不够清晰的表达。可以在下面评论区评论,也可以邮件至 635672377@qq.com

文章标题:C++幕后故事(八)--给我来一打对象

文章字数:1.7k

本文作者:刘世雄

发布时间:2020-01-10, 13:54:46

最后更新:2020-01-10, 06:08:34

原始链接:http://lsxcpp.com/2020/01/10/cpp-8-a-dozens-obj/

版权声明: "署名-非商用-相同方式共享 4.0" 转载请保留原文链接及作者。

目录