大纲 C++ 面向对象深入 new 和 delete 运算符 new 和 delete 的底层干了什么 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> class MyTest {public : MyTest () { } ~MyTest () { } }; int main () { MyTest* mt = new MyTest (); delete mt; return 0 ; }
1 2 3 4 MyTest* mt = new MyTest(); |_____ operator new() |_____ malloc() // C 语言中的 malloc() 函数,负责分配内存空间 |_____ MyTest::MyTest // 类的构造函数,负责初始化对象
1 2 3 4 delete mt; |_____ MyTest::~MyTest // 类的析构函数,负责销毁对象 |_____ operator delete() |_____ free() // C 语言中的 free() 函数,负责释放内存空间
new 加括号与不加括号的区别 思考问题
在 C++ 中,使用 new 创建对象时,加括号与不加括号的区别是什么?
1 2 3 4 5 6 7 8 9 10 11 12 13 #include <iostream> class MyTest {}; int main () { MyTest* mt = new MyTest (); MyTest* mt2 = new MyTest; return 0 ; }
2、如果类中有成员变量,带括号的 new 会将一些和成员变量相关的内存置零,但不是整个对象的内存全部置零 1 2 3 4 5 6 7 8 9 10 11 12 13 14 #include <iostream> class MyTest {private : int m_i; }; int main () { MyTest* mt = new MyTest (); MyTest* mt2 = new MyTest; return 0 ; }
3、如果类中有成员变量,且有自定义的空构造函数,两种写法没有任何区别 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 #include <iostream> class MyTest {public : MyTest () { } private : int m_i; }; int main () { MyTest* mt = new MyTest (); MyTest* mt2 = new MyTest; return 0 ; }
new 底层实现细节深入探秘 new 额外分配的内存空间 new 的内存分配并不是简单地 “分配几个字节”。实际进行内存分配时,内存分配器通常需要维护额外的管理信息,例如内存块的大小、状态以及空闲块之间的关系等,因此实际占用的内存范围往往会比用户请求的大小更大。以申请一小块内存为例,并不意味着底层只需要处理对应的几个字节,而是在这块内存附近可能还需要维护额外的元数据。此外,new 的工作也不只是分配内存。它还涉及对象的构造;对应的 delete 则需要先调用对象的析构函数,再释放底层内存。因此,C++ 的动态内存管理相比简单的内存分配函数,包含了更多与对象生命周期相关的处理。需要注意的是,具体的内存布局和额外开销取决于编译器、标准库以及底层内存分配器的实现,不能简单认为 new 一定会额外固定增加多少字节。
特别注意
在分配内存时,为了记录和管理分配出去的内存,编译器会额外多分配不少内存,从而造成了浪费;尤其是频繁分配小块内存时,造成的浪费更严重。
如何记录 new 分配的内存大小供 delete 使用 C++ 编译器或运行时库在 new 分配内存时,通常会在用户请求的内存前额外多分配一块空间,用于存储内存块的大小信息,然后返回偏移后的指针;delete 根据指针向前访问这个内存块大小信息,从而知道实际分配的内存大小并释放。new 的底层通常调用 malloc() 来分配内存,而 malloc() 自身在内存块前维护了内存块的大小信息,以便 free() 正确释放内存;这些内存块大小信息由底层内存管理器维护,而不是由 new 本身维护。
理解注意事项:(1) 当调用 malloc(totalSize) 时,底层内存管理器(C 标准库)会记录这块内存的实际大小和管理信息(通常在内存块头部隐藏存储)。 (2) 当调用 free(ptr) 时,它会读取自己内部的管理信息来知道这块内存的大小,然后回收,不需要用户手动告诉它大小。 (3) 因此在 operator delete 中,用户只需要传给 free() 原始分配的指针,底层内存管理器会自动处理。 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 #include <iostream> using namespace std;class MyClass {public : MyClass () { cout << "MyClass() called" << endl; } ~MyClass () { cout << "~MyClass() called" << endl; } }; void test01 () { cout << "---------- test01() ----------" << endl; MyClass mc; size_t len = sizeof (mc); cout << "size: " << len << endl; } void test02 () { int * iarray = new int [2 ]; delete [] iarray; } void test03 () { cout << "---------- test03() ----------" << endl; MyClass* parray = new MyClass[2 ]; delete [] parray; }
为什么在上述的 test03() 函数中,如果不手动通过 delete[] 释放对象数组时,会泄露 18 个字节呢?
(1) 对象大小
MyClass 类只有构造函数和析构函数,没有成员变量,对象本身大小为 1 字节(空类保证每个对象都有唯一地址)。在大多数 64 位编译器下,空类大小通常是 1 字节。 如果类里有成员变量,例如 int m_i,类的大小通常是 sizeof(int),按 4 或者 8 字节对齐。 (2) 数组长度信息
编译器在用 new[] 分配数组时,通常会额外在内存块前额外多分配一块空间来存储数组长度信息,用于告诉 delete[] 需要调用多少次析构函数。 这个数组长度通常是 size_t(在 64 位系统上占 8 字节)。 (3) 内存块大小信息
new 底层调用 malloc() 分配内存,malloc 自身会在内存块前额外多分配一块空间,用于存储内存块的大小信息,以便 free() 回收内存。这内存块大小信息通常占 8 ~ 16 字节(依赖系统和实现)。 (4) 内存布局示意(简化版)
1 2 3 4 5 6 7 8 9 +------------------------+ | 内存块大小 = 8 ~ 16 字节 | <-- malloc 内部隐藏头(管理内存块大小) +------------------------+ | 数组长度 size_t = 8 字节 | <-- 编译器存储数组元素数量 +------------------------+ | 对象 MyClass[0] = 1 字节 | +------------------------+ | 对象 MyClass[1] = 1 字节 | +------------------------+
(5) 实际泄露的内存大小
对象数据本身大小:2 × sizeof(MyClass) 额外存储的数组长度信息:sizeof(size_t)(在 64 位系统上占 8 字节) 额外存储的内存块大小信息:约占 8 ~ 16 字节(依赖系统和实现) (6) 举例说明
假设是 64 位系统,空类大小占 1 字节,数组长度(size_t)占 8 字节,内存块大小信息占 8 字节:1 总的内存泄漏大小 ≈ 8 (内存块大小) + 8 (数组长度) + 2 × 1 (对象大小) = 18 字节
如果类有一个 int 成员变量(4 字节),并按 4 字节对齐:1 2 sizeof(MyClass) = 4字节 总的内存泄漏大小 ≈ 8 (内存块大小) + 8 (数组长度) + 2 × 4 (对象大小) = 24 字节
为什么在上述的 test02() 函数中,如果不手动通过 delete[] 释放对象数组时,会泄露 16 个字节呢?
(1) 对象大小
int 在 64 位系统上通常是占用 4 字节(32 位整数)。数组有 2 个元素,所以对象数据占用:1 2 × sizeof(int) = 2 × 4 = 8 字节
(2) 数组长度信息
编译器在用 new[] 分配数组时,通常会额外在内存块前额外多分配一块空间来存储数组长度信息,用于告诉 delete[] 需要调用多少次析构函数。 对于 int 这种基础类型没有析构函数,数组长度信息通常会被编译器优化掉不存储(也就是说不会占用额外字节)。 因此数组长度占用 0 字节。 (3) 内存块大小信息
new 底层调用 malloc() 分配内存,malloc 自身会在内存块前额外多分配一块空间,用于存储内存块的大小信息,以便 free() 回收内存。这内存块大小信息通常占 8 ~ 16 字节(依赖系统和实现)。 (4) 内存布局示意(简化版)
1 2 3 4 5 6 7 +------------------------+ | 内存块大小 = 8 ~ 16 字节 | <-- malloc 内部隐藏头(管理内存块大小) +------------------------+ | 对象 MyClass[0] = 4 字节 | +------------------------+ | 对象 MyClass[1] = 4 字节 | +------------------------+
(5) 实际泄露的内存大小
对象数据本身大小:2 × sizeof(int) 额外存储的数组长度信息:0 字节(基础类型) 额外存储的内存块大小信息:约占 8 ~ 16 字节(依赖系统和实现)1 总的内存泄漏大小 ≈ 8 (内存块大小) + 2 x 4 (对象大小) = 16 字节
关键点
数组长度信息:由编译器自己存储,用于告诉 delete[] 需要调用多少次析构函数。 内存块大小信息:由 malloc() / 底层内存管理器存储,用于告诉 free() 实际回收的内存大小。 特别注意
在 C++ 中,使用 new[] 分配数组内存后,必须使用 delete[] 释放内存;若误用 delete 释放数组内存,仅当数组元素类型为基础类型或者不含自定义析构函数的类类型时,行为才不是未定义的(通常不会引发错误) ,否则会导致内存泄漏或程序崩溃。正确写法应始终成对使用 new[] 与 delete[]。
重载 new、delete 运算符 类重载 new、delete 运算符 在类内部重载 new 和 delete 运算符时,operator new 本质上负责分配原始内存,而不是创建对象,因此返回类型使用 void*。对象的构造由后续的构造函数完成,最终 new 表达式才得到 MyClass*。对应地,operator delete 负责释放原始内存,而对象析构由析构函数完成。
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 #include <cstdlib> #include <iostream> #include <new> using namespace std;class MyClass {public : MyClass () { cout << "MyClass() called" << endl; } ~MyClass () { cout << "~MyClass() called" << endl; } static void * operator new (size_t size) { cout << "operator new called, size: " << size << endl; void * ptr = malloc (size); if (!ptr) { throw bad_alloc (); } return ptr; } static void operator delete (void * ptr) { cout << "operator delete called" << endl; if (ptr) { free (ptr); } } static void * operator new [](size_t size) { cout << "operator new[] called, size: " << size << endl; void * ptr = malloc (size); if (!ptr) { throw bad_alloc (); } return ptr; } static void operator delete [](void * ptr) { cout << "operator delete[] called" << endl; if (ptr) { free (ptr); } } public : int m_i; }; int main () { MyClass* mc = new MyClass (); delete mc; cout << "------------------" << endl; MyClass* marry = new MyClass[3 ]; delete [] marry; return 0 ; }
程序运行输出的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 operator new called, size: 4 MyClass() called ~MyClass() called operator delete called ------------------ operator new[] called, size: 20 MyClass() called MyClass() called MyClass() called ~MyClass() called ~MyClass() called ~MyClass() called operator delete[] called
特别注意
在 C++ 中,new 和 delete 具备对堆上分配的内存进行初始化和释放的能力,这是 malloc()、free() 不具备的。 对象构造时,先使用 new 分配对象内存,然后再调用对象的构造函数;对象析构时,先调用对象的析构函数,再调用 delete 释放对象内存。 全局重载 new、delete 运算符 在 C++ 中,全局重载 new 和 delete 运算符的情况比较少见,因为它们会影响整个程序的内存分配与释放行为。所有通过普通 new 和 delete 进行的动态内存操作,都可能受到全局重载的影响。因此,全局重载通常需要非常谨慎,不仅要考虑内存分配效率,还要正确处理内存对齐、异常安全、数组分配、线程安全等问题。一旦实现存在 Bug,可能影响程序中的大量代码,甚至导致难以定位的内存错误。实际开发中,如果只是希望针对某一种类型或某一类对象优化内存分配,通常更适合在类内部重载 new 和 delete 运算符,或者单独实现内存池,而不是直接修改全局的内存分配行为。
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 #include <cstdlib> #include <iostream> #include <new> void * operator new (size_t size) { return malloc (size); } void operator delete (void * ptr) { free (ptr); } void * operator new [](size_t size) { return malloc (size); } void operator delete [](void * ptr) { free (ptr); } class MyClass {public : MyClass () { std::cout << "MyClass::MyClass()" << std::endl; } ~MyClass () { std::cout << "MyClass::~MyClass()" << std::endl; } private : int m_i = 0 ; }; int main () { int * p_int = new int (12 ); delete p_int; char * p_chars = new char [3 ]; delete [] p_chars; MyClass* p_obj = new MyClass (); delete p_obj; MyClass* p_obj_arr = new MyClass[2 ]; delete [] p_obj_arr; return 0 ; }
程序运行输出的结果如下:
1 2 3 4 5 6 MyClass::MyClass() MyClass::~MyClass() MyClass::MyClass() MyClass::MyClass() MyClass::~MyClass() MyClass::~MyClass()
多种 new 运算符重载版本 在 C++ 中,可以重载多个版本的 operator new,只要每个版本的参数列表不同即可(即通过参数个数或参数类型来区分重载)。不过,无论重载哪个版本,其第一个参数都是固定的,必须是 std::size_t 类型,用来表示所要 new 的对象的字节大小(即 sizeof() 的结果)。这也是编译器在调用 operator new 时自动传入的第一个实参,后续参数则由用户在 new 表达式中显式提供(如定位 new 的 void* 或自定义的额外参数)。需要注意的是,除了定位 new 重载之外,其他重载只改变内存分配函数本身的行为,并不改变 new 表达式 “先分配内存、再调用构造函数” 的整体语义,因此仍应遵循相应的失败约定,并为对应的 operator delete 提供匹配版本以正确释放内存。
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #include <cstdlib> #include <iostream> #include <new> class MyClass {public : MyClass () : m_a (0 ) { std::cout << "MyClass::MyClass()" << std::endl; } MyClass (const int val) : m_a (val) { std::cout << "MyClass::MyClass(const int val)" << std::endl; } ~MyClass () { std::cout << "MyClass::~MyClass()" << std::endl; } static void *operator new (size_t size) { std::cout << "MyClass::operator new(size_t size)" << std::endl; void *ptr = malloc (size); if (ptr == nullptr ) { throw std::bad_alloc (); } return ptr; } static void *operator new (size_t size, int val) { std::cout << "MyClass::operator new(size_t size, int val)" << std::endl; void *ptr = malloc (size); if (ptr == nullptr ) { throw std::bad_alloc (); } return ptr; } static void operator delete (void *ptr) { std::cout << "MyClass::operator delete(void *ptr)" << std::endl; free (ptr); } private : int m_a; }; int main () { MyClass *mc1 = new MyClass (); delete mc1; MyClass *mc2 = new (123 ) MyClass (); delete mc2; return 0 ; }
程序运行输出的结果如下:
1 2 3 4 5 6 7 8 MyClass::operator new(size_t size) MyClass::MyClass() MyClass::~MyClass() MyClass::operator delete(void *ptr) MyClass::operator new(size_t size, int val) MyClass::MyClass() MyClass::~MyClass() MyClass::operator delete(void *ptr)
定位 new 定位 new(placement new) 是 C++ 提供的一种在指定内存地址上直接构造对象 的方式,语法格式:new (内存地址) 类类型(构造参数)。它不会重新分配内存,而是利用已经准备好的内存空间调用对象的构造函数,因此常用于内存池、对象池、共享内存、预分配内存等场景。需要注意的是,定位 new 只负责在指定内存地址上构造对象,不负责分配或释放内存,对象使用完毕后通常需要显式调用析构函数,再根据内存的实际来源决定是否释放内存 。在 C++ 中,只有定位 new 的概念,不存在定位 delete 的概念。
定位 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 30 31 32 33 34 35 36 37 38 39 #include <iostream> class MyClass {public : MyClass () : m_a (0 ) { std::cout << "MyClass::MyClass()" << std::endl; } MyClass (const int val) : m_a (val) { std::cout << "MyClass::MyClass(const int val)" << std::endl; } ~MyClass () { std::cout << "MyClass::~MyClass()" << std::endl; } private : int m_a; }; int main () { void *ptr = (void *)new char [sizeof (MyClass)]; MyClass *mc = new (ptr) MyClass (); mc->~MyClass (); delete [] (char *)ptr; return 0 ; }
程序运行输出的结果如下:
1 2 MyClass::MyClass() MyClass::~MyClass()
特别注意
在上面的案例代码中,为什么不能直接 delete mc?因为 mc 不是通过普通 new MyClass() 得到的。简而言之,谁负责构造,谁负责销毁;定位 new 只负责在已有内存上构造对象,因此不能使用普通 delete 销毁对象,而应该显式调用对象的析构函数,再手动释放原始内存 。值得一提的是,C++ 中构造函数不能主动调用,必须通过对象初始化或定位 new 等方式调用;析构函数可以显式调用。
定位 new 的重载 C++ 中的定位 new(placement new)重载,通常是指为 operator new 重载增加一个额外的 void* 参数版本,其标准形式声明在 <new> 头文件中,作用是在调用者已经提供的、指向某块已分配内存的地址上构造一个对象,它本身并不分配内存,只返回传入的指针,随后由编译器在该内存地址上调用对象的构造函数。它与普通 operator new 重载的关键区别在于:普通 operator new 重载负责从堆上真正分配原始内存并返回地址,失败时按约定抛出 std::bad_alloc 或返回空指针,需要配套的 operator delete 来释放内存;而定位 new 重载只做 “将对象构造在指定内存地址上” 这件事,不负责分配内存,因此不应为它配套调用普通的 operator delete 来释放内存,对象的销毁需要显式调用析构函数,内存的释放则完全由提供该内存地址的代码负责。此外,由于定位 new 的参数列表里带有 void*,它可以被用户自定义重载以接受更多参数(如日志、分配器上下文等),但 C++ 标准库提供的那个 void* 版本是唯一不能被用户替换的;同时,定位 new 也不参与 “内存分配失败抛出 bad_alloc“ 这一常规分配语义,它更多是构造语义的入口,常用于容器、内存池、std::allocator 等需要将内存分配与对象构造分离的场景。
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 #include <cstdlib> #include <iostream> #include <new> class MyClass {public : MyClass () : m_a (0 ) { std::cout << "MyClass::MyClass()" << std::endl; } MyClass (const int val) : m_a (val) { std::cout << "MyClass::MyClass(const int val)" << std::endl; } ~MyClass () { std::cout << "MyClass::~MyClass()" << std::endl; } static void *operator new (size_t size) { std::cout << "MyClass::operator new(size_t size)" << std::endl; void *ptr = malloc (size); if (ptr == nullptr ) { throw std::bad_alloc (); } return ptr; } static void *operator new (size_t size, void *ptr) { std::cout << "MyClass::operator new(size_t size, void *ptr)" << std::endl; return ptr; } private : int m_a; }; int main () { void *ptr = (void *)new char [sizeof (MyClass)]; MyClass *mc = new (ptr) MyClass (); mc->~MyClass (); delete [] (char *)ptr; return 0 ; }
程序运行输出的结果如下:
1 2 3 MyClass::operator new(size_t size, void *ptr) MyClass::MyClass() MyClass::~MyClass()
内存池的简单实现 案例代码实现 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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 #include <cstddef> #include <iostream> class MyClass {public : MyClass () { std::cout << "MyClass::MyClass()" << std::endl; } ~MyClass () { std::cout << "~MyClass()" << std::endl; } static void * operator new (std::size_t size) ; static void operator delete (void * phead) ; static int m_new_count; static int m_malloc_count; private : MyClass* m_next; static MyClass* m_free_posi; static int m_trunk_count; }; int MyClass::m_new_count = 0 ;int MyClass::m_malloc_count = 0 ;MyClass* MyClass::m_free_posi = nullptr ; int MyClass::m_trunk_count = 5 ; void * MyClass::operator new (std::size_t size) { MyClass* tmplink; if (m_free_posi == nullptr ) { size_t realsize = size * m_trunk_count; m_free_posi = reinterpret_cast <MyClass*>(new char [realsize]); tmplink = m_free_posi; for (int i = 0 ; i < m_trunk_count - 1 ; ++i) { tmplink->m_next = tmplink + 1 ; ++tmplink; } tmplink->m_next = nullptr ; ++m_malloc_count; } tmplink = m_free_posi; m_free_posi = m_free_posi->m_next; ++m_new_count; return tmplink; } void MyClass::operator delete (void * phead) { if (phead == nullptr ) { return ; } MyClass* p = static_cast <MyClass*>(phead); p->m_next = m_free_posi; m_free_posi = p; } int main () { MyClass* p1 = new MyClass; MyClass* p2 = new MyClass; MyClass* p3 = new MyClass; MyClass* p4 = new MyClass; MyClass* p5 = new MyClass; delete p1; delete p2; MyClass* p6 = new MyClass; MyClass* p7 = new MyClass; delete p3; delete p4; delete p5; delete p6; delete p7; std::cout << "new count: " << MyClass::m_new_count << std::endl; std::cout << "malloc count: " << MyClass::m_malloc_count << std::endl; return 0 ; }
程序运行输出的结果如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 MyClass::MyClass() MyClass::MyClass() MyClass::MyClass() MyClass::MyClass() MyClass::MyClass() ~MyClass() ~MyClass() MyClass::MyClass() MyClass::MyClass() ~MyClass() ~MyClass() ~MyClass() ~MyClass() ~MyClass() new count: 7 malloc count: 1
案例代码分析 使用内存池后,通过 new 创建对象的图解
使用内存池后,通过 delete 销毁对象的图解
案例代码问题 上述案例代码只为演示内存池的简单实现,其中存在以下多个问题;若需要真正实现 C++ 内存池,强烈建议参考 Nginx 内存池源码和 SGI STL 内存池源码的底层实现。
内存池的优化实现 在前面的内存池案例代码中,每个 MyClass 对象都包含一个 m_next 指针。当空闲内存块被分配给对象使用后,m_next 指针不再用于维护空闲链表,这部分的内存空间实际上不会被使用到。为了减少这种额外的成员开销,可以利用嵌入式指针 :仅在内存块处于空闲状态时,利用其自身的内存空间保存下一块空闲内存块的地址,从而维护空闲链表。
嵌入式指针概念 在 C++ 中,嵌入式指针(Embedded Pointer)通常指:把一个指针直接存放在空闲内存块内部,用这个指针记录下一个空闲内存块的位置。它常用于内存池、对象池、Free List(空闲链表)等场景。为什么叫 “嵌入式”?因为这个指针不是额外分配的内存,而是直接 “嵌入” 到空闲内存块本身。
1 2 3 4 5 +----------------+ +----------------+ +----------------+ | next 指针 | ---> | next 指针 | ---> | next 指针 | | | | | | nullptr | +----------------+ +----------------+ +----------------+ 空闲块1 空闲块2 空闲块3
每个空闲块的前几个字节(不同平台字节数有差异)不再存放用户对象,而是直接存放一个指针,例如: 1 2 3 struct FreeBlock { FreeBlock* next; };
这里的 next 就可以理解为嵌入式指针,完整的案例代码如下:
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 #include <iostream> class MyTest {public : struct FreeBlock { struct FreeBlock *next ; }; private : int m_i; int m_j; int m_k; }; int main () { MyTest mytest; std::cout << sizeof (mytest) << std::endl; MyTest::FreeBlock *ptemp; ptemp = reinterpret_cast <MyTest::FreeBlock *>(&mytest); std::cout << sizeof (MyTest::FreeBlock) << std::endl; std::cout << sizeof (ptemp->next) << std::endl; ptemp->next = nullptr ; return 0 ; }
总结
嵌入式指针是利用空闲内存块自身的空间保存下一个空闲内存块的地址,从而将多个空闲内存块组织成链表。 使用嵌入式指针的前提是,类对象的 sizeof() 大小不能小于嵌入式指针本身的大小,否则无法在空闲内存块中存放该嵌入式指针 。案例代码实现 使用嵌入式指针优化内存池的实现
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 #include <cstdlib> #include <iostream> #include <new> #include <vector> class MyAllocator {public : ~MyAllocator () { for (void * memory_block : m_memory_blocks) { free (memory_block); } } void * allocate (size_t size) { if (size < sizeof (Object)) { throw std::bad_alloc (); } Object* tmplink; if (m_free_posi == nullptr ) { size_t real_size = size * m_trunk_count; void * memory_block = (Object*)malloc (real_size); if (memory_block == nullptr ) { throw std::bad_alloc (); } m_memory_blocks.push_back (memory_block); m_free_posi = (Object*)memory_block; tmplink = m_free_posi; for (int i = 0 ; i < m_trunk_count - 1 ; ++i) { tmplink->next = (Object*)((char *)tmplink + size); tmplink = tmplink->next; } tmplink->next = nullptr ; } tmplink = m_free_posi; m_free_posi = m_free_posi->next; return tmplink; } void deallocate (void * phead) { if (phead == nullptr ) { return ; } Object* p = (Object*)phead; p->next = m_free_posi; m_free_posi = p; } private : struct Object { struct Object * next ; }; size_t m_trunk_count = 5 ; Object* m_free_posi = nullptr ; std::vector<void *> m_memory_blocks; }; class Student {public : static MyAllocator allocator; static void * operator new (size_t size) { return allocator.allocate (size); } static void operator delete (void * p) { allocator.deallocate (p); } private : int age = 0 ; int code = 0 ; }; MyAllocator Student::allocator; int main () { Student* students[100 ]; for (int i = 0 ; i < 10 ; ++i) { students[i] = new Student (); std::cout << students[i] << std::endl; } for (int i = 0 ; i < 10 ; ++i) { delete students[i]; } return 0 ; }
程序运行输出的结果如下:
1 2 3 4 5 6 7 8 9 10 0x232506d1690 0x232506d1698 0x232506d16a0 0x232506d16a8 0x232506d16b0 0x232506d16e0 0x232506d16e8 0x232506d16f0 0x232506d16f8 0x232506d1700
为了更好地复用内存池代码,可以引入宏定义
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 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 #include <cstdlib> #include <iostream> #include <new> #include <vector> class MyAllocator {public : ~MyAllocator () { for (void * memory_block : m_memory_blocks) { free (memory_block); } } void * allocate (size_t size) { if (size < sizeof (Object)) { throw std::bad_alloc (); } Object* tmplink; if (m_free_posi == nullptr ) { size_t real_size = size * m_trunk_count; void * memory_block = (Object*)malloc (real_size); if (memory_block == nullptr ) { throw std::bad_alloc (); } m_memory_blocks.push_back (memory_block); m_free_posi = (Object*)memory_block; tmplink = m_free_posi; for (int i = 0 ; i < m_trunk_count - 1 ; ++i) { tmplink->next = (Object*)((char *)tmplink + size); tmplink = tmplink->next; } tmplink->next = nullptr ; } tmplink = m_free_posi; m_free_posi = m_free_posi->next; return tmplink; } void deallocate (void * phead) { if (phead == nullptr ) { return ; } Object* p = (Object*)phead; p->next = m_free_posi; m_free_posi = p; } private : struct Object { struct Object * next ; }; size_t m_trunk_count = 5 ; Object* m_free_posi = nullptr ; std::vector<void *> m_memory_blocks; }; #define DECLARE_POOL_ALLOCATOR() \ public: \ static MyAllocator allocator; \ void* operator new(size_t size) { \ return allocator.allocate(size); \ } \ void operator delete(void* p) { \ allocator.deallocate(p); \ } #define IMPLEMENT_POOL_ALLOCATOR(ClassName) \ MyAllocator ClassName::allocator; \ class Student { DECLARE_POOL_ALLOCATOR () private : int age = 0 ; int code = 0 ; }; IMPLEMENT_POOL_ALLOCATOR (Student)int main () { Student* students[100 ]; for (int i = 0 ; i < 10 ; ++i) { students[i] = new Student (); std::cout << students[i] << std::endl; } for (int i = 0 ; i < 10 ; ++i) { delete students[i]; } return 0 ; }
程序运行输出的结果如下:
1 2 3 4 5 6 7 8 9 10 0x232506d1690 0x232506d1698 0x232506d16a0 0x232506d16a8 0x232506d16b0 0x232506d16e0 0x232506d16e8 0x232506d16f0 0x232506d16f8 0x232506d1700
案例代码分析 使用嵌入式指针后的内存池存储结构,绿色部分是空闲内存块,紫色部分是嵌入式指针
案例代码问题 上述案例代码只为演示内存池的简单实现,其中存在以下多个问题;若需要真正实现 C++ 内存池,强烈建议参考 Nginx 内存池源码和 SGI STL 内存池源码的底层实现。
内存对齐问题
内存池通过 地址 + size 计算下一个内存块地址,因此需要保证每个内存块的起始地址满足对象的对齐要求。当前 Student 的 sizeof(Student) 刚好能够满足其内存对齐要求,因此通常不存在问题。但是,如果将 MyAllocator 扩展为一个通用内存池,支持任意数据类型,这就可能无法满足内存对齐要求,此时需要正式考虑 alignof(T),特别是 C++ 17 的过度对齐(Over-Aligned)类型。 没有考虑多线程
当前实现中的 m_free_posi、m_memory_blocks 都是多个线程共享的数据,没有进行线程同步保护。如果多个线程同时执行 new 或 delete,可能同时修改空闲链表,从而产生数据竞争甚至破坏内存池。因此当前版本只适合单线程环境,多线程场景需要增加锁或采用其他并发内存池方案。