3. 动态内存管理:malloc/free与new/delete,内存泄漏的检测,悬空指针的产生

动态内存管理,说白了就是程序运行时跟操作系统“要内存”和“还内存”。

这事儿看着简单,但坑特别多。我见过太多线上事故,最后查下来都是内存管理出了问题。今天咱们就把这块掰开揉碎了讲清楚。

3.1 malloc/free 与 new/delete 的区别

很多新手觉得这两组东西差不多,其实差别大了去了。

对比项 malloc/free new/delete
语言 C 标准库函数 C++ 运算符
返回值 void*,需要强制类型转换 自动返回正确类型指针
调用构造函数 不会 会
调用析构函数 不会 会
失败处理 返回 NULL 抛出 std::bad_alloc 异常
内存大小 需要手动计算字节数 编译器自动计算

我个人习惯是:C++ 项目里只用 new/delete。为什么?因为 malloc 不会调用构造函数,你想想看,一个对象只分配了内存却没初始化,后面用起来全是未定义行为。

核心原则:malloc/free 和 new/delete 绝对不能混用。你用 new 分配的内存,必须用 delete 释放;用 malloc 分配的,必须用 free 释放。混着用,轻则内存泄漏,重则程序崩溃。

// 错误示范:混用
int* p = (int*)malloc(sizeof(int));
delete p;  // 未定义行为!

3.2 内存泄漏的检测

内存泄漏是什么?就是你申请了内存,用完了没还回去。一次两次没事,但程序跑久了,内存越占越多,最后系统扛不住就挂了。

我在项目中遇到过最离谱的一次:一个后台服务跑了三天,内存从 200MB 涨到了 8GB。查了两天才发现是一个循环里 new 了对象,但只在特定条件下才 delete。

3.2.1 手动检测方法

  • 代码审查:检查每个 new 是否有对应的 delete,每个 malloc 是否有对应的 free
  • 重载 new/delete:自己写一个内存分配计数器,记录分配和释放的次数
  • 智能指针:用 std::unique_ptr 或 std::shared_ptr 替代裸指针

小技巧:我写代码有个习惯——每次 new 完,立刻写对应的 delete。哪怕后面要改逻辑,delete 的位置也先占着。这叫“先还再借”,能避免很多遗漏。

3.2.2 工具检测

手动查太累了,咱们用工具。

工具 平台 特点
Valgrind Linux 功能强大,能检测内存泄漏、越界、未初始化变量
AddressSanitizer Linux/macOS 编译时插桩,速度快,适合集成到 CI
Dr. Memory Windows/Linux 类似 Valgrind,对 Windows 支持更好
Visual Studio 诊断工具 Windows 集成在 IDE 里,用起来方便
// 用 AddressSanitizer 检测内存泄漏
// 编译时加上 -fsanitize=address -g
// 运行程序,如果有泄漏,会打印出详细的调用栈

#include <stdlib.h>

void leak() {
    int* p = (int*)malloc(sizeof(int) * 100);
    // 忘记 free(p)
}

int main() {
    leak();
    return 0;
}
// 运行后输出:
// ==12345==ERROR: LeakSanitizer: detected memory leaks
// Direct leak of 400 byte(s) in 1 object(s) allocated from:
//     #0 0x7f... in malloc
//     #1 0x... in leak() /path/to/file.cpp:5

注意:AddressSanitizer 会显著增加内存占用(约 2-3 倍),不适合在生产环境开启。但开发和测试阶段一定要用。

3.3 悬空指针的产生

悬空指针,就是指针指向的内存已经被释放了,但指针本身还在用。这是 C/C++ 里最危险的 bug 之一。

为什么会这样?说白了就是“人走了,钥匙还在你手里”。你拿着钥匙去开门,发现门已经拆了——程序就崩了。

3.3.1 常见的悬空指针场景

  1. 释放后未置空:delete 之后指针还保存着原来的地址
  2. 函数返回局部变量地址:局部变量在函数返回后就被销毁了
  3. 多个指针指向同一块内存:一个指针释放了内存,其他指针不知道
  4. 容器元素被删除:迭代器或指针还指向已经被删除的元素
// 场景1:释放后未置空
int* p = new int(42);
delete p;
*p = 10;  // 悬空指针!未定义行为

// 场景2:返回局部变量地址
int* getLocal() {
    int x = 10;
    return &x;  // x 在函数返回后就不存在了
}
int* p = getLocal();
cout << *p;  // 悬空指针!

// 场景3:多个指针指向同一块内存
int* p1 = new int(100);
int* p2 = p1;
delete p1;
p1 = nullptr;
*p2 = 200;  // 悬空指针!p2 指向的内存已经被释放

3.3.2 如何避免悬空指针

  • 释放后立即置空:delete 之后马上把指针设为 nullptr
  • 使用智能指针:std::shared_ptr 和 std::weak_ptr 能自动管理生命周期
  • 明确所有权:谁分配谁释放,不要多个模块同时持有裸指针
  • RAII 思想:资源获取即初始化,让对象的析构函数自动释放资源

避坑指南:我曾经在一个多线程项目里吃过亏。线程 A 释放了内存,线程 B 还在用那个指针。查了两天才发现是竞态条件导致的悬空指针。后来我强制团队规定:所有跨线程传递的指针必须用 shared_ptr,谁都不准用裸指针。

3.4 知识体系总览

下面这张图把本章的核心逻辑串起来了。你看一眼就能明白:动态内存管理其实就三件事——怎么分配、怎么释放、怎么避免踩坑。

动态内存管理知识体系 分配方式 释放方式 常见问题 malloc new free delete 内存泄漏 悬空指针 检测与预防手段 Valgrind AddressSanitizer 智能指针 RAII 思想 核心原则:谁分配,谁释放;释放后,即置空

嗯,动态内存管理这块内容就这些。记住一句话:谁分配谁释放,释放后即置空。能做到这点的,基本不会出大问题。

总结:

  • malloc/free 是 C 的方式,new/delete 是 C++ 的方式,别混用
  • 内存泄漏用 Valgrind 或 AddressSanitizer 检测,省时省力
  • 悬空指针最危险,释放后置空 + 智能指针是王道
  • 多线程环境下,用 shared_ptr 替代裸指针传递所有权

可可精品资料库