C++ 内存管理:从内存分区到 new/delete 底层原理

C++ 内存管理:从内存分区到 new/delete 底层原理

写 C 或 C++ 程序时,我们经常会说某个变量"在栈上"或者"在堆上"。这句话看似简单,真正分析代码时却很容易混淆:指针变量可能在栈上,它指向的对象却在堆上;字符数组和字符串字面量内容相同,存储位置与可修改性并不相同;newoperator new 名字很像,负责的工作也不是一回事。

内存管理的问题通常不会在编译阶段暴露。申请与释放不匹配、对象析构没有执行、realloc 使用方式不当,都可能让程序在运行一段时间后才崩溃。下面从程序的内存区域开始,逐步讲到 malloc/freenew/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;
}

在常见实现中,leftrightresult 都位于当前函数的栈帧中。它们的生命周期受函数调用控制。

2. 堆

堆用于运行时动态内存分配。malloccallocreallocnew 申请的动态空间通常来自这里。空间不会因为申请它的函数结束而自动释放,必须按照对应规则归还。

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;
}

globalValuefileValuecallCount 的作用域不同,但都具有静态存储期。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 提供了四个常用接口:malloccallocreallocfree。它们在 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++ 使用 newdelete 管理动态对象。它们会结合类型信息完成空间申请、对象初始化和对象清理。

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] 理解为:

  1. 申请足以容纳多个对象的原始空间。
  2. 在这片空间中依次构造 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

这里的顺序不能乱:

  1. 先获得原始空间。
  2. 使用定位 new 在该空间构造对象。
  3. 显式调用析构函数。
  4. 用与申请方式匹配的接口归还原始空间。

定位 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;

把指针置空不能修复其他仍指向同一对象的指针,但能避免当前变量被误用。对空指针执行 deletedelete[] 是安全的。

4. 重复释放

同一块空间只能释放一次。两个指针保存同一地址时,其中一个完成释放,另一个也随即成为悬空指针。

5. 忘记释放造成内存泄漏

cpp 复制代码
void Process() {
    int* values = new int[1024];
    // 提前 return 或抛异常时可能漏掉 delete[]
}

只要动态空间失去最后一个可用地址,程序就无法再释放它。循环或长期运行的服务中,少量泄漏也会不断累积。

6. 把"字节清零"当成"对象初始化"

callocmemset 可以操作原始字节,但不能代替类的构造函数。一个类可能需要维护虚函数表、内部指针或其他不变量,简单清零无法建立合法对象。

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、智能指针和容器的资源管理会顺畅很多。

相关推荐
31535669131 小时前
Superpowers:GPT-5.6 时代下的流程毒瘤
前端·后端
wabs6661 小时前
关于单调栈【力扣739.每日温度的思考】
java·开发语言
AOwhisky1 小时前
Python 学习笔记(第三期)——流程控制核心知识点自测与详解
开发语言·笔记·python·学习·云原生·运维开发·流程控制
aaPIXa6222 小时前
C++ 用 13 条规则让模型写出安全现代 C++
java·c++·安全
逆向编程2 小时前
Socket异常连接自动清理方案
开发语言
神奇小汤圆3 小时前
2026 Java岗面试题库(已收录GitHub),覆盖所有考点
后端
ArixBit3 小时前
用 Go 手撸 Agent 框架 #4:工具定义与契约——让 Agent 学会调用多工具
后端
枕星而眠3 小时前
【数据结构】红黑树入门指南
运维·数据结构·c++·后端
神奇小汤圆3 小时前
JDK 8 → 17 → 21 → 25:一次性讲清四代版本的关键跃迁
后端