14、动态内存:new与delete、堆与栈、内存泄漏预防

动态内存管理,说白了就是让程序在运行时「按需分配」内存。你写代码时不知道用户要输入多少数据?不知道文件有多大?这时候静态数组就不好使了。动态内存就是用来解决这类问题的。

我刚开始学C++时,觉得这玩意儿挺酷的——想分配多少就分配多少,多自由。后来在项目中吃过几次亏,才明白自由是有代价的。嗯,今天咱们就把这块彻底讲透。

堆 vs 栈:两个不同的世界

先搞清楚这两个概念。很多新手搞混,其实它们差别很大。

对比项 栈(Stack) 堆(Heap)
分配方式 自动分配、自动释放 手动分配、手动释放
速度 极快(寄存器操作) 较慢(需要系统调用)
大小 通常几MB(有限) 取决于物理内存(较大)
生命周期 函数结束时自动销毁 直到手动释放或程序结束
典型用途 局部变量、函数参数 大对象、动态数组、跨函数数据

举个例子。你在函数里写 int a = 10;,这个 a 就在栈上。函数一结束,它自动消失。但如果你写 int* p = new int(10);,这个 p 本身在栈上(指针变量),但它指向的内存却在堆上。函数结束后,p 没了,但堆上的那个 10 还在——除非你手动 delete 它。

我在项目中遇到过一个问题:一个服务跑着跑着内存暴涨,查了半天发现是某个函数里 new 了对象,但忘记 delete。函数每次调用都泄漏一点,积少成多,最后把服务器搞崩了。这就是典型的堆内存管理失误。

new 和 delete:基本用法

先看最简单的用法:

// 分配单个对象
int* p = new int;      // 未初始化
int* q = new int(42);  // 初始化为42

// 使用
*p = 100;
cout << *q << endl;   // 输出42

// 释放
delete p;
delete q;

分配数组也一样:

// 分配数组
int* arr = new int[10];  // 10个int,未初始化

// 使用
for (int i = 0; i < 10; ++i) {
    arr[i] = i * 2;
}

// 释放数组(注意是中括号)
delete[] arr;
⚠️ 重要: new 配 delete,new[] 配 delete[]。混用会导致未定义行为。我见过有人用 delete 释放 new[] 出来的数组,结果程序崩溃得一塌糊涂。

内存泄漏:看不见的杀手

什么是内存泄漏?说白了就是:你向系统申请了一块内存,用完之后没还回去。系统以为你还在用,就一直留着。你不断申请、不归还,可用内存就越来越少。

为什么会泄漏?常见原因就这几个:

  • 忘记 delete——最常见,没有之一
  • 异常导致跳过 delete——函数中途抛异常,后面的 delete 没执行
  • 指针被覆盖——原来指向堆内存的指针被赋了新值,旧地址找不到了
  • 容器中的指针未释放——vector 里存了 raw pointer,clear 时只清指针本身,没释放指向的内存

我曾经维护过一个老项目,里面有个模块每次处理请求都会 new 一个对象,但只在正常路径下 delete。如果请求参数非法,函数提前 return 了,delete 就被跳过了。结果线上跑了三天,内存占用从 200MB 涨到 8GB,运维直接报警了。

预防内存泄漏的实用技巧

嗯,这里我总结几个实战中验证过的方法:

核心原则: 谁 new 谁 delete,谁申请谁释放。不要跨模块、跨函数传递所有权,除非你很清楚自己在做什么。

1. 使用 RAII 思想

RAII(资源获取即初始化)是 C++ 的精髓之一。说白了就是:把资源管理绑定到对象的生命周期上。对象构造时获取资源,析构时自动释放。

class IntArray {
private:
    int* data;
    size_t size;
public:
    IntArray(size_t n) : size(n) {
        data = new int[n];
    }
    ~IntArray() {
        delete[] data;  // 自动释放!
    }
    // ... 其他成员函数
};

// 使用:完全不用担心泄漏
void func() {
    IntArray arr(1000);  // 构造时分配
    // 使用 arr ...
    // 函数结束时,arr 析构,自动释放内存
}

2. 优先使用智能指针

C++11 之后,智能指针是首选。我个人习惯是:除非有特殊理由,否则不用 raw pointer 管理动态内存。

#include <memory>

// unique_ptr:独占所有权
std::unique_ptr<int> p1(new int(42));
// 不能拷贝,只能移动
std::unique_ptr<int> p2 = std::move(p1);

// shared_ptr:共享所有权(引用计数)
std::shared_ptr<int> s1 = std::make_shared<int>(100);
std::shared_ptr<int> s2 = s1;  // 引用计数+1
// 最后一个 shared_ptr 销毁时,自动 delete
💡 小技巧: 尽量用 std::make_uniquestd::make_shared,而不是直接 new。它们更安全、更高效,而且代码更简洁。

3. 养成检查习惯

如果你不得不使用 raw pointer,建议遵循这个流程:

  1. 写 new 的同时,立刻写对应的 delete
  2. 考虑所有可能的执行路径(包括异常路径)
  3. 代码审查时重点检查 new/delete 配对
  4. 用 Valgrind、AddressSanitizer 等工具定期检测

知识体系总览

下面这张图把本章的核心逻辑串起来了:

动态内存管理核心知识体系 堆 vs 栈 • 栈:自动管理,速度快,容量小 • 堆:手动管理,速度慢,容量大 • 栈存局部变量,堆存动态对象 • 生命周期不同,管理方式不同 new / delete • new 分配,delete 释放 • new[] 配 delete[] • 单个对象 vs 数组 • 必须配对使用 内存泄漏 • 忘记 delete • 异常跳过释放 • 指针被覆盖 • 容器未正确清理 预防措施(三大法宝) RAII 思想 构造获取,析构释放 智能指针 unique_ptr / shared_ptr 检查习惯 配对检查 + 工具检测 核心原则:谁申请谁释放,优先智能指针,RAII 是王道

一个完整的例子

最后,咱们看一个综合例子。假设你要写一个函数,读取文件中的整数并返回它们的平均值:

#include <iostream>
#include <fstream>
#include <memory>
#include <vector>

// 不好的写法:手动管理,容易泄漏
double badAverage(const std::string& filename) {
    std::ifstream file(filename);
    if (!file) return 0.0;
    
    int count = 0;
    int* data = new int[1000];  // 假设最多1000个数
    int sum = 0;
    
    while (file >> data[count] && count < 1000) {
        sum += data[count];
        ++count;
    }
    
    // 如果这里抛异常,data 就泄漏了
    double avg = (count == 0) ? 0.0 : static_cast<double>(sum) / count;
    
    delete[] data;  // 容易忘记
    return avg;
}

// 好的写法:用 vector 自动管理
double goodAverage(const std::string& filename) {
    std::ifstream file(filename);
    if (!file) return 0.0;
    
    std::vector<int> data;
    int value;
    int sum = 0;
    
    while (file >> value) {
        data.push_back(value);
        sum += value;
    }
    
    return data.empty() ? 0.0 : static_cast<double>(sum) / data.size();
    // vector 析构时自动释放内存,不用担心泄漏
}

你看,用 std::vector 替代 raw array,代码更短、更安全、更清晰。这就是 RAII 的力量。

💡 我的建议: 写 C++ 时,优先用标准库容器和智能指针。只有在性能极度敏感、或者需要与 C 代码交互时,才考虑手动管理内存。而且,每次写 new 的时候,问自己一句:能不能用智能指针替代?

动态内存管理是 C++ 的核心能力,也是最容易出坑的地方。记住三点:理解堆栈差异、养成 RAII 习惯、善用智能指针。做到这三点,内存泄漏基本就离你远去了。


公众号:蓝海资料掘金营,微信deep3321