C/C++ 内存管理:从 malloc 到 new——掌控每一块内存
C/C++ 内存管理:从 malloc 到 new——掌控每一块内存
内存泄漏不是 bug,是失控。而失控,是绝不能容忍的。
为什么需要 C++ 的内存管理方式
C 程序员对 malloc 和 free 再熟悉不过——申请一块堆内存,用完释放,干净利落。这套机制在处理 int、char、struct 时完全够用。
直到遇到类。
malloc 只管划出一块原始内存。它不知道什么叫构造函数,也不会在释放时帮你调用析构函数。对 class 类型的对象,这意味着什么?用 malloc 创建的对象内部状态完全未初始化,指针成员指向垃圾;用 free 释放时,它持有的资源悄无声息地泄漏了。
C++ 引入 new 和 delete 就是为了解决这个缺口:把内存分配和对象生命周期绑定在一起。new 不仅分配空间,还执行构造函数;delete 不仅释放空间,还先调用析构函数清理资源。这不是"更方便"的问题——这是确定性的问题。
在深入 new/delete 之前,先看清楚程序内存的全景图。
C/C++ 内存分布全景
一个运行中的 C/C++ 程序,其虚拟地址空间大致分为以下几个区域:
| 内存区域 | 存储内容 | 特点 |
|---|---|---|
| 栈(Stack) | 非静态局部变量、函数参数、返回值 | 向下增长,编译器自动管理,生命周期随作用域 |
| 堆(Heap) | 动态分配的内存(malloc/new) | 向上增长,程序员手动管理 |
| 数据段(Data Segment) | 全局变量、静态变量 | 编译期确定,程序整个生命周期存活 |
| 代码段(Code Segment) | 可执行代码、只读常量(如字符串字面量) | 只读,固定大小 |
💡 背景补充:C 语言中
static修饰的局部变量不随函数返回而销毁——因为它实际存放在数据段,而非栈上。它的初始化只发生一次,之后的值跨越多次函数调用保持。
看一段经典的内存分布判断题:
int globalVar = 1;
static int staticGlobalVar = 1;
void Test()
{
static int staticVar = 1;
int localVar = 1;
int num1[10] = { 1, 2, 3, 4 };
char char2[] = "abcd"; // 栈上的数组,内容从代码段拷贝而来
const char* pChar3 = "abcd"; // 指针在栈上,指向代码段的字面量
int* ptr1 = (int*)malloc(sizeof(int) * 4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
free(ptr1);
free(ptr3);
}
各变量归属逐一定位:
| 变量 | 所在区域 | 说明 |
|---|---|---|
globalVar |
数据段 | 全局变量 |
staticGlobalVar |
数据段 | 静态全局变量 |
staticVar |
数据段 | 静态局部变量——虽在函数内,生命周期却是全局的 |
localVar |
栈 | 普通局部变量 |
num1 |
栈 | 数组整体分配在栈上 |
char2 |
栈 | 数组名是栈上的地址 |
*char2(首元素) |
栈 | 数组内容在栈上 |
pChar3 |
栈 | 指针变量本身 |
*pChar3(指向内容) |
代码段 | 字符串字面量 "abcd" 是只读常量 |
ptr1 |
栈 | 指针变量本身在栈上 |
*ptr1(指向内容) |
堆 | malloc 分配的内存在堆上 |
一个容易混淆的点:char2 是数组,其内容(含 \0)全部在栈上;pChar3 指向的 "abcd" 在代码段中,是只读的。试图通过 pChar3[0] = 'X' 修改它——未定义行为。不要这样做。
C 语言动态内存管理回顾
malloc、calloc、realloc、free 是 C 标准库提供的四个内存管理函数:
| 函数 | 行为 | 初始化 |
|---|---|---|
malloc(size) |
分配 size 字节连续空间 |
不初始化(残留垃圾值) |
calloc(n, size) |
分配 n * size 字节,即 n 个元素 |
初始化为 0 |
realloc(ptr, new_size) |
调整已分配空间大小 | 保留原有内容,新增部分未初始化 |
free(ptr) |
释放之前分配的空间 | — |
一个 realloc 面试题:
int* p2 = (int*)calloc(4, sizeof(int));
int* p3 = (int*)realloc(p2, sizeof(int) * 10);
// 这里需要 free(p2) 吗?
free(p3);
答案:不需要。realloc 扩容成功时会自动回收原内存(即使原地扩展,逻辑上旧内存也已失效),p2 此时是悬垂指针。只需 free(p3)。若 realloc 失败返回 NULL,原内存仍有效,此时仍需 free(p2)——但这是另一个分支了。
C 内存管理最大的问题不在函数本身,而在使用者。以下是野指针的三大成因:
// 1. 指针未初始化——指向随机地址
char* p; // 野指针!值不可预测
char* p = nullptr; // 正确做法
// 2. free/delete 后未置空——仍指向已释放内存
char* p = (char*)malloc(100);
free(p);
// p 仍存着那个地址,但那里已是垃圾
p = nullptr; // 必须置空——这是我的规则
// 3. 指针超出变量作用域——指向已销毁对象
int* p;
{
int a = 42;
p = &a; // a 的生命期仅在这个大括号内
}
// *p 是未定义行为——a 已经不存在了
⚠️ 野指针不是 NULL 指针。
if (p != NULL)对它毫无意义——它指向某个地址,只是那个地址的内容已经失控。这种隐藏的失败,是内存 bug 中最难追踪的一类。
C++ new/delete 机制
内置类型
对内置类型,new/delete 和 malloc/free 行为几乎一致——失败处理方式和语法除外:
#include <iostream>
int main()
{
// 申请单个 int
int* ptr4 = new int; // 未初始化
// 申请单个 int 并初始化为 10
int* ptr5 = new int(10); // 初始化为 10
// 申请 10 个 int 的连续空间
int* ptr6 = new int[3]; // 未初始化
delete ptr4;
delete ptr5;
delete[] ptr6; // 数组释放必须用 delete[]
}
⚠️
new配delete,new[]配delete[]——必须严格匹配。混用是未定义行为。这条规则没有例外。
自定义类型——这才是关键
对自定义类型,new/delete 与 malloc/free 的差异是本质性的:
#include <iostream>
class A
{
public:
A(int a = 0) : _a(a)
{
std::cout << "A():" << this << std::endl;
}
~A()
{
std::cout << "~A():" << this << std::endl;
}
private:
int _a;
};
int main()
{
// malloc/free —— 不开辟对象,只开辟内存
A* p1 = (A*)malloc(sizeof(A)); // 没有输出 A()
free(p1); // 没有输出 ~A()
// new/delete —— 开辟内存 + 构造对象 + 析构对象
A* p2 = new A(1); // 输出 A():地址
delete p2; // 输出 ~A():地址
// 数组版本——差异更明显
A* p3 = (A*)malloc(sizeof(A) * 10); // 静默——10 个未初始化的内存块
free(p3); // 静默——资源泄漏,如果有的话
A* p4 = new A[10]; // 输出 10 行 A():地址
delete[] p4; // 输出 10 行 ~A():地址
return 0;
}
malloc 和 free 对构造/析构函数一无所知。它们只关心字节数。用 malloc 创建了一个持有文件句柄、网络连接、动态内存的对象——内部状态未定义,free 直接丢弃它,所有资源就此泄漏。它跑着跑着就会崩。
new 和 delete 做的是完整的生命周期管理:构造 → 使用 → 析构 → 释放。四步,缺一不可。
operator new 与 operator delete——底层机制
new 和 delete 是操作符,不是函数。但它们底层依赖两个系统提供的全局函数:operator new 和 operator delete。
// operator new —— 底层是 malloc,但增加了异常处理
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// 申请失败 → 抛出 bad_alloc 异常,而非返回 NULL
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return p;
}
// operator delete —— 底层是 free
void operator delete(void *pUserData)
{
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK); // 线程安全——加锁
_free_dbg(pUserData, pHead->nBlockUse);
_munlock(_HEAP_LOCK); // 解锁
}
关键信息很清楚:operator new 内部调用 malloc,operator delete 内部调用 free。也就是说:
new= operator new(分配内存) + 构造函数(初始化对象)delete= 析构函数(清理资源) + operator delete(释放内存)
💡 背景补充:
malloc失败时返回NULL,调用者必须判空;operator new失败时抛出std::bad_alloc异常。返回码 vs 异常——两种不同的错误处理哲学。C++ 选择了异常,因为构造失败而返回 NULL 意味着拿到一个"不存在的对象",后续任何操作都是灾难。
new/delete 实现原理——完整调用链
内置类型
new int→ 调用operator new→malloc申请空间。失败则抛异常。delete p→ 调用operator delete→free释放空间。
基本就是 malloc/free 的封装,唯一区别是失败处理。
自定义类型
new T 的两步:
- 调用
operator new(sizeof(T))申请原始空间 - 在申请到的空间上执行构造函数
delete p 的两步:
- 在 p 指向的空间上执行析构函数,清理对象持有的资源
- 调用
operator delete(p)释放原始空间
new T[N] 的两步:
- 调用
operator new[]申请 N 个对象的连续空间(内部仍是调operator new) - 在空间上依次执行 N 次构造函数
delete[] p 的两步:
- 在空间上依次执行 N 次析构函数
- 调用
operator delete[]释放空间(内部调operator delete)
一条铁律:
A* p = new A[10];
delete[] p; // ✅ 10 次析构 + 完整释放
delete p; // ❌ 只析构 p[0],其余 9 个对象资源泄漏
编译器在 new A[10] 时可能额外记录数组长度(用于析构时确定调用次数)。delete 不读这个记录,只释放了第一个对象大小的空间。结果是未定义行为——而且是那种在简单测试中碰巧能跑、生产环境才爆的未定义行为。
placement new——在指定地址构造对象
有一种场景:已经有一块原始内存(malloc 出来的,或内存池分配的),想在它上面构造一个对象。
直接强转指针不行——强转只是类型系统层面的操作,构造函数没有被调用。这块内存还不是一个"对象"。
定位 new 表达式(placement new) 就是为此设计的:
#include <iostream>
#include <new> // placement new 需要此头文件
class A
{
public:
A(int a = 0) : _a(a)
{
std::cout << "A():" << this << std::endl;
}
~A()
{
std::cout << "~A():" << this << std::endl;
}
private:
int _a;
};
int main()
{
// p1 只是一块大小够放 A 的内存,还不是一个对象
A* p1 = (A*)malloc(sizeof(A));
new(p1) A; // placement new —— 在这块内存上调用构造函数
p1->~A(); // 手动调用析构函数
free(p1); // 手动释放
// operator new 版本(与 malloc 类似,但失败抛异常)
A* p2 = (A*)operator new(sizeof(A));
new(p2) A(10); // placement new 带参数
p2->~A();
operator delete(p2);
return 0;
}
语法:new (地址指针) 类型(初始化参数)。注意没有对应的 “placement delete”——析构函数需要手动调用,内存需要手动释放。
placement new 主要用在内配池场景:池子分配出未初始化的内存块,对象在块上就地构造,避免了 new 隐含的 malloc 开销。
malloc/free vs new/delete:完整对比
| 维度 | malloc/free | new/delete |
|---|---|---|
| 本质 | C 库函数 | C++ 操作符 |
| 返回类型 | void*,需强制转型 |
类型安全,自动推导 |
| 空间大小 | 手动计算字节数 sizeof(T) |
编译器根据类型自动计算 |
| 初始化 | 不初始化(calloc 除外) | 支持原地初始化 new T(val) |
| 构造/析构 | 不调用 | 自动调用 |
| 失败处理 | 返回 NULL,需手动判空 |
抛出 std::bad_alloc 异常 |
| 可否重载 | 不可 | 可重载 operator new/delete |
| 配对释放 | free(ptr) |
delete(单个) / delete[](数组) |
最后一条再强调一遍:malloc/free 和 new/delete 不能混用。malloc 分配的内存绝不能用 delete 释放,new 分配的内存绝不能用 free 释放。交叉使用是未定义行为——它能编过,能跑,然后在某个不确定的时刻崩溃。
本节要点
- C/C++ 内存分四区:栈(局部变量)、堆(动态分配)、数据段(全局/静态变量)、代码段(常量/指令)
new= 分配 + 构造,delete= 析构 + 释放——这是与malloc/free的本质区别- 自定义类型必须用
new/delete,malloc/free不会调用构造和析构函数 new[]和delete[]必须配对,混用导致未定义行为- 野指针是内存 bug 的头号来源——
free后立即置nullptr,不是建议,是规则 operator new底层仍是malloc,但失败时抛bad_alloc异常而非返回 NULL- placement new 在已有原始内存上构造对象,用于内存池等高性能场景
📖 参考:《高质量C++/C编程指南》第7章 —— 内存管理;《Effective C++》条款49-52 —— 定制 new 和 delete
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐

所有评论(0)