不考虑面向对象的情况下,C++ 当中的 struct
与 C 语言的 struct
的一个大不同就是C++
当中存在构造函数和析构函数。对象创建时,要调用构造函数;对象离开作用域,会调用析构函数。
c++
中 new
的几种形式
C++
中的 new
有 3 种形式,常见的用来在堆上构建对象的那个 new
,还有 operator new
和 placement new
. operator new
可以重载,其实 placement new
就是operator new
的一种重载形式。
内存分配调用
都知道new
一个对象,其实是调用 operator new
来分配内存的,然后再调用构造函数。operator new
内部调用 glibc
的 malloc
来分配内存,当然一般的还是有第三方的内存分配器库如tcmallo
和 jemalloc
作为 glibc
的内存分配器的替代品。malloc
会在起始地址额外分配几个字节,用来记录分配的内存的长度,这样 free
的时候才知道需要释放的内存长度。
在 C++
里,问题来了。在释放内存之前,需要先调用析构函数。对于 new
形式分配的内存,这没有问题,因为 delete
会调用析构函数。问题在于,对于new []
形式分配的数组,为啥需要delete []
来释放呢?问题的根源就在于只有delete [] 才能正确的调用数组所有成员的析构函数,并且将正确的地址传递给free进行释放。
所以delete[]
释放内存的步骤是首先获取数组的元素个数,然后依次对数组里的每个元素调用析构函数,最后将由底层内存分配器 malloc
分配的内存交由 free
进行释放。
源码面前,了无秘密
源码
struct T
{
int i = 0x1234;
~T (){
printf("delete\n");
}
};
int main()
{
T* ptr = new T[3];
delete [] ptr;
return 0;
}
在 Linux 下的 X64 平台编译之后,对得到的 elf
文件进行反编译。
int main()
{
400706: 55 push %rbp
400707: 48 89 e5 mov %rsp,%rbp
40070a: 41 55 push %r13
40070c: 41 54 push %r12
40070e: 53 push %rbx
40070f: 48 83 ec 18 sub $0x18,%rsp
T* ptr = new T[3];
400713: bf 14 00 00 00 mov $0x14,%edi #一共 20 个字节
400718: e8 c3 fe ff ff callq 4005e0 <operator new[](unsigned long)@plt>
40071d: 48 89 c3 mov %rax,%rbx #rbx 指向内存的起始地址
400720: 48 c7 03 03 00 00 00 movq $0x3,(%rbx) #前 8 个字节用来保存数据的元素个数
400727: 48 8d 43 08 lea 0x8(%rbx),%rax #rax 指向数组的起始地址
40072b: 41 bc 02 00 00 00 mov $0x2,%r12d #循环的次数
400731: 49 89 c5 mov %rax,%r13
400734: 4d 85 e4 test %r12,%r12 #判断循环是否结束
400737: 78 12 js 40074b <main+0x45> #循环结束跳转出去,返回数组的起始地址
400739: 4c 89 ef mov %r13,%rdi #r13 用来保存参数,传递给最终的参数寄存器 rdi
40073c: e8 9d 00 00 00 callq 4007de <T::T()>
400741: 49 83 c5 04 add $0x4,%r13 #r13 指向下一个地址,这个地址作为 this 指针来调用构造函数
400745: 49 83 ec 01 sub $0x1,%r12 #循环次数 -1
400749: eb e9 jmp 400734 <main+0x2e>
40074b: 48 8d 43 08 lea 0x8(%rbx),%rax
40074f: 48 89 45 d8 mov %rax,-0x28(%rbp) #赋值给栈上的局部变量 ptr
delete [] ptr;
400753: 48 83 7d d8 00 cmpq $0x0,-0x28(%rbp) # 判断 ptr 是否为空
400758: 74 59 je 4007b3 <main+0xad> #为 0 直接跳转到return 处,因为对空指针进行 delete 是合法的
40075a: 48 8b 45 d8 mov -0x28(%rbp),%rax
40075e: 48 83 e8 08 sub $0x8,%rax
400762: 48 8b 00 mov (%rax),%rax #rax #数组的起始地址往前偏移 8 个字节,就是获取数组的长度
400765: 48 8d 14 85 00 00 00 lea 0x0(,%rax,4),%rdx #rdx = rax * 4 = 12,4起始是数组成员的长度,所以这里就是获取数组的总内存字节长度
40076c: 00
40076d: 48 8b 45 d8 mov -0x28(%rbp),%rax
400771: 48 8d 1c 02 lea (%rdx,%rax,1),%rbx #rax是数组的起始地址,rdx 是数组的字节长度,所以 rbx 是数组的终止地址,用来作为循环结束的标志
400775: 48 3b 5d d8 cmp -0x28(%rbp),%rbx #判断循环是否结束
400779: 74 0e je 400789 <main+0x83>
40077b: 48 83 eb 04 sub $0x4,%rbx #终止地址往前 4 个直接,指向最后一个元素
40077f: 48 89 df mov %rbx,%rdi
400782: e8 3d 00 00 00 callq 4007c4 <T::~T()> #对最后一个元素进行析构
400787: eb ec jmp 400775 <main+0x6f> #从后往前,依次对所有元素进行析构
400789: 48 8b 45 d8 mov -0x28(%rbp),%rax
40078d: 48 83 e8 08 sub $0x8,%rax
400791: 48 8b 00 mov (%rax),%rax #rax = 3 为数组长度 总的内存的长度就是 4 * rax + 8 = 4 * (rax + 2),下边就是这两条指令,结果保存在 rdx 当中
400794: 48 83 c0 02 add $0x2,%rax
400798: 48 8d 14 85 00 00 00 lea 0x0(,%rax,4),%rdx #rdx = 20
40079f: 00
4007a0: 48 8b 45 d8 mov -0x28(%rbp),%rax
4007a4: 48 83 e8 08 sub $0x8,%rax
4007a8: 48 89 d6 mov %rdx,%rsi #rsi 当中的就是实际的内存长度,作为第二个参数
4007ab: 48 89 c7 mov %rax,%rdi #rdi 指向起始地址,作为第一个参数
4007ae: e8 5d fe ff ff callq 400610 <operator delete[](void*, unsigned long)@plt>
return 0;
4007b3: b8 00 00 00 00 mov $0x0,%eax
4007b8: 48 83 c4 18 add $0x18,%rsp
4007bc: 5b pop %rbx
4007bd: 41 5c pop %r12
4007bf: 41 5d pop %r13
4007c1: 5d pop %rbp
4007c2: c3 retq
4007c3: 90 nop
源码解释
T
类型本身是 4 个字节,所有 T[3]
数组的大小是 12 = 4 * 3
字节,但是调用 operator new
来分配的内存是0x14 = 12 + 8
字节,就是因为前边需要 8 个字节记录数组的元素的数量(数组的长度)。创建数组的时候,从数组的起始地址,第 0 个元素所在的地址,及operator new
最终的返回地址处,开始对象的构造。
400734: 4d 85 e4 test %r12,%r12 #r12保存循环测的次数,判断循环是否结束
400737: 78 12 js 40074b <main+0x45> #循环结束跳转出去,返回数组的起始地址
400739: 4c 89 ef mov %r13,%rdi #r13 用来保存参数,传递给最终的参数寄存器 rdi
40073c: e8 9d 00 00 00 callq 4007de <T::T()>
400741: 49 83 c5 04 add $0x4,%r13 #r13 指向下一个地址,这个地址作为 this 指针来调用构造函数
400745: 49 83 ec 01 sub $0x1,%r12 #循环次数 -1
400749: eb e9 jmp 400734 <main+0x2e>
40074b: 48 8d 43 08 lea 0x8(%rbx),%rax
析构时,是从最后一个元素开始析构,以是否达到起始元素的地址作为判断,进行所有元素的析构。
400771: 48 8d 1c 02 lea (%rdx,%rax,1),%rbx #rax是数组的起始地址,rdx 是数组的字节长度,所以 rbx 是数组的终止地址,用来作为循环结束的标志
400775: 48 3b 5d d8 cmp -0x28(%rbp),%rbx #判断循环是否结束
400779: 74 0e je 400789 <main+0x83>
40077b: 48 83 eb 04 sub $0x4,%rbx #终止地址往前 4 个字节,指向最后一个元素
40077f: 48 89 df mov %rbx,%rdi
400782: e8 3d 00 00 00 callq 4007c4 <T::~T()> #对最后一个元素进行析构
400787: eb ec jmp 400775 <main+0x6f> #从后往前,依次对所有元素进行析构
问题
那么对于内置基本类型没有析构函数,混用就没有问题吗?
int* ptr = new int[4];
delete [] ptr;
对应的汇编如下
40062e: bf 10 00 00 00 mov $0x10,%edi
400633: e8 e8 fe ff ff callq 400520 <operator new[](unsigned long)@plt>
400638: 48 89 45 f8 mov %rax,-0x8(%rbp)
40063c: 48 83 7d f8 00 cmpq $0x0,-0x8(%rbp)
400641: 74 0c je 40064f <main+0x29>
400643: 48 8b 45 f8 mov -0x8(%rbp),%rax
400647: 48 89 c7 mov %rax,%rdi
40064a: e8 e1 fe ff ff callq 400530 <operator delete[](void*)@plt>
如果是错误的调用不匹配的delete
delete ptr
对应的汇编如下
40064e: bf 10 00 00 00 mov $0x10,%edi
400653: e8 e8 fe ff ff callq 400540 <operator new[](unsigned long)@plt>
400658: 48 89 45 f8 mov %rax,-0x8(%rbp)
40065c: 48 8b 45 f8 mov -0x8(%rbp),%rax
400660: be 04 00 00 00 mov $0x4,%esi
400665: 48 89 c7 mov %rax,%rdi
400668: e8 e3 fe ff ff callq 400550 <operator delete(void*, unsigned long)@plt>
可以看到正确的调用,最终释放时调用的是operator delete[](void*)
, 错误的调用不匹配的形式,最终调用的是operator delete(void*, unsigned long)
,第二个参数传递的只是一个 int
类型的大小,这显然不对。
所以 delete
和 free
的形式必须匹配。