C++ 中 new, delete 形式必须匹配的原理

不考虑面向对象的情况下,C++ 当中的 struct与 C 语言的 struct 的一个大不同就是C++当中存在构造函数和析构函数。对象创建时,要调用构造函数;对象离开作用域,会调用析构函数。

c++new 的几种形式

C++中的 new有 3 种形式,常见的用来在堆上构建对象的那个 new,还有 operator newplacement new. operator new 可以重载,其实 placement new 就是operator new 的一种重载形式。

内存分配调用

都知道new一个对象,其实是调用 operator new来分配内存的,然后再调用构造函数。operator new 内部调用 glibcmalloc 来分配内存,当然一般的还是有第三方的内存分配器库如tcmallojemalloc作为 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类型的大小,这显然不对。

所以 deletefree的形式必须匹配。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值