C++ new & delete

前言

new 和 delete 是 C++ 引入的用于动态内存管理的运算符,在 C 语言中我们则需要使用 malloc 和 free 这两个函数来申请空间和释放空间。使用 malloc 时,我们需要手动传入申请空间的大小,同时因为它返回的是 void*,又需要我们手动进行强制转换,C++ 中 new 的出现简化了这两个步骤,然而 new 和 malloc 的区别不止于此。

new 和 delete 是为了更好地支持面向对象,除了分配空间和释放空间,对象还需要进行构造与析构,下面就简单地介绍 new 和 delete 的实现(gcc)


Ⅰ new 和 delete

一 基本数据类型

首先针对基本数据类型,看看底层是如何表现的

int *p;

void alloc() {
    p = new int(42);
}

void dealloc() {
    delete p;
}

这里对应的汇编

alloc():
        push    rbp
        mov     rbp, rsp
        mov     edi, 4	// 这里是 sizeof(int)
        call    operator new(unsigned long)
        mov     DWORD PTR [rax], 42
        mov     QWORD PTR p[rip], rax
        nop
        pop     rbp
        ret
dealloc():
        push    rbp
        mov     rbp, rsp
        mov     rax, QWORD PTR p[rip]
        test    rax, rax
        je      .L4
        mov     esi, 4
        mov     rdi, rax
        call    operator delete(void*, unsigned long)
.L4:
        nop
        pop     rbp
        ret

(我不懂多少汇编)看上面的例子我们可以了解到

  • new 使用了一个 operator new 函数
  • delete 使用了一个 operator delete 函数
  • operator new 函数的参数,也就是申请空间的大小,底层已经计算好了

这里还有一个有趣的东西,看 dealloc 的这几行汇编

mov     rax, QWORD PTR p[rip]
test    rax, rax
je      .L4
  • 获得 p 的值(指向的地址),保存到 rax 中
  • 将 rax 的值与自己按位与,标志寄存器中的一些标志发生变化
  • 检查零标志位,如果为 1,则跳转到 .L4,跳过了 operator delete

那什么情况下跳转呢,当 rax 与 rax 按位与结果为 0 时,零标志位置为 1,发生跳转
其实就是当 p 为 nullptr 时,不会去释放内存,也就是 delete nullptr; 是安全的

二 复杂数据类型

基本数据类型不需要进行构造,我们再看一个类类型的例子

class A {
  public:
    size_t  length;
    int* data;
  public:
    // 默认构造函数
    A() : length(0), data(nullptr) {
        std::cout << "A()\n";
    }
    // 带参构造函数
    A(int len) : length(len) {
        std::cout << "A(int)\n";
        data = new int[length];
    }
    // 析构函数
    ~A() {
        std::cout << "destructor\n";
        delete[] data;
    }
};
A* p;

void alloc() {
    p = new A(42);
}

void dealloc() {
    delete p;
}

2.1 new 复杂类型

这里对应的汇编如下

alloc():
        push    rbp
        mov     rbp, rsp
        push    r13
        push    r12
        push    rbx
        sub     rsp, 8
        mov     edi, 16
        call    operator new(unsigned long)
        mov     rbx, rax
        mov     r13d, 1
        mov     esi, 42
        mov     rdi, rbx
        call    A::A(int) [complete object constructor]
        mov     QWORD PTR p[rip], rbx
        jmp     .L12
        mov     r12, rax
        test    r13b, r13b
        je      .L10
        mov     esi, 16
        mov     rdi, rbx
        call    operator delete(void*, unsigned long)
.L10:
        mov     rax, r12
        mov     rdi, rax
        call    _Unwind_Resume
.L12:
        add     rsp, 8
        pop     rbx
        pop     r12
        pop     r13
        pop     rbp
        ret

通过这段汇编我们可以了解到,new 作用于复杂类型时:

  • 首先调用 operator new 申请空间,大小 sizeof(A) 已经计算好了,是 16
  • 然后根据参数 42 调用了 A:A(int) 构造函数
  • 构造完毕后,将地址赋给了 p

至于中间的一段我不是特别明白,在将构造好的对象的地址赋给了 p 后,下一条是一个无条件跳转指令 jmp ,跳到了.L12 进行函数返回操作

2.2 delete 复杂类型

dealloc() 这里就相对简单了

dealloc():
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 8
        mov     rbx, QWORD PTR p[rip]
        test    rbx, rbx
        je      .L15
        mov     rdi, rbx
        call    A::~A() [complete object destructor]
        mov     esi, 16
        mov     rdi, rbx
        call    operator delete(void*, unsigned long)
.L15:
        nop
        mov     rbx, QWORD PTR [rbp-8]
        leave
        ret

delete 作用在复杂类型上时:

  • 首先调用析构
  • 然后释放内存

三 探索源码

通过上面两个例子,这里简单总结一下
new

  • 通过 operator new 函数申请空间
  • 调用构造函数

delete

  • 调用析构函数
  • 通过 operator delete 函数释放空间
  • delete nullptr; 无伤大雅

下面我们就来看看这两个函数的源码吧:

3.1 源码在哪?

通过使用 gdb 断点和 bt 命令,我们可以看见函数调用栈,这里无意中看见了源码文件

在这里插入图片描述

然后我们 step into 跳进这行代码

在这里插入图片描述

这个 operator new 源码位于 gcc-13.1.0/libstdc++-v3/libsupc++/new_op.cc:47

这里是 gcc 源码 github 传送门

终于在这里找到了源码:

在这里插入图片描述

3.2 operator new

_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
  void *p;

  /* malloc (0) is unpredictable; avoid it.  */
  if (sz == 0)
    sz = 1;

  while (__builtin_expect ((p = malloc (sz)) == 0, false))
    {
      new_handler handler = std::get_new_handler ();
      if (! handler)
		_GLIBCXX_THROW_OR_ABORT(bad_alloc());
      handler ();
    }

  return p;
}

看见没,最后不还是调用了 malloc,所以它俩的区别就是 new 调用了 malloc

代码主要逻辑是使用 malloc 函数进行内存分配,然后使用 __builtin_expect 宏来指示分配失败的概率较低。如果 malloc 返回的指针为0(即分配失败),则进入循环体,否则返回这个指针

在循环体内部,首先通过 std::get_new_handler() 获取当前的自定义错误处理函数(new handler)。如果没有设置自定义错误处理函数(即返回值为 nullptr),则抛出 bad_alloc 异常,表示内存分配失败,这将终止程序执行

如果处理函数存在,则调用后,继续尝试 malloc,这样一直循环直到申请成功

3.3 new_handler

  /** If you write your own error handler to be called by @c new, it must
   *  be of this type.  */
  typedef void (*new_handler)();

  /// Takes a replacement handler as the argument, returns the
  /// previous handler.
  new_handler set_new_handler(new_handler) throw();

#if __cplusplus >= 201103L
  /// Return the current new handler.
  new_handler get_new_handler() noexcept;
#endif

通过上面的 API 我们就可以自定义错误处理函数

我觉得可以试试在这里面进行一些释放空间的动作,直到有足够的空间

这里注意, 并没有提供默认的 new_handler

在这里插入图片描述

3.4 operator delete

delete 就更简单了

这个位于 gcc/libstdc+±v3/libsupc++/del_ops.cc:33

_GLIBCXX_WEAK_DEFINITION void
operator delete(void* ptr, std::size_t) _GLIBCXX_USE_NOEXCEPT
{
  ::operator delete (ptr);
}

调用了另一个重载

这个位于 gcc/libstdc+±v3/libsupc++/del_op.cc:49

_GLIBCXX_WEAK_DEFINITION void
operator delete(void* ptr) _GLIBCXX_USE_NOEXCEPT
{
  std::free(ptr);
}

其实就是简单调用了 free 函数进行内存释放


Ⅱ new[ ] 和 delete[ ]

再回顾一下:
new

  • 通过 operator new 函数申请空间,内部调用 malloc
  • 调用构造函数

delete

  • 调用析构函数
  • 通过 operator delete 函数释放空间,内部调用 free

上面通过源码,我们对 new 和 delete 已经有了大概的认识了,那 new[ ] 和 delete[ ] 是如何实现的呢?

一 基本数据类型

void alloc() {
    p = new int[10];
}

void dealloc() {
    delete[] p;
}

对应的汇编如下

alloc():
        push    rbp
        mov     rbp, rsp
        mov     edi, 40
        call    operator new[](unsigned long)
        mov     QWORD PTR p[rip], rax
        nop
        pop     rbp
        ret
dealloc():
        push    rbp
        mov     rbp, rsp
        mov     rax, QWORD PTR p[rip]
        test    rax, rax
        je      .L4
        mov     rax, QWORD PTR p[rip]
        mov     rdi, rax
        call    operator delete[](void*)
.L4:
        nop
        pop     rbp
        ret

记住,这里申请的空间大小是 10 * sizeof(int)

operator new[](unsigned long) 位于 gcc/libstdc++-v3/libsupc++/new_opv.cc:32

_GLIBCXX_WEAK_DEFINITION void*
operator new[] (std::size_t sz) _GLIBCXX_THROW (std::bad_alloc)
{
  return ::operator new(sz);
}

然后就是调用之前的 operator new ,位于 gcc/libstdc++-v3/libsupc++/new_op.cc:43

operator delete 位于 gcc/libstdc++-v3/libsupc++/del_opv.cc:35

_GLIBCXX_WEAK_DEFINITION void
operator delete[] (void *ptr) _GLIBCXX_USE_NOEXCEPT
{
  ::operator delete (ptr);
}

同样,里面也是调用了之前的 operator delete ,位于 gcc/libstdc+±v3/libsupc++/del_op.cc:49

所以对于基本类型来说,new[ ] 和 new 的实现几乎一样,比较简单

二 复杂数据类型

对于多个复杂数据类型就比较复杂了

void alloc() {
    p = new A[10];
}

void dealloc() {
    delete[] p;
}

2.1 new[ ] 复杂类型

alloc 对应的汇编

alloc():
        push    rbp
        mov     rbp, rsp
        push    r15
        push    r14
        push    r13
        push    r12
        push    rbx
        sub     rsp, 8
        mov     edi, 168	// !!!
        call    operator new[](unsigned long)
        mov     r12, rax
        mov     QWORD PTR [r12], 10
        mov     r15d, 1
        lea     rbx, [r12+8]
        mov     r13d, 9
        mov     r14, rbx
        jmp     .L6
.L7:
        mov     rdi, r14
        call    A::A() [complete object constructor]
        sub     r13, 1
        add     r14, 16
.L6:
        test    r13, r13
        jns     .L7
        lea     rax, [r12+8]
        mov     QWORD PTR p[rip], rax
        jmp     .L13
        mov     r14, rax
        test    rbx, rbx
        je      .L9
        mov     eax, 9
        sub     rax, r13
        sal     rax, 4
        lea     r13, [rbx+rax]
.L10:
        cmp     r13, rbx
        je      .L9
        sub     r13, 16
        mov     rdi, r13
        call    A::~A() [complete object destructor]
        jmp     .L10
.L9:
        mov     rbx, r14
        test    r15b, r15b
        je      .L11
        mov     esi, 168
        mov     rdi, r12
        call    operator delete[](void*, unsigned long)
.L11:
        mov     rax, rbx
        mov     rdi, rax
        call    _Unwind_Resume
.L13:
        add     rsp, 8
        pop     rbx
        pop     r12
        pop     r13
        pop     r14
        pop     r15
        pop     rbp
        ret

这里比较复杂,先给大家一个大概的印象

  • 申请空间,这里使用的 operator new[] 和上小节一样
  • 使用循环来逐个构造

然后这里有一个细节必须说明:A 的大小是 sizeof(A) = 16,我们 new A[10] 也就是需要 16 * 10 = 160 字节,但是,从上面的汇编我们可以看出,它实际上想申请 168 个字节,多出了 8 个字节。其实这 8 个字节是用来存储这个数组的对象个数的,这里是 10

看汇编,逐行分析

mov     edi, 168						// 需要申请 168 字节空间
call    operator new[](unsigned long)	// 这个和上一小节基本数据类型一样
mov     r12, rax						// rax : operator new[] 返回值
										// r12 记录着已申请空间的首地址
mov     QWORD PTR [r12], 10				// 把数组大小 10 赋给开头 8 个字节
mov     r15d, 1							// 忽略这个
lea     rbx, [r12+8]					// 跳过开头 8 个字节
mov     r13d, 9							// r13d 是 r13(64位) 的低32位
										// 这里 9 是循环的参数,构造这 10 个对象
mov     r14, rbx						// rbx 跳过了开头 8 个字节,从这地址开始
										// 依次构造 10 个对象
jmp     .L6								// 跳转到 .L6

.L7:
    mov     rdi, r14					// 在 r14 这个地址上构造一个对象
    call    A::A() [complete object constructor]
    sub     r13, 1						// 循环次数减一,直到 -1
    add     r14, 16						// A 对象的大小
.L6:
    test    r13, r13					// r13 循环参数
    jns     .L7							// 非负跳转
    
    lea     rax, [r12+8]				// r12 记录着已申请空间的首地址
    mov     QWORD PTR p[rip], rax		// p 指向的地址跳过了开头 8 个字节
    jmp     .L13

最后的结果就是这样
在这里插入图片描述

为什么需要记录这个数组大小呢,原来是要给配对的 delete[ ] 使用,new[ ]这里逐个构造,delete[ ] 那里就需要逐个析构

2.2 delete[ ] 复杂类型

dealloc 对应的汇编

dealloc():
        push    rbp
        mov     rbp, rsp
        push    rbx
        sub     rsp, 8
        mov     rax, QWORD PTR p[rip]
        test    rax, rax
        je      .L18
        mov     rax, QWORD PTR p[rip]
        mov     rdx, QWORD PTR p[rip]
        sub     rdx, 8
        mov     rdx, QWORD PTR [rdx]
        sal     rdx, 4
        lea     rbx, [rax+rdx]
.L17:
        mov     rax, QWORD PTR p[rip]
        cmp     rbx, rax
        je      .L16
        sub     rbx, 16
        mov     rdi, rbx
        call    A::~A() [complete object destructor]
        jmp     .L17
.L16:
        mov     rax, QWORD PTR p[rip]
        sub     rax, 8
        mov     rax, QWORD PTR [rax]
        sal     rax, 4
        lea     rdx, [rax+8]
        mov     rax, QWORD PTR p[rip]
        sub     rax, 8
        mov     rsi, rdx
        mov     rdi, rax
        call    operator delete[](void*, unsigned long)
.L18:
        nop
        mov     rbx, QWORD PTR [rbp-8]
        leave
        ret

析构这里是从最后一个对象开始,逐个往前析构

大概流程:

  • 首先通过 首地址 + 数组个数 偏移得到最后一个对象的首地址
  • 依次调用析构
  • 循环退出的条件是当前指针指向的地址是这块内存的首地址,首地址存放着数组大小,当然不用析构
  • 最后释放空间

这里还是看一看怎么释放空间的

.L16:
mov     rax, QWORD PTR p[rip]
sub     rax, 8
mov     rax, QWORD PTR [rax] 	// rax = 10
sal     rax, 4					// rax = rax << 4 = 10 * 2^4 = 160
lea     rdx, [rax+8]			// rdx = 160 + 8
mov     rax, QWORD PTR p[rip]
sub     rax, 8
mov     rsi, rdx	// 第二个参数 空间大小
mov     rdi, rax	// 第一个参数 需要释放空间的首地址
call    operator delete[](void*, unsigned long)

这里的 operator delete[] 有两个参数,和之前基本类型那里不一样哦

_GLIBCXX_WEAK_DEFINITION void
operator delete[] (void *ptr, std::size_t) _GLIBCXX_USE_NOEXCEPT
{
  ::operator delete[] (ptr);
}

里面调用了之前基本类型的那个单个参数重载

也不知道这里的大小到底用在哪了。。。

总结

在编写代码层面上,new 和 delete 隐藏了很多细节,让我们的代码更加简洁

在底层实现上,最后还是调用的 malloc 和 free 这两个 C 接口

简单总结一下

1.对于基本数据类型

int *p = new int(42);

  • 自动计算申请空间大小 4
  • operator new(size_t) 申请空间,里面调用 malloc
  • 在空间上直接赋值
  • 空间首地址赋值给 p

delete p;

  • 判断 p 是否为 nullptr
  • p 不是 nullptr,operator delete(void*, size_t) 释放空间,里面调用 operator delete(void*),里面调用 free

int *p = new int[10];
// p = new int[10] (2) ; // error
// p = new int[10] {2} ; // ok,第一个元素赋值 2,其余 0

  • 自动计算申请空间大小 4 * 10
  • operator new[](size_t) 申请空间,里面调用 operator new(size_t),里面调用 malloc
  • 赋值,只能使用初始化列表,挨个赋值,剩余 0 使用循环赋值
  • 空间首地址赋值给 p

delete[] p;

  • 判断 p 是否为 nullptr
  • p 不是 nullptr,operator delete[](void*) 释放空间,里面调用 operator delete(void*),里面调用 free

2.对于复杂数据类型

A *p = new A();

  • 自动计算大小 sizeof(A) = 16
  • operator new(size_t) 申请空间,里面调用 malloc
  • 在空间上调用构造函数
  • 空间首地址赋值给 p

delete p;

  • 判断 p 是否为 nullptr
  • p 不是 nullptr,调用析构函数
  • operator delete(void*, size_t) 释放空间,里面调用 operator delete(void*),里面调用 free

A *p = new A[10];

  • 自动计算大小 8 + 10 * sizeof(A) = 168
  • operator new[](size_t) 申请空间,里面调用 operator new(size_t),里面调用 malloc
  • 空间首地址处存储数组大小
  • 从前往后,循环构造
  • 第一个对象首地址赋值给 p

delete[] p;

  • 判断 p 是否为 nullptr
  • p 不是 nullptr,从后往前,循环调用析构
  • operator delete[](void*, size_t) 释放空间,里面调用 operator delete,里面调用 operator delete(void*),里面调用 free

其他补充

  • new 失败抛出 std::bad_alloc 异常

p = new(std::nothrow) int(1); // 失败返回 nullptr

_GLIBCXX_WEAK_DEFINITION void *
operator new (std::size_t sz, const std::nothrow_t&) _GLIBCXX_USE_NOEXCEPT
{
  void *p;

  /* malloc (0) is unpredictable; avoid it.  */
  if (sz == 0)
    sz = 1;

  while (__builtin_expect ((p = malloc (sz)) == 0, false))
    {
      new_handler handler = std::get_new_handler ();
      if (! handler)
	return 0;
      __try
	{
	  handler ();
	}
      __catch(const bad_alloc&)
	{
	  return 0;
	}
    }

  return p;
}
  • placement new

  • std::allocator

本文使用 Compiler Explorer 查看汇编代码,使用的是 x86-64 gcc 13.2,-O0

  • 35
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
newdeleteC++中用于动态内存分配和释放的关键字,它们可以让我们在程序运行时动态地分配和释放内存。 new操作符: new操作符用于在堆上分配内存空间。它的语法如下: ``` new 数据类型; ``` 或者 ``` new 数据类型(初始化参数); ``` 当我们使用new操作符时,它会在堆上分配一块内存空间,并返回该内存空间的首地址。同时,如果我们使用第二种语法,可以在分配内存空间的同时,调用对象的构造函数对对象进行初始化。 delete操作符: delete操作符用于释放new操作符分配的内存空间。它的语法如下: ``` delete 指针; ``` 当我们使用delete操作符时,它会释放指针所指向的内存空间,并调用对象的析构函数释放对象占用的资源。 以下是一个简单的示例代码,展示了如何使用newdelete操作符: ``` #include <iostream> using namespace std; class Person { public: Person(const char* name, int age) { this->name = name; this->age = age; cout << "Person created." << endl; } ~Person() { cout << "Person destroyed." << endl; } void display() { cout << "Name: " << name << ", Age: " << age << endl; } private: string name; int age; }; int main() { // 使用new操作符动态分配内存空间 Person* p = new Person("Tom", 20); // 使用指针访问对象 p->display(); // 使用delete操作符释放内存空间 delete p; p = nullptr; return 0; } ``` 在这个示例中,我们使用new操作符动态分配了一个Person对象的内存空间,并返回对象的地址给指针p。然后,我们使用指针访问对象并执行display()方法。最后,我们使用delete操作符释放对象占用的内存空间,并将指针p置为nullptr,避免野指针的问题。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值