libc++源码分析之new和delete

示例:new/delete 一个对象

代码

1
2
3
4
5
int main(int argc, char** argv) {
std::string* str = new std::string();
delete str;
return 0;
}

汇编

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
    0x108852450 <+0>:   pushq  %rbp
0x108852451 <+1>: movq %rsp, %rbp
0x108852454 <+4>: subq $0x30, %rsp
0x108852458 <+8>: movl $0x0, -0x4(%rbp)
0x10885245f <+15>: movl %edi, -0x8(%rbp)
0x108852462 <+18>: movq %rsi, -0x10(%rbp)
0x108852466 <+22>: movl $0x18, %edi
0x10885246b <+27>: callq 0x1088c26c6 ; symbol stub for: operator new(unsigned long)
0x108852470 <+32>: movq %rax, %rdi
0x108852473 <+35>: movq %rax, -0x20(%rbp)
0x108852477 <+39>: callq 0x1088524c0 ; std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::basic_string at string:1725
0x10885247c <+44>: movq -0x20(%rbp), %rax
0x108852480 <+48>: movq %rax, -0x18(%rbp)
0x108852484 <+52>: movq -0x18(%rbp), %rcx
0x108852488 <+56>: cmpq $0x0, %rcx
0x10885248c <+60>: movq %rcx, -0x28(%rbp)
0x108852490 <+64>: je 0x1088524ab ; <+91> at main.cpp
0x108852496 <+70>: movq -0x28(%rbp), %rdi
0x10885249a <+74>: callq 0x1088c240e ; symbol stub for: std::__1::basic_string<char, std::__1::char_traits<char>, std::__1::allocator<char> >::~basic_string()
0x10885249f <+79>: movq -0x28(%rbp), %rax
0x1088524a3 <+83>: movq %rax, %rdi
0x1088524a6 <+86>: callq 0x1088c26b4 ; symbol stub for: operator delete(void*)
0x1088524ab <+91>: xorl %eax, %eax
-> 0x1088524ad <+93>: addq $0x30, %rsp
0x1088524b1 <+97>: popq %rbp
0x1088524b2 <+98>: retq
0x1088524b3 <+99>: nopw %cs:(%rax,%rax)
0x1088524bd <+109>: nopl (%rax)

结论

new关键字会触发三个步骤:

  1. 调用函数operator new分配内存
  2. 调用对象的构造函数
  3. 返回对象的纯右值指针

delete关键字会触发两个步骤

  1. 调用对象的析构函数
  2. 调用函数operator delete释放内存

operator new

operator new函数负责实现内存的分配,分为全局实现和对象实现,可以通过重写该函数来自定义内存的分配。

STL实现

1
2
3
4
5
6
void* operator new(std::size_t size);                                   // replaceable, nodiscard in C++20
void* operator new(std::size_t size, std::align_val_t alignment); // replaceable, C++17, nodiscard in C++20
void* operator new(std::size_t size, const std::nothrow_t&) noexcept; // replaceable, nodiscard in C++20
void* operator new(std::size_t size, std::align_val_t alignment,
const std::nothrow_t&) noexcept; // replaceable, C++17, nodiscard in C++20
void* operator new (std::size_t size, void* ptr) noexcept; // nodiscard in C++20

operator new(std::size_t size)
最常用的实现,其中的size就是要分配的内存大小,如果size是0也就是空类,那么会默认分配1个字节的内存。默认分配内存是通过malloc来实现的,如果分配失败则会调用get_new_handler函数,然后再次尝试分配内存,所以可以通过注册get_new_handler函数当内存不足时进行内存释放。在默认的实现中,如果开启了exception,则分配失败会抛出std::bad_alloc异常,否则会返回nullptr。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void *operator new(std::size_t size) _THROW_BAD_ALLOC
{
if (size == 0)
size = 1;
void* p;
while ((p = ::malloc(size)) == nullptr)
{
// If malloc fails and there is a new_handler,
// call it to try free up memory.
std::new_handler nh = std::get_new_handler();
if (nh)
nh();
else
#ifndef _LIBCPP_NO_EXCEPTIONS
throw std::bad_alloc();
#else
break;
#endif
}
return p;
}

operator new(std::size_t size, const std::nothrow_t&) noexcept
该重载是针对不想使用异常的情况,在该实现内部会自己捕捉异常,失败会直接返回nullptr。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
void* operator new(size_t size, const std::nothrow_t&) noexcept
{
void* p = nullptr;
#ifndef _LIBCPP_NO_EXCEPTIONS
try
{
#endif // _LIBCPP_NO_EXCEPTIONS
p = ::operator new(size);
#ifndef _LIBCPP_NO_EXCEPTIONS
}
catch (...)
{
}
#endif // _LIBCPP_NO_EXCEPTIONS
return p;
}

operator new(std::size_t size, void ptr) noexcept*
该重载就是placement new,多了一个指针参数,在该实现中不会分配内存,而是使用现有已分配内存,可以看出实现就是简单的返回参数指针。使用placement new可以方便的控制内存的分配,在STL内部大量的使用了该方法。

1
inline void* operator new  (std::size_t, void* __p) _NOEXCEPT {return __p;}

在下面的示例中通过malloc自己分配了内存,然后通过placement new调用了A的构造函数。

1
2
A* a = static_cast<A*>(malloc(sizeof(A)));
a = new (a) A();

operator new(std::size_t size, std::align_val_t alignment)
该重载是C++17添加的新函数,align_val_t参数可以强制内存对齐。*STDCPP_DEFAULT_NEW_ALIGNMENT*是默认的内存对齐值,也就是new的地址是该值的倍数。 通过align_val_t参数可以指定new的内存对齐。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
void *operator new(std::size_t size, std::align_val_t alignment) _THROW_BAD_ALLOC
{
if (size == 0)
size = 1;
if (static_cast<size_t>(alignment) < sizeof(void*))
alignment = std::align_val_t(sizeof(void*));

// Try allocating memory. If allocation fails and there is a new_handler,
// call it to try free up memory, and try again until it succeeds, or until
// the new_handler decides to terminate.
//
// If allocation fails and there is no new_handler, we throw bad_alloc
// (or return nullptr if exceptions are disabled).
void* p;
while ((p = std::__libcpp_aligned_alloc(static_cast<std::size_t>(alignment), size)) == nullptr)
{
std::new_handler nh = std::get_new_handler();
if (nh)
nh();
else {
#ifndef _LIBCPP_NO_EXCEPTIONS
throw std::bad_alloc();
#else
break;
#endif
}
}
return p;
}

下面的示例中将aligned_int对象的内存对齐指定为128,则分配的地址一定是128的倍数。

1
2
int* aligned_int = new (std::align_val_t(128)) int;
bool b = intptr_t(aligned_int) % 128 == 0;

operator new(std::size_t size, std::align_val_t alignment, const std::nothrow_t&) noexcept
该重载是上面的无异常版本,内部会捕捉异常。

内存对齐

C++11后,可以通过alignas关键字来改变对齐值,通过alignof来查询对齐值。

默认情况下,struct按照内部最大类型来进行对齐,在Align结构体中,最大类型是int,所以其对齐值为4。char占1个字节,后面补上3个0字节,int占4个字节,所以sizeeof为8。

1
2
3
4
5
6
7
struct Align {
char a;
int b;
};
size_t align2 = alignof(align); // 4
size_t size2 = sizeof(align); // 8
// a000 bbbb

通过alignas设置结构体的对齐值位16,char占1个字节,后面补上3个0字节,int占4个字节,后面补上8个0字节,所以sizeof为16。

1
2
3
4
5
6
7
struct alignas(16)  Align {
char a;
int b;
};
size_t align2 = alignof(align); // 16
size_t size2 = sizeof(align); // 16
// a000 bbbb 0000 0000

通过alignas设置结构体内部int的对齐值为8,则结构体最大对齐值为8,char占1个字节,后面补上7个0字节,int占4个字节,后面补上4个0字节,所以sizeof为16。

1
2
3
4
5
6
7
struct Align {
char a;
alignas(8) int b;
};
size_t align2 = alignof(align); // 8
size_t size2 = sizeof(align); // 16
// c000 0000 bbbb 0000

通过alignas设置结构体内部char的对齐值为8,则结构体最大对齐值为8,char占1一个字节,后面有7个字节的空位,int占4个字节,完全够用,则int直接在这次对齐内分配,不需要重新再起一行,所以sizeof为8。

1
2
3
4
5
6
7
struct Align {
alignas(8) char a;
int b;
};
size_t align2 = alignof(align); // 8
size_t size2 = sizeof(align); // 8
// c000 bbbb

自定义实现

operator new函数时可以自定义的,分为全局自定义和对象自定义,全局自定义的operator new会导致所有的对象的new都应用该自定义函数。而对象自定义只会影响到该对象创建。当两种实现都自定义时,对象自定义优先。

全局自定义

1
2
3
4
5
void* operator new(size_t size)
{
void* pVoid = malloc(size);
return pVoid;
}

对象自定义

1
2
3
4
5
6
7
8
9
10
class A {
public:
A() { std::cout << "constructor" << std::endl; }

~A() { std::cout << "destructor" << std::endl; }

void* operator new(size_t size) {
return ::operator new(size);
}
};

operator new[]

operator new[]函数负责实现数组内存的分配,同样也分为全局实现和对象实现,可以通过重写该函数来自定义内存的分配。

长度信息存储

当创建的对象定义了析构函数时,为了在delete[] 时能够循环的调用每一个对象的析构函数,那么在new[]分配内存时会多分配一段内存,在这段内存中存储当前数据的长度信息,所以当new[]一个带析构函数的对象数组时,operator new[]的size参数会比整个数组长度要大,并且我们得到的数组指针并不是返回的void*地址,而是一个去掉前面一段的偏移地址。

在下面的实例中,当Align定义了析构函数时,size的值是11,当Align没有定义析构时,size的值是3。所以在这里用来存储数组长度的区域为8个字节,malloc分配的地址是0x7f8e40504b40,返回给a的地址是偏移8个字节后的0x7f8e40504b48。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void* operator new[](size_t size) {
std::cout << "call global new[] size: " << size << std::endl;
void* pVoid = malloc(size); // 0x7f8e40504b40
return pVoid;
}

class Align {
public:
~Align() { std::cout << "destructor" << std::endl; }
};

int main(int argc, char** argv) {
Align* a = new Align[3]; // 0x7f8e40504b48
}

STL实现

1
2
3
4
5
6
7
void* operator new[](std::size_t size);                                 // replaceable, nodiscard in C++20
void* operator new[](std::size_t size,
std::align_val_t alignment) noexcept; // replaceable, C++17, nodiscard in C++20
void* operator new[](std::size_t size, const std::nothrow_t&) noexcept; // replaceable, nodiscard in C++20
void* operator new[](std::size_t size, std::align_val_t alignment,
const std::nothrow_t&) noexcept; // replaceable, C++17, nodiscard in C++20
void* operator new[](std::size_t size, void* ptr) noexcept; // nodiscard in C++20

operator new[]的实现默认都是直接调用operator new来分配,这里就不展开了。

1
2
3
4
void* operator new[](size_t size) _THROW_BAD_ALLOC
{
return ::operator new(size);
}

自定义实现

全局自定义

1
2
3
4
void* operator new[](size_t size) {
void* pVoid = malloc(size);
return pVoid;
}

对象自定义

1
2
3
4
5
6
7
8
9
10
class A {
public:
A() { std::cout << "constructor" << std::endl; }

~A() { std::cout << "destructor" << std::endl; }

void* operator new[](size_t size) {
return ::operator new[](size);
}
};

operator delete

operator delete函数负责实现内存的释放,分为全局实现和对象实现,可以通过重写该函数来自定义内存的分配。operator delete需要和operator new配套实现,调用什么参数的operator new,内部在失败释放的时候就会调用什么参数的operator delete。

STL实现

1
2
3
4
5
6
7
8
9
void  operator delete(void* ptr) noexcept;                              // replaceable
void operator delete(void* ptr, std::size_t size) noexcept; // replaceable, C++14
void operator delete(void* ptr, std::align_val_t alignment) noexcept; // replaceable, C++17
void operator delete(void* ptr, std::size_t size,
std::align_val_t alignment) noexcept; // replaceable, C++17
void operator delete(void* ptr, const std::nothrow_t&) noexcept; // replaceable
void operator delete(void* ptr, std:align_val_t alignment,
const std::nothrow_t&) noexcept; // replaceable, C++17
void operator delete (void* ptr, void*) noexcept;

operator delete(void ptr) noexcept*
operator delete函数默认的实现是调用free来进行内存的释放,之所以free能够正确的释放,是因为在malloc得到的地址其实是一个handler,在内部都有一个私有簿记(book keeping)区域来记录每个handler对应的内存区域是多大。所以malloc和free必须要配套使用,这样才能正确的释放内存。

1
2
3
4
void operator delete(void* ptr) noexcept
{
::free(ptr);
}

operator delete(void ptr, const std::nothrow_t&) noexcept*

1
2
3
4
void operator delete(void* ptr, const std::nothrow_t&) noexcept
{
::operator delete(ptr);
}

operator delete(void ptr, void) noexcept**

1
inline void  operator delete  (void*, void*) _NOEXCEPT {}

operator delete[]

operator delete[]函数负责实现数组内存的释放,同样也分为全局实现和对象实现,可以通过重写该函数来自定义内存的分配。delete[]和new[]一样,对于拥有析构函数对象的数组,其前面会有一段内存用来存储数组长度,但是传递到operator delete[]函数上时,是去掉偏移值的地址。

STL实现

1
2
3
4
5
6
7
8
9
10
void  operator delete[](void* ptr) noexcept;                            // replaceable
void operator delete[](void* ptr, std::size_t size) noexcept; // replaceable, C++14
void operator delete[](void* ptr,
std::align_val_t alignment) noexcept; // replaceable, C++17
void operator delete[](void* ptr, std::size_t size,
std::align_val_t alignment) noexcept; // replaceable, C++17
void operator delete[](void* ptr, const std::nothrow_t&) noexcept; // replaceable
void operator delete[](void* ptr, std::align_val_t alignment,
const std::nothrow_t&) noexcept; // replaceable, C++17
void operator delete[](void* ptr, void*) noexcept;