知识点#
18.1 new、delete 的进一步认识#
- @ new与delete深入图 ![[白板/P56 new与delete深入.canvas|new与delete深入]]
18.1.1 总述与回顾#
[!tip] 回顾
new/delete的基本使用已在 13.4.2 节和 16.2 节讲解- 本章从高级角度深入探讨内存相关知识
- 理解内存高级话题有助于理解模板中的内存分配等概念
- 实战中常遇到内存池等概念
18.1.2 从 new 说起#
new 类对象加与不加括号的差别#
[!tip] new类对象的括号差异
new A()vsnew A:
- 空类:无区别
- 有成员变量无构造函数:
new A()成员变量初始化为0,new A成员变量为随机值- 有构造函数:两者相同,初始化工作交给构造函数
简单类型:
new int:初值为随机值new int():初值为0new int(100):初值为100
new 做了什么事#
[!tip] new关键字的调用关系
new关键字主要做了两件事:
- 调用
operator new()分配内存- 调用类的构造函数
完整调用关系:
A *pa = new A(); // 操作符 operator new(); // 函数 malloc(); // C风格函数分配内存 A::A(); // 有构造函数就调用构造函数
- !
operator new是一个函数,可以被直接调用:operator new(12);
[!tip] delete关键字的调用关系
delete关键字的调用关系(注意顺序):delete pa; A::~A(); // 先调用析构函数 operator delete(); // 函数 free(); // C风格函数释放内存
new 与 malloc 的区别#
[!tip] new与malloc区别(面试必答)
new是关键字/操作符,malloc是函数new不但分配内存,还会调用类的构造函数new可以将成员变量初始化为0,malloc没这个能力delete不但释放内存,还会调用析构函数;free只释放内存
- ! 一般提倡使用
new/delete,不提倡使用malloc/free(C编程风格)
18.2 new 内存分配细节探秘与重载类内 operator new、delete#
- @ 内存分配细节图 ![[白板/P56 new与delete深入.canvas|new与delete深入]]
18.2.1 new 内存分配细节探秘#
[!tip] malloc分配内存的真相
- 分配
10字节内存,实际分配远不止10字节- 额外内存用于记录:分配字节数、调试信息、边界调整填充、回收内存标记等
- 在分配的内存首地址前
12字节位置记录了分配出去的字节数free释放内存时影响的范围远大于分配的字节数==程序员拿到的地址是
malloc分配的整块内存中间的某个位置==
[!tip] 内存分配与释放的合并
- 释放内存时,
free函数负责将临近的空闲内存块合并- 合并空闲块、登记空闲块大小、设置标记等都是
free的职责- 频繁分配小块内存浪费更加严重
18.2.2 重载类中的 operator new 和 operator delete 操作符#
[!tip] 重载类内operator new/delete
- 在类中重载
operator new和operator delete可以取代系统的同名函数- 重载后的函数负责分配和释放内存
operator new应为static函数(new对象时还没对象存在)class A { public: static void *operator new(size_t size); static void operator delete(void *phead); }; void *A::operator new(size_t size) { cout << "A::operator new被调用了" << endl; A *ppoint = (A *)malloc(size); return ppoint; } void A::operator delete(void *phead) { cout << "A::operator delete被调用了" << endl; free(phead); }
- ! 调用
::new A()使用全局new,不调用类内重载的operator new,但仍会调用构造函数
18.2.3 重载类中的 operator new[] 和 operator delete[] 操作符#
[!tip] 数组操作符重载
new A[3]()调用的是operator new[],不是operator new
operator new[]和operator delete[]只调用1次构造函数和析构函数分别调用
3次(数组大小)对象数组分配时,额外
4字节记录数组大小
malloc返回的首地址与程序员拿到的指针相差4字节
[!tip] 对象数组内存概貌
| 记录字节数(4B) | 调试信息(~几十B) | 数组大小(4B) | 3个对象(3B) | 边界填充 | 回收标记(4B) | ↑ 程序员拿到的指针
- 多出的
4字节记录数组元素个数,delete[]时据此知道调用多少次析构函数
18.3 内存池概念、代码实现和详细分析#
- @ 内存池图 ![[白板/P57 内存池.canvas|内存池]]
18.3.1 内存池的概念和实现原理简介#
[!tip] 内存池概念
内存池: ==用
malloc申请一大块内存,分配时从大块中一点点分配,用完再申请一大块==
- 解决的主要问题:减少
malloc调用次数,减少内存浪费- 提高运行效率是顺带的,
malloc本身执行速度极快- 实现原理:一次申请一大块,小块分配,用完再申请
18.3.2 针对一个类的内存池实现演示代码#
[!tip] 内存池实现核心思路
- 通过重载类内
operator new/operator delete实现内存池operator new:从空闲链表中取出一块返回;链表为空时一次性malloc多块operator delete:不调用free,而是将内存块链回空闲链表class A { public: static void *operator new(size_t size); static void operator delete(void *phead); static int m_iCount; // 分配计数 static int m_iMallocCount; // malloc次数 private: A *next; // 空闲链表指针 static A *m_FreePosi; // 指向可分配内存的首地址 static int m_sTrunkCount; // 一次分配多少块 };
[!tip] 内存池分配流程
- 第一次
new:m_FreePosi为nullptr,malloc一大块(m_sTrunkCount块),用next指针链成链表- 后续
new:从m_FreePosi取出一块,m_FreePosi指向下一块- 链表用完:再次
malloc一大块,链入空闲链表delete:将释放的内存块链回空闲链表头部,m_FreePosi指向它
- ! 内存池的
operator delete并不真正归还内存给系统(不调用free),而是链回空闲链表 - !
m_sTrunkCount不宜设置太大,否则首次分配浪费;几十之间较合适
[!tip] 内存池性能对比
方式 500万次分配用时 malloc次数内存池(每次5块) ~500-800ms 100万次 内存池(每次500块) ~300-500ms 1万次 原生 malloc~1000-1500ms 500万次
malloc速度本身极快,内存池提升效率有限- 内存池主要价值在于==减少内存浪费==
18.3.3 内存池代码后续说明#
- % 每
5个分配的内存地址是挨着的(间隔4字节),说明内存池在发挥作用 - ! 内存池代码不完善:释放时没有真正
delete,只是链回空闲链表 - ! 程序退出时建议真正释放内存池占用的内存
18.4 嵌入式指针概念及范例、内存池改进版#
- @ 嵌入式指针图 ![[白板/P57 内存池.canvas|内存池]]
18.4.1 嵌入式指针#
[!tip] 嵌入式指针(embedded pointer)
嵌入式指针: ==借用对象所占内存空间的前
4字节来保存空闲链表指针,省去额外的next成员变量==
- 前提条件:类的
sizeof必须 >=4字节- 工作原理:空闲时前
4字节保存链表指针;分配出去后被对象数据覆盖,无影响- 常用于内存池代码实现中
[!tip] 嵌入式指针实现方式
class TestEP { public: int m_i; int m_j; private: struct obj { struct obj *next; // 嵌入式指针 }; };
struct obj定义在类内部(private),限制外部使用next指针借用对象前4字节,空闲时链住空闲块
18.4.2 内存池代码的改进#
[!tip] 独立内存池类 myallocator
- 将内存池逻辑提取为独立的
myallocator类- 使用嵌入式指针替代
next成员变量- 任何类只需声明静态
myallocator成员即可使用class myallocator { public: void *allocate(size_t size); // 分配内存接口 void deallocate(void *phead); // 释放内存接口 private: struct obj { struct obj *next; }; // 嵌入式指针 int m_sTrunkCount = 5; obj *m_FreePosi = nullptr; };
[!tip] 使用宏简化内存池声明
#define DECLARE_POOL_ALLOC() \ public: \ static void *operator new(size_t size) { return myalloc.allocate(size); } \ static void operator delete(void *phead) { return myalloc.deallocate(phead); } \ static myallocator myalloc; #define IMPLEMENT_POOL_ALLOC(classname) \ myallocator classname::myalloc; // 使用 class A { DECLARE_POOL_ALLOC(); public: int m_i; int m_j; }; IMPLEMENT_POOL_ALLOC(A)
18.5 重载全局 new/delete、定位 new 及重载#
- @ 定位new图 ![[白板/P58 定位new.canvas|定位new]]
18.5.1 重载全局 operator new 和 operator delete 操作符#
[!tip] 重载全局operator new/delete
void *operator new(size_t size) { return malloc(size); } void *operator new[](size_t size) { return malloc(size); } void operator delete(void *phead) { free(phead); } void operator delete[](void *phead) { free(phead); }
- 必须放在全局空间,不能放在自定义命名空间
- 影响面太广,==很少在实际项目中使用==
- 类内重载会覆盖全局重载
18.5.2 定位 new(placement new)#
[!tip] 定位new(placement new)
定位new: ==在已经分配好的原始内存中初始化一个对象(调用构造函数),不额外分配内存==
- 不分配内存,内存必须事先分配好
- 只初始化对象(调用构造函数)
- 没有
placement delete
[!tip] 定位new用法
// 格式:new (地址) 类类型(参数) // 1. 先分配内存 void *mymemPoint = (void *)new char[sizeof(PLA)]; // 2. 定位new:在已分配内存上构造对象 PLA *pmyAobj1 = new(mymemPoint) PLA(); // 无参构造 PLA *pmyAobj2 = new(mymemPoint2) PLA(12); // 带参构造 // 3. 手动调用析构函数 pmyAobj1->~PLA(); // 4. 释放原始内存 delete[](void *)pmyAobj1;
[!tip] 定位new的调用关系
PLA *pa = new(分配好的内存首地址) PLA(); // 定位new操作符 operator new(); // 函数,不调用malloc PLA::PLA(); // 调用构造函数
- 定位new的
operator new不调用malloc,不分配内存- 可以在类中重载定位new的
operator new,参数比传统new多一个
[!tip] 重载定位new的operator new
// 在类PLA中重载 void *operator new(size_t size, void *phead) { // 不要分配内存 return phead; // 返回传入的内存地址 }
18.5.3 多种版本的 operator new 重载#
Reply by Email[!tip] 多版本operator new重载
- 可以重载多个版本的
operator new,只要参数不同- 第一个参数固定为
size_t(对象的sizeof值)- 其他参数通过
new时指定// 类中重载 void *operator new(size_t size, int typ1, int typ2) { return NULL; } void operator delete(void *phead, int typ1, int typ2) { return; } // 调用 PLA *pla = new(1234, 56) PLA();
- 这种用法不会调用类的构造函数
- 主要目的是扩大见闻,不建议常规使用

