C++:内存管理 |内存分布|回顾|new/delete底层|实现原理|拓展|定位new|池化技术|总结区别对比
上篇文章:
https://blog.csdn.net/2401_86123468/article/details/153559422?spm=1001.2014.3001.5501
与本文完全相关的C语言:动态内存管理的文章:
https://blog.csdn.net/2401_86123468/article/details/149401001?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522065e0f5ad0bddf75224a00bcc77d1bf3%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=065e0f5ad0bddf75224a00bcc77d1bf3&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-10-149401001-null-null.nonecase&utm_term=malloc&spm=1018.2226.3001.4450https://blog.csdn.net/2401_86123468/article/details/149401001?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522065e0f5ad0bddf75224a00bcc77d1bf3%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fblog.%2522%257D&request_id=065e0f5ad0bddf75224a00bcc77d1bf3&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~blog~first_rank_ecpm_v1~rank_v31_ecpm-10-149401001-null-null.nonecase&utm_term=malloc&spm=1018.2226.3001.4450
本文阅读可选择略读第二小节内容(更详细的内容见上述链接,C语言主题已详细分析)和池化技术内容(现在接触过早,后续文章会讲)。
前言
程序运行时的内存,处在虚拟进程地址空间中。一段C++程序中,变量,函数,对象均藏在进程的虚拟地址空间里,现代操作系统会为每个进程分配独立的虚拟进程地址空间,在32位系统下,这篇空间最大可达4GB(2^32字节),64位系统则扩展到2^64字节的量级。
我们将操作系统类比为“工厂”,那么进程就是“工人”(在分配好的“虚拟地址空间”里执行任务),更关键的是,这片“虚拟地址空间”被精细的划分为多个功能明确的区域。
栈:“编译器自动管控的高效区域”。函数的局部变量、函数调用的上下文(比如返回地址、传递的参数)都存储在这里。栈遵循 “先进后出” 的规则,分配与释放的效率极高,但空间相对有限。
堆:“程序员主导的灵活区域”。需要动态创建的对象(比如用new申请的内存)就存放在堆中。堆的空间更为广阔,但需要程序员手动管理分配与释放—— 若处理不当,“内存泄漏”“重复释放” 等问题会接踵而至。
静态区(数据段):“全局 / 静态变量的‘常驻’区域”。程序启动时,操作系统会为全局变量、静态变量分配内存;直到程序结束,这片内存才会被回收。
常量区(代码段):“存储指令与常量的‘只读’区域”。这里不仅有程序要执行的代码(指令),还包含字符串常量等不可修改的数据。为了保证程序安全,常量区通常是 “只读” 的,防止指令或常量被意外篡改。
1.C/C++内存分布
先阅读下方代码,并给出自己的观点:
回答问题:
详细讲解:
1. globalVar
在哪里?选 C(数据段)
globalVar
是全局变量,全局变量存储在 数据段(静态区)。程序启动时为全局变量分配内存,直到程序结束才会回收这片内存。
2. staticGlobalVar
在哪里?选 C(数据段)
staticGlobalVar
是静态全局变量。“static
修饰全局变量” 时,变量仍存储在数据段(静态区),且作用域限制在当前文件,但生命周期伴随整个程序(从启动到结束)。
3. staticVar
在哪里?选 C(数据段)
staticVar
是函数内的静态局部变量。虽然定义在函数内部,但 “static
修饰局部变量” 会改变其存储位置:从 “栈” 转移到数据段(静态区),且生命周期延长为 “伴随整个程序”(仅初始化一次,函数多次调用时保持值的连续性)。
4. localVar
在哪里?选 A(栈)
localVar
是函数内的普通局部变量(无 static
修饰)。局部变量存储在 栈 中,函数调用时自动为栈上的局部变量分配空间,函数结束时栈空间自动释放,局部变量的生命周期也随之结束。
5. num1
在哪里?选 A(栈)
num1
是函数内的局部数组。数组属于 “复合局部变量”,存储在 栈 中,函数结束时栈上的数组空间会自动释放。
6. char2
在哪里?选 A(栈)
char2
是函数内的局部字符数组(char char2[] = "abcd";
)。此时是 “用字符串常量初始化字符数组”,会把字符串 "abcd"
的内容拷贝到栈上的数组 char2
中,因此数组 char2
本身存储在栈。
7. *char2
在哪里?选 A(栈)
char2
是栈上的字符数组,*char2
表示数组的首元素(char2[0]
)。首元素属于数组的一部分,因此也存储在栈中。
8. pChar3
在哪里?选 A(栈)
pChar3
是函数内的局部指针变量。指针变量本身(即存储 “地址” 的变量)存储在 栈 中,函数结束时栈上的指针空间会自动释放(但指针指向的内容是否释放,需看指向的区域)。
9. *pChar3
在哪里?选 D(代码段 / 常量区)
pChar3
是 const char*
类型,指向字符串常量 "abcd"
。字符串常量(如 "abcd"
)存储在 代码段(常量区),该区域是 “只读” 的,程序运行时不会被修改。
10. ptr1
在哪里?选 A(栈)
ptr1
是函数内的局部指针变量。和 pChar3
类似,指针变量本身(存储 malloc
分配的堆内存地址)存储在 栈 中,函数结束时栈上的指针空间自动释放。
11. *ptr1
在哪里?选 B(堆)
ptr1
通过 malloc
函数动态分配内存(malloc
/calloc
/realloc
或 C++ 的 new
都用于堆内存分配)。动态分配的内存存储在 堆 中,需要手动通过 free
(或 C++ 的 delete
)释放,否则会造成 “内存泄漏”。
2.C语言中动态内存管理方式:malloc / calloc / realloc / free
2.1malloc/calloc/realloc的区别
1. malloc
函数
- 功能:从堆中分配一块指定字节数的内存。
- 初始化:分配的内存不初始化,内容是随机的 “垃圾值”(不确定)。
- 语法:
void* malloc(size_t size);
(size
为需要分配的总字节数)。 - 返回值:成功时返回分配内存的首地址(需强制转换为目标类型指针);失败时返回
NULL
。 - 适用场景:仅需分配内存、不关心初始内容的场景(比如后续会立即给内存赋值)。
2. calloc
函数
- 功能:从堆中分配连续的、指定数量 × 单个大小的内存(总大小为
num * size
)。 - 初始化:会将分配的内存全部初始化为 0,无 “垃圾值”。
- 语法:
void* calloc(size_t num, size_t size);
(num
是元素数量,size
是单个元素的字节大小)。 - 返回值:成功时返回分配内存的首地址(需强制转换);失败时返回
NULL
。 - 适用场景:需要内存初始为 0 的场景(比如数组、结构体的初始化)。
3. realloc
函数
- 功能:调整已分配内存块的大小(可扩大或缩小)。
- 初始化:
- 若扩大内存且需要 “新分配” 部分内存(如原地址后空间不足,需换地址),新分配的内存会初始化为 0;
- 若缩小内存,原有数据会保留,被 “裁掉” 的部分数据丢失。
- 语法:
void* realloc(void* ptr, size_t new_size);
(ptr
是原内存指针,new_size
是新的总字节数)。 - 返回值:成功时返回调整后内存的指针(可能是原地址,也可能是新地址);失败时返回
NULL
,且原内存块不会被释放(仍保留原数据)。 - 特殊情况:
- 若
ptr
为NULL
,realloc
等价于malloc(new_size)
(全新分配内存); - 若
new_size
为0
,realloc
等价于free(ptr)
(释放原内存,返回NULL
)。
- 若
- 适用场景:已分配内存后,需要扩大 / 缩小内存的场景(比如动态数组的扩容)。
总结对比
特性 | malloc | calloc | realloc |
---|---|---|---|
功能 | 分配指定字节的内存 | 分配并初始化 0 的内存(num×size ) | 调整已分配内存的大小 |
初始化 | 不初始化(内容为垃圾值) | 初始化为 0 | 扩展时新内存初始为 0;缩小时原数据保留 |
参数 | 1 个(总字节数 size ) | 2 个(元素数量 num 、单个大小 size ) | 2 个(原指针 ptr 、新总字节数 new_size ) |
返回值 | 分配内存的指针(失败返回 NULL ) | 分配内存的指针(失败返回 NULL ) | 调整后内存的指针(失败返回 NULL ,原内存保留) |
适用场景 | 只分配内存,不关心初始值 | 需要初始为 0 的内存(如数组) | 调整已有内存的大小(如动态扩容) |
2.2malloc的实现原理
https://www.bilibili.com/video/BV117411w7o2/?spm_id_from=333.788.videocard.0
3.C++内存管理方式
C++拥有自己的内存管理方式:通过new和delete操作符进行动态内存管理。
3.1new/delete操作内置类型
C++中并没有提供类似realloc扩容的方式,需要自己手工扩容。
3.2new和delete操作自定义类型
既然c++中也可以使用c语言中的malloc等,那为什么还要创建new这类方式呢?
原因是:C语言中的方式只开空间,不调用构造初始化
使用C++构建链表更方便:
struct ListNode
{ListNode* _next;int _val;ListNode(int val):_next(nullptr), _val(val){ }
};
int main()
{ListNode* n1 = new ListNode(1);ListNode* n2 = new ListNode(2);ListNode* n3 = new ListNode(3);return 0;
}
3.3new与malloc的返回区别
malloc失败会返回空指针,new失败会抛异常,而对于异常,我们需要捕获,见代码:
void func()
{int i = 1;int* ptr = nullptr;do {if (i == 11105){int x = 0;}ptr = new int[1024 * 1024 ];cout << i++ << ":" << ptr << endl;} while (ptr);cout << i++ << ":" << ptr << endl;
}
int main()
{try{func();}catch (const exception& e){cout << e.what() << endl;}return 0;
}
注意:此时我知道会在11105处抛异常,可以写代码中被框起来的if语句,此作用类似于打断点。
4.new/delete底层
new在面对自定义的类类型时,需要两步:
1.在堆上开空间(从底层上讲,new使用的依旧是malloc,可以认为new是malloc的再包装)
2.调用构造函数
delete是free的封装
4.1operator new 与 operator delete函数
new和delete是用户进行动态内存申请和释放的操作符,operatornew和operatordelete是系统提供的全局函数,new在底层调用operator new全局函数来申请空间,delete在底层通过operator delete全局函数来释放空间。
系统的底层代码:
/*
operator new:该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间
失败,尝试执行空 间不足应对措施,如果改应对措施用户设置了,则继续申请,否
则抛异常。
*/
void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{// try to allocate size bytesvoid* p;while ((p = malloc(size)) == 0)if (_callnewh(size) == 0){// report no memory// 如果申请内存失败了,这里会抛出bad_alloc 类型异常static const std::bad_alloc nomem;_RAISE(nomem);}return (p);
}
/*
operator delete: 该函数最终是通过free来释放空间的
*/
void operator delete(void* pUserData)
{_CrtMemBlockHeader* pHead;RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));if (pUserData == NULL)return;_mlock(_HEAP_LOCK); /* block other threads */__TRY/* get a pointer to memory block header */pHead = pHdr(pUserData);/* verify block type */_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));_free_dbg(pUserData, pHead->nBlockUse);__FINALLY_munlock(_HEAP_LOCK); /* release other threads */__END_TRY_FINALLYreturn;
}
/*
free的实现
*/
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
通过上述两个全局函数的实现知道,operator new实际也是通过malloc来申请空间,如果malloc申请空间成功就直接返回,否则执行用户提供的空间不足应对措施,如果用户提供该措施就继续申请,否则就抛异常。operator delete最终是通过free来释放空间的。
5.new和delete的实现原理
5.1内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不同的地方是:new/delete申请和释放的是单个元素的空间,new和delete申请的是连续空间,而且new在申请空间失败时会抛异常,malloc会返回NULL。
5.2自定义类型
转到反汇编:
new的原理
1.调用operatornew函数申请空间
2.在申请的空间上执行构造函数,完成对象的构造
delete的原理
1.在空间上执行析构函数,完成对象中资源的清理工作
2.调用operatordelete函数释放对象的空间
newT[N]的原理
1.调用operator new[函数,在operator new』中实际调用operator new函数完成N个对象空间的申请
2.在申请的空间上执行N次构造函数
delete[]的原理
1.在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
2.调用operator delete释放空间,实际在operator delete[中调用operator delete来释放空间
6.拓展
6.1为什么多出4个字节
根据代码,此处应该有40个字节,但转到反汇编后我们发现,居然有44个字节,这是为什么呢?
实际上,这多出的四个字节,是需要存数组中对应对象的个数:
6.2为什么又“正常”了
此时又是40个字节
看代码,此时没有析构,只需要调用默认析构,且代码中没有什么资源需要释放,因此聪明的编译器不会多开字节去存个数。
7.定位new表达式(placement-new)
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。
使用格式:
new (place_address) type或者new (place_address) type(initializer-list)
place_address必须是一个指针,initializer-list是类型的初始化列表
使用场景:
定位new表达式在实际中一般是配合内存池使用。因为内存池分配出的内存没有初始化,所以如果是自定义类型的对象,需要使用new的定义表达式进行显示调构造函数进行初始化。
7.1池化技术
池化技术(Pooling) 是一种预分配资源、重复利用的技术思想。
核心目标是:
避免频繁的创建与销毁(尤其是代价昂贵的资源,如内存块、数据库连接、线程、文件句柄等)。
举几个常见的“池”的类型:
类型 | 用途 | 示例 |
---|---|---|
连接池(Connection Pool) | 复用数据库连接 | Java JDBC连接池、Redis连接池 |
线程池(Thread Pool) | 重用线程执行任务 | std::thread + 任务队列 |
内存池(Memory Pool) | 复用内存块 | Boost Pool、tcmalloc、jemalloc、Google 自研 Arena |
7.2内存池
C++ 默认的 new
/ delete
或 malloc
/ free
都要经过:
-
内核空间与用户空间切换;
-
内存分配器(glibc malloc)的锁;
-
较高的碎片化风险。
而 内存池 则是:
预先申请一大块内存,然后自己在这块区域内进行小块分配和回收。
内存池的基本结构
-
Pool(池体):管理大块连续内存。
-
Block(块):将池体划分为多个固定大小的块。
-
Free List(空闲链表):记录哪些块当前可用。
7.3Google 的内存池技术(以 tcmalloc
为例)
tcmalloc
:高性能多线程内存分配器
-
TCMalloc(Thread-Caching Malloc) 是 Google 为多线程环境优化的内存分配器。
-
它解决了 glibc malloc 的全局锁问题。
原理简述:
-
每个线程维护一个本地缓存(Thread Cache),避免锁竞争;
-
按对象大小分级(size class);
-
小对象使用内存池分配,大对象直接系统分配;
-
回收时放回对应的缓存。
性能对比:
分配器 | 多线程性能 | 小块分配速度 |
---|---|---|
glibc malloc | 慢(锁争用) | 一般 |
jemalloc | 稳定 | 快 |
tcmalloc | 极快 | 极快 |
在 Google 的服务器程序、Chrome 浏览器中广泛使用。
7.4总结对比
方案 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
普通 new/delete | 简单,自动管理 | 慢,碎片多 | 小规模 |
自建内存池 | 快速复用 | 管理复杂 | 固定大小对象 |
TCMalloc / Jemalloc | 通用高性能 | 较大依赖 | 服务端并发系统 |
Google Arena | 超快,批量释放 | 不支持单独回收 | 短生命周期对象 |
8.malloc/free和new/delete的区别
可从用法,核心特性,原理去理解
malloc/free和new/delete的共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
1.malloc和free是函数,new和delete是操作符
2.malloc申请的空间不会初始化,new可以初始化
3.malloc申请空间时,需要手动计算空间大小并传递,new只需在其后跟上空间的类型即可,如果是多个对象,[]中指定对象个数即可
4.malloc的返回值为void*,在使用时必须强转,new不需要,因为new后跟的是空间的类型
5.malloc申请空间失败时,返回的是NULL,因此使用时必须判空,new不需要,但是new需要捕获异常
6.申请自定义类型对象时,malloc/free只会开辟空间,不会调用构造函数与析构函数,而new在申请空间后会调用构造函数完成对象的初始化,delete在释放空间前会调用析构函数完成空间中资源的清理释放
本章完。