这节我们的知识点就两个:
1.对象数组是如何构造的。
2.对象数组是如何析构的。
在C++幕后故事(七)中我们详细的解析了一个对象的生与死,在了解了一个对象的生与死的过程中基础上,这一次我们要一次性搞清楚多个对象的是如何构造和析构的。
看代码:
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标志这块内存的大小。
我们接着上面的代码,接着看反汇编的代码:
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个字节被释放,实际分配的内存比我们预计的还要多。
在对象数组构造时,先是分配内存,然后再去循环调用每个对象的构造函数。
在对象数组析构时,先是调用最后一个对象的析构函数直到第一个对象的析构函数,最后再去释放内存。
这里画一张简陋点的内存图,这个章节关于内存的分配我就浅尝辄止,后面有机会我在详细的写写内存分配的内容。