C++ 内存管理:从内存分区到 new/delete 底层原理
写 C 或 C++ 程序时,我们经常会说某个变量"在栈上"或者"在堆上"。这句话看似简单,真正分析代码时却很容易混淆:指针变量可能在栈上,它指向的对象却在堆上;字符数组和字符串字面量内容相同,存储位置与可修改性并不相同;new 和 operator new 名字很像,负责的工作也不是一回事。
内存管理的问题通常不会在编译阶段暴露。申请与释放不匹配、对象析构没有执行、realloc 使用方式不当,都可能让程序在运行一段时间后才崩溃。下面从程序的内存区域开始,逐步讲到 malloc/free、new/delete、底层调用过程和定位 new。
一、程序运行时的内存区域
一个进程的虚拟地址空间通常会划分为多个区域。不同操作系统、编译器和链接方式会影响具体布局,下面展示的是常见的概念模型:
#mermaid-svg-wzKcNU63GzTRL3dS{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-wzKcNU63GzTRL3dS .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-wzKcNU63GzTRL3dS .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-wzKcNU63GzTRL3dS .error-icon{fill:#552222;}#mermaid-svg-wzKcNU63GzTRL3dS .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-wzKcNU63GzTRL3dS .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-wzKcNU63GzTRL3dS .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-wzKcNU63GzTRL3dS .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-wzKcNU63GzTRL3dS .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-wzKcNU63GzTRL3dS .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-wzKcNU63GzTRL3dS .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-wzKcNU63GzTRL3dS .marker{fill:#333333;stroke:#333333;}#mermaid-svg-wzKcNU63GzTRL3dS .marker.cross{stroke:#333333;}#mermaid-svg-wzKcNU63GzTRL3dS svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-wzKcNU63GzTRL3dS p{margin:0;}#mermaid-svg-wzKcNU63GzTRL3dS .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-wzKcNU63GzTRL3dS .cluster-label text{fill:#333;}#mermaid-svg-wzKcNU63GzTRL3dS .cluster-label span{color:#333;}#mermaid-svg-wzKcNU63GzTRL3dS .cluster-label span p{background-color:transparent;}#mermaid-svg-wzKcNU63GzTRL3dS .label text,#mermaid-svg-wzKcNU63GzTRL3dS span{fill:#333;color:#333;}#mermaid-svg-wzKcNU63GzTRL3dS .node rect,#mermaid-svg-wzKcNU63GzTRL3dS .node circle,#mermaid-svg-wzKcNU63GzTRL3dS .node ellipse,#mermaid-svg-wzKcNU63GzTRL3dS .node polygon,#mermaid-svg-wzKcNU63GzTRL3dS .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-wzKcNU63GzTRL3dS .rough-node .label text,#mermaid-svg-wzKcNU63GzTRL3dS .node .label text,#mermaid-svg-wzKcNU63GzTRL3dS .image-shape .label,#mermaid-svg-wzKcNU63GzTRL3dS .icon-shape .label{text-anchor:middle;}#mermaid-svg-wzKcNU63GzTRL3dS .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-wzKcNU63GzTRL3dS .rough-node .label,#mermaid-svg-wzKcNU63GzTRL3dS .node .label,#mermaid-svg-wzKcNU63GzTRL3dS .image-shape .label,#mermaid-svg-wzKcNU63GzTRL3dS .icon-shape .label{text-align:center;}#mermaid-svg-wzKcNU63GzTRL3dS .node.clickable{cursor:pointer;}#mermaid-svg-wzKcNU63GzTRL3dS .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-wzKcNU63GzTRL3dS .arrowheadPath{fill:#333333;}#mermaid-svg-wzKcNU63GzTRL3dS .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-wzKcNU63GzTRL3dS .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-wzKcNU63GzTRL3dS .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wzKcNU63GzTRL3dS .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-wzKcNU63GzTRL3dS .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wzKcNU63GzTRL3dS .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-wzKcNU63GzTRL3dS .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-wzKcNU63GzTRL3dS .cluster text{fill:#333;}#mermaid-svg-wzKcNU63GzTRL3dS .cluster span{color:#333;}#mermaid-svg-wzKcNU63GzTRL3dS div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-wzKcNU63GzTRL3dS .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-wzKcNU63GzTRL3dS rect.text{fill:none;stroke-width:0;}#mermaid-svg-wzKcNU63GzTRL3dS .icon-shape,#mermaid-svg-wzKcNU63GzTRL3dS .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-wzKcNU63GzTRL3dS .icon-shape p,#mermaid-svg-wzKcNU63GzTRL3dS .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-wzKcNU63GzTRL3dS .icon-shape .label rect,#mermaid-svg-wzKcNU63GzTRL3dS .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-wzKcNU63GzTRL3dS .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-wzKcNU63GzTRL3dS .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-wzKcNU63GzTRL3dS :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 高地址
内核空间
栈:局部变量、函数参数等
内存映射区:共享库、文件映射、共享内存
堆:运行时动态申请的空间
数据区:全局变量、静态变量
代码与只读区域:机器指令、只读常量
低地址
这张图适合帮助理解,不能把它当成所有平台都必须遵守的固定地址排列。例如,栈向低地址增长、堆向高地址增长是常见实现,但 C++ 语言本身并不规定增长方向。
1. 栈
栈主要保存函数调用相关的数据,例如非静态局部变量、函数参数和部分返回信息。函数调用结束后,对应栈帧会被回收,不需要程序员手动释放。
cpp
void Add(int left, int right) {
int result = left + right;
}
在常见实现中,left、right 和 result 都位于当前函数的栈帧中。它们的生命周期受函数调用控制。
2. 堆
堆用于运行时动态内存分配。malloc、calloc、realloc 和 new 申请的动态空间通常来自这里。空间不会因为申请它的函数结束而自动释放,必须按照对应规则归还。
cpp
int* pointer = new int(10);
delete pointer;
这里要把两个位置分开看:局部变量 pointer 本身通常在栈上,而 pointer 指向的整数对象位于动态存储区。
3. 数据区
全局变量和静态变量具有静态存储期,它们通常位于数据相关区域,生命周期贯穿整个程序运行过程。
cpp
int globalValue = 1;
static int fileValue = 2;
void Test() {
static int callCount = 0;
++callCount;
}
globalValue、fileValue 和 callCount 的作用域不同,但都具有静态存储期。callCount 只在第一次执行到它的定义时完成初始化,函数返回后也不会销毁。
4. 代码与只读区域
程序的机器指令通常存放在代码区域,字符串字面量等只读内容通常也位于只读区域。代码中可以通过指针读取字符串字面量,但不能修改它。
cpp
char array[] = "abcd";
const char* text = "abcd";
array 是一个字符数组,内容被复制到数组自身的存储空间,可以修改:
cpp
array[0] = 'A';
text 是指针变量,指向字符串字面量。下面的行为是错误的:
cpp
// text[0] = 'A'; // 编译器通常会阻止;强行修改会产生未定义行为
5. 一段代码看懂变量到底在哪里
cpp
#include <cstdlib>
int globalVar = 1;
static int staticGlobalVar = 1;
void Test() {
static int staticVar = 1;
int localVar = 1;
int numbers[4] = {1, 2, 3, 4};
char buffer[] = "abcd";
const char* literal = "abcd";
int* dynamicValue = static_cast<int*>(std::malloc(sizeof(int)));
std::free(dynamicValue);
}
| 表达式或对象 | 常见存储区域 | 分析 |
|---|---|---|
globalVar |
数据区 | 全局变量,具有静态存储期 |
staticGlobalVar |
数据区 | 静态全局变量 |
staticVar |
数据区 | 静态局部变量,不随函数返回销毁 |
localVar |
栈 | 非静态局部变量 |
numbers 及数组元素 |
栈 | 数组本体是局部对象 |
buffer 及字符内容 |
栈 | 字面量内容被复制进局部数组 |
literal |
栈 | 指针变量本身是局部变量 |
*literal |
只读区域 | 指向字符串字面量中的字符 |
dynamicValue |
栈 | 指针变量本身位于当前栈帧 |
*dynamicValue |
堆 | malloc 返回的动态空间 |
判断时不要只盯着变量名。看到指针,至少要问两遍:指针本身在哪里,它指向的对象又在哪里。
二、C 风格动态内存管理
C 提供了四个常用接口:malloc、calloc、realloc 和 free。它们在 C++ 中仍可使用,只是不会自动处理对象的构造与析构。
| 接口 | 作用 | 初始内容 | 失败结果 |
|---|---|---|---|
malloc(bytes) |
申请指定字节数 | 未初始化 | 返回空指针 |
calloc(count, size) |
申请 count * size 字节 |
所有字节置零 | 返回空指针 |
realloc(ptr, bytes) |
调整已有内存块大小 | 新增部分内容未指定 | 返回空指针,原内存仍有效 |
free(ptr) |
释放动态内存 | 不适用 | 无返回值 |
1. malloc
malloc 接收字节数,成功时返回 void*。在 C++ 中,void* 不会自动转换成其他对象指针,因此需要显式转换:
cpp
#include <cstdlib>
int* values = static_cast<int*>(std::malloc(sizeof(int) * 4));
if (values == nullptr) {
// 申请失败
return;
}
std::free(values);
values = nullptr;
malloc 只提供原始空间,不会初始化其中的整数。
2. calloc
calloc 接收元素数量和单个元素大小,并将申请到的所有字节清零:
cpp
int* values = static_cast<int*>(std::calloc(4, sizeof(int)));
对于整数数组,清零后的结果通常就是四个 0。不过"所有字节为零"和"任意 C++ 对象都被正确初始化"不是同一个概念,不能用 calloc 代替类的构造函数。
3. realloc
realloc 可以扩大或缩小已有内存块。它可能在原地址调整,也可能申请一块新空间、复制旧内容并释放旧空间。
下面是常见的错误写法:
cpp
// 错误:失败时返回 nullptr,原地址会因为被覆盖而丢失
// values = static_cast<int*>(std::realloc(values, newSize));
更稳妥的方式是先保存返回值:
cpp
#include <cstdlib>
int* values = static_cast<int*>(std::calloc(4, sizeof(int)));
if (values == nullptr) {
return;
}
void* raw = std::realloc(values, sizeof(int) * 10);
if (raw == nullptr) {
std::free(values); // 调整失败,原内存仍然有效
return;
}
values = static_cast<int*>(raw);
std::free(values);
realloc 成功后,旧指针不能再使用。若返回地址与原地址不同,旧内存已经被内部释放;若失败,旧内存仍由调用者管理。因此不能在成功后再对旧指针调用一次 free。
三、new 和 delete 的基本用法
C++ 使用 new 和 delete 管理动态对象。它们会结合类型信息完成空间申请、对象初始化和对象清理。
1. 内置类型
cpp
int* p1 = new int; // 默认初始化,值不确定
int* p2 = new int(10); // 初始化为 10
int* p3 = new int(); // 值初始化为 0
delete p1;
delete p2;
delete p3;
动态数组使用 new[]:
cpp
int* values1 = new int[3]; // 元素值不确定
int* values2 = new int[3]{}; // 三个元素值初始化为 0
delete[] values1;
delete[] values2;
申请单个对象用 delete,申请数组用 delete[]。两组形式必须匹配。
new int[3]中的3是元素数量,不是字节数。编译器会根据类型自动计算所需空间。
2. 自定义类型
对类对象使用 new 时,构造函数会执行;使用 delete 时,析构函数会先执行。
cpp
#include <iostream>
class Resource {
public:
explicit Resource(int id = 0)
: _id(id) {
std::cout << "construct: " << _id << '\n';
}
~Resource() {
std::cout << "destroy: " << _id << '\n';
}
private:
int _id;
};
int main() {
Resource* object = new Resource(7);
delete object;
Resource* array = new Resource[2];
delete[] array;
}
编译运行:
bash
g++ resource.cpp -std=c++11 -o resource
./resource
预期输出中的地址和细节可能不同,构造与析构顺序应当是:
text
construct: 7
destroy: 7
construct: 0
construct: 0
destroy: 0
destroy: 0
数组中的对象按顺序构造,析构时通常按相反顺序执行。即使示例里的两个对象看不出差别,delete[] 仍然必须与 new[] 配对。
3. malloc 只给空间,new 才建立对象
cpp
Resource* raw = static_cast<Resource*>(std::malloc(sizeof(Resource)));
Resource* object = new Resource(7);
raw 指向一块足以容纳 Resource 的原始空间,但普通的 malloc 调用没有执行构造函数。对需要构造的类直接使用这块空间,会破坏对象生命周期规则。
object 指向的则是已经完成构造的 Resource 对象。释放时也不能混用:
cpp
std::free(raw);
delete object;
把 new 返回的指针交给 free,或者把 malloc 返回的指针交给普通 delete,都会产生未定义行为。
四、new 表达式与 operator new 不是一回事
new 是 C++ 表达式的一部分,operator new 是负责申请原始空间的函数。可以把创建单个对象的过程理解为两步:
#mermaid-svg-0RrBf8j7eWODJj3Y{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-0RrBf8j7eWODJj3Y .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-0RrBf8j7eWODJj3Y .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-0RrBf8j7eWODJj3Y .error-icon{fill:#552222;}#mermaid-svg-0RrBf8j7eWODJj3Y .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-0RrBf8j7eWODJj3Y .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-0RrBf8j7eWODJj3Y .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-0RrBf8j7eWODJj3Y .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-0RrBf8j7eWODJj3Y .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-0RrBf8j7eWODJj3Y .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-0RrBf8j7eWODJj3Y .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-0RrBf8j7eWODJj3Y .marker{fill:#333333;stroke:#333333;}#mermaid-svg-0RrBf8j7eWODJj3Y .marker.cross{stroke:#333333;}#mermaid-svg-0RrBf8j7eWODJj3Y svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-0RrBf8j7eWODJj3Y p{margin:0;}#mermaid-svg-0RrBf8j7eWODJj3Y .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-0RrBf8j7eWODJj3Y .cluster-label text{fill:#333;}#mermaid-svg-0RrBf8j7eWODJj3Y .cluster-label span{color:#333;}#mermaid-svg-0RrBf8j7eWODJj3Y .cluster-label span p{background-color:transparent;}#mermaid-svg-0RrBf8j7eWODJj3Y .label text,#mermaid-svg-0RrBf8j7eWODJj3Y span{fill:#333;color:#333;}#mermaid-svg-0RrBf8j7eWODJj3Y .node rect,#mermaid-svg-0RrBf8j7eWODJj3Y .node circle,#mermaid-svg-0RrBf8j7eWODJj3Y .node ellipse,#mermaid-svg-0RrBf8j7eWODJj3Y .node polygon,#mermaid-svg-0RrBf8j7eWODJj3Y .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-0RrBf8j7eWODJj3Y .rough-node .label text,#mermaid-svg-0RrBf8j7eWODJj3Y .node .label text,#mermaid-svg-0RrBf8j7eWODJj3Y .image-shape .label,#mermaid-svg-0RrBf8j7eWODJj3Y .icon-shape .label{text-anchor:middle;}#mermaid-svg-0RrBf8j7eWODJj3Y .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-0RrBf8j7eWODJj3Y .rough-node .label,#mermaid-svg-0RrBf8j7eWODJj3Y .node .label,#mermaid-svg-0RrBf8j7eWODJj3Y .image-shape .label,#mermaid-svg-0RrBf8j7eWODJj3Y .icon-shape .label{text-align:center;}#mermaid-svg-0RrBf8j7eWODJj3Y .node.clickable{cursor:pointer;}#mermaid-svg-0RrBf8j7eWODJj3Y .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-0RrBf8j7eWODJj3Y .arrowheadPath{fill:#333333;}#mermaid-svg-0RrBf8j7eWODJj3Y .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-0RrBf8j7eWODJj3Y .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-0RrBf8j7eWODJj3Y .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0RrBf8j7eWODJj3Y .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-0RrBf8j7eWODJj3Y .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0RrBf8j7eWODJj3Y .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-0RrBf8j7eWODJj3Y .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-0RrBf8j7eWODJj3Y .cluster text{fill:#333;}#mermaid-svg-0RrBf8j7eWODJj3Y .cluster span{color:#333;}#mermaid-svg-0RrBf8j7eWODJj3Y div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-0RrBf8j7eWODJj3Y .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-0RrBf8j7eWODJj3Y rect.text{fill:none;stroke-width:0;}#mermaid-svg-0RrBf8j7eWODJj3Y .icon-shape,#mermaid-svg-0RrBf8j7eWODJj3Y .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-0RrBf8j7eWODJj3Y .icon-shape p,#mermaid-svg-0RrBf8j7eWODJj3Y .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-0RrBf8j7eWODJj3Y .icon-shape .label rect,#mermaid-svg-0RrBf8j7eWODJj3Y .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-0RrBf8j7eWODJj3Y .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-0RrBf8j7eWODJj3Y .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-0RrBf8j7eWODJj3Y :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} new T(args)
调用 operator new 申请原始空间
在空间中调用 T 的构造函数
返回 T*
删除对象也分成两步:
#mermaid-svg-BYM7xQOOqcb6MP5L{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-BYM7xQOOqcb6MP5L .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-BYM7xQOOqcb6MP5L .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-BYM7xQOOqcb6MP5L .error-icon{fill:#552222;}#mermaid-svg-BYM7xQOOqcb6MP5L .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-BYM7xQOOqcb6MP5L .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-BYM7xQOOqcb6MP5L .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-BYM7xQOOqcb6MP5L .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-BYM7xQOOqcb6MP5L .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-BYM7xQOOqcb6MP5L .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-BYM7xQOOqcb6MP5L .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-BYM7xQOOqcb6MP5L .marker{fill:#333333;stroke:#333333;}#mermaid-svg-BYM7xQOOqcb6MP5L .marker.cross{stroke:#333333;}#mermaid-svg-BYM7xQOOqcb6MP5L svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-BYM7xQOOqcb6MP5L p{margin:0;}#mermaid-svg-BYM7xQOOqcb6MP5L .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-BYM7xQOOqcb6MP5L .cluster-label text{fill:#333;}#mermaid-svg-BYM7xQOOqcb6MP5L .cluster-label span{color:#333;}#mermaid-svg-BYM7xQOOqcb6MP5L .cluster-label span p{background-color:transparent;}#mermaid-svg-BYM7xQOOqcb6MP5L .label text,#mermaid-svg-BYM7xQOOqcb6MP5L span{fill:#333;color:#333;}#mermaid-svg-BYM7xQOOqcb6MP5L .node rect,#mermaid-svg-BYM7xQOOqcb6MP5L .node circle,#mermaid-svg-BYM7xQOOqcb6MP5L .node ellipse,#mermaid-svg-BYM7xQOOqcb6MP5L .node polygon,#mermaid-svg-BYM7xQOOqcb6MP5L .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-BYM7xQOOqcb6MP5L .rough-node .label text,#mermaid-svg-BYM7xQOOqcb6MP5L .node .label text,#mermaid-svg-BYM7xQOOqcb6MP5L .image-shape .label,#mermaid-svg-BYM7xQOOqcb6MP5L .icon-shape .label{text-anchor:middle;}#mermaid-svg-BYM7xQOOqcb6MP5L .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-BYM7xQOOqcb6MP5L .rough-node .label,#mermaid-svg-BYM7xQOOqcb6MP5L .node .label,#mermaid-svg-BYM7xQOOqcb6MP5L .image-shape .label,#mermaid-svg-BYM7xQOOqcb6MP5L .icon-shape .label{text-align:center;}#mermaid-svg-BYM7xQOOqcb6MP5L .node.clickable{cursor:pointer;}#mermaid-svg-BYM7xQOOqcb6MP5L .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-BYM7xQOOqcb6MP5L .arrowheadPath{fill:#333333;}#mermaid-svg-BYM7xQOOqcb6MP5L .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-BYM7xQOOqcb6MP5L .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-BYM7xQOOqcb6MP5L .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BYM7xQOOqcb6MP5L .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-BYM7xQOOqcb6MP5L .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BYM7xQOOqcb6MP5L .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-BYM7xQOOqcb6MP5L .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-BYM7xQOOqcb6MP5L .cluster text{fill:#333;}#mermaid-svg-BYM7xQOOqcb6MP5L .cluster span{color:#333;}#mermaid-svg-BYM7xQOOqcb6MP5L div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-BYM7xQOOqcb6MP5L .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-BYM7xQOOqcb6MP5L rect.text{fill:none;stroke-width:0;}#mermaid-svg-BYM7xQOOqcb6MP5L .icon-shape,#mermaid-svg-BYM7xQOOqcb6MP5L .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-BYM7xQOOqcb6MP5L .icon-shape p,#mermaid-svg-BYM7xQOOqcb6MP5L .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-BYM7xQOOqcb6MP5L .icon-shape .label rect,#mermaid-svg-BYM7xQOOqcb6MP5L .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-BYM7xQOOqcb6MP5L .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-BYM7xQOOqcb6MP5L .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-BYM7xQOOqcb6MP5L :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} delete pointer
调用 T 的析构函数
调用 operator delete 释放原始空间
1. operator new 的职责
全局 operator new 的接口形式类似:
cpp
void* operator new(std::size_t size);
void operator delete(void* pointer) noexcept;
operator new 接收字节数并返回一块满足要求的原始空间。普通抛异常版本申请失败时会抛出 std::bad_alloc,而 malloc 失败时返回空指针。
某些标准库实现会在 operator new 内部借助 malloc,但这属于实现细节。C++ 标准规定的是函数行为,没有要求它必须调用某个 C 接口。
2. 构造函数抛异常时怎么办
执行 new T(args) 时,如果原始空间申请成功,但 T 的构造函数抛出异常,与之匹配的 operator delete 会被调用,刚申请的空间会被归还。也就是说,普通 new 表达式会处理"空间已经拿到、对象却构造失败"的清理问题。
这也是不应该手动把 operator new 和构造函数随意拼接的原因。手工管理原始空间时,异常清理责任会落到调用者身上。
五、new\[\] 和 delete\[\] 的执行过程
创建对象数组时,可以把 new T[count] 理解为:
- 申请足以容纳多个对象的原始空间。
- 在这片空间中依次构造
count个对象。
delete[] 则会先逐个执行析构函数,再释放整块原始空间。
cpp
Resource* objects = new Resource[3];
delete[] objects;
运行库需要知道该销毁多少个对象,具体记录方式属于实现细节。代码层面只需要坚持一个规则:new[] 的结果必须交给 delete[]。
如果数组构造到一半时某个构造函数抛出异常,已经成功构造的元素会被析构,申请的空间也会被释放。尚未构造成功的元素没有析构过程。
六、定位 new:在已有空间中构造对象
定位 new,也叫 placement new,不负责重新申请空间。它只在给定地址上调用构造函数。
基本形式是:
cpp
new (address) Type(arguments);
需要包含头文件 <new>。下面使用全局 operator new 申请原始空间,再通过定位 new 构造对象:
cpp
#include <iostream>
#include <new>
class Widget {
public:
explicit Widget(int value)
: _value(value) {
std::cout << "construct: " << _value << '\n';
}
~Widget() {
std::cout << "destroy: " << _value << '\n';
}
private:
int _value;
};
int main() {
void* rawMemory = ::operator new(sizeof(Widget));
Widget* object = nullptr;
try {
object = new (rawMemory) Widget(42);
object->~Widget();
::operator delete(rawMemory);
} catch (...) {
::operator delete(rawMemory);
throw;
}
}
编译运行:
bash
g++ placement_new.cpp -std=c++11 -o placement_new
./placement_new
预期输出:
text
construct: 42
destroy: 42
这里的顺序不能乱:
- 先获得原始空间。
- 使用定位
new在该空间构造对象。 - 显式调用析构函数。
- 用与申请方式匹配的接口归还原始空间。
定位 new 创建的对象不能直接写 delete object,因为 delete 会把对象析构和空间释放绑定在一起,而这块空间可能来自内存池、共享缓冲区或其他自定义分配器。
定位 new 常见于内存池和容器底层实现。普通业务代码很少需要直接使用它,因为手动控制对象生命周期很容易漏掉异常路径。
七、malloc/free 与 new/delete 对比
| 对比项 | malloc/free |
new/delete |
|---|---|---|
| 语言属性 | C 标准库函数 | C++ 表达式与操作符 |
| 申请参数 | 字节数 | 类型和对象数量 |
| 返回类型 | void* |
对应类型的指针 |
| 是否执行构造函数 | 否 | 是 |
| 是否执行析构函数 | 否 | 是 |
| 普通失败方式 | 返回空指针 | 抛出 std::bad_alloc |
| 数组形式 | 手动计算总字节数 | new[] 与 delete[] |
| 能否调整已有内存块 | realloc 可以 |
没有直接对应形式 |
二者都可以获得动态空间,也都要求调用者负责释放。对 C++ 类对象而言,new/delete 能维护对象生命周期,语义更完整。对原始字节缓冲区或与 C 接口交互的场景,malloc/free 仍可能出现。
八、常见问题与易错点
1. new\[\] 和 delete 不匹配
cpp
int* values = new int[10];
// delete values; // 错误
delete[] values; // 正确
即使数组元素是内置类型,混用依然属于未定义行为。
2. malloc 和 delete 混用
内存由哪套接口申请,就必须交回同一套接口:
cpp
void* p1 = std::malloc(100);
std::free(p1);
int* p2 = new int(10);
delete p2;
3. 释放后继续使用
cpp
int* value = new int(10);
delete value;
value = nullptr;
把指针置空不能修复其他仍指向同一对象的指针,但能避免当前变量被误用。对空指针执行 delete 或 delete[] 是安全的。
4. 重复释放
同一块空间只能释放一次。两个指针保存同一地址时,其中一个完成释放,另一个也随即成为悬空指针。
5. 忘记释放造成内存泄漏
cpp
void Process() {
int* values = new int[1024];
// 提前 return 或抛异常时可能漏掉 delete[]
}
只要动态空间失去最后一个可用地址,程序就无法再释放它。循环或长期运行的服务中,少量泄漏也会不断累积。
6. 把"字节清零"当成"对象初始化"
calloc 和 memset 可以操作原始字节,但不能代替类的构造函数。一个类可能需要维护虚函数表、内部指针或其他不变量,简单清零无法建立合法对象。
7. 忽略 realloc 失败
不要直接用 realloc 的返回值覆盖唯一的旧指针。先用临时指针接收,成功后再更新原指针。
九、工程实践中的内存管理
直接使用 new/delete 有助于理解 C++ 对象模型,但在现代 C++ 业务代码里,通常会把资源交给能够自动清理的对象管理。
例如,动态数组可以优先使用 std::vector,独占动态对象可以使用 std::unique_ptr。它们遵循 RAII 思想:资源的获得与对象初始化绑定,资源释放与对象析构绑定。这样即使函数提前返回或抛出异常,清理逻辑也会自动执行。
cpp
#include <memory>
#include <vector>
std::vector<int> values(10, 0);
std::unique_ptr<Resource> resource(new Resource(7));
C++14 可以进一步写成:
cpp
auto resource = std::make_unique<Resource>(7);
理解裸指针内存管理仍然必要,因为容器、智能指针和内存池的底层都建立在这些规则上。实际开发时,则应尽量缩小手动释放的范围,把生命周期交给类型系统。
十、总结
分析内存位置时,要区分变量本身和它所指向的对象。局部指针可以位于栈中,它指向的动态对象位于堆中;字符数组保存一份可修改副本,指向字符串字面量的指针读取的是只读内容。
malloc/free 管理原始字节,new/delete 还会维护 C++ 对象的构造与析构。new 表达式通过 operator new 获得空间,再执行构造函数;delete 先析构对象,再通过 operator delete 归还空间。数组形式必须使用 new[]/delete[] 配对。
定位 new 把空间分配与对象构造拆开,适合内存池等底层场景,也把更多清理责任交给了调用者。掌握这些边界后,再去理解 RAII、智能指针和容器的资源管理会顺畅很多。