目录
前言
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