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;
内存泄漏:看不见的杀手
什么是内存泄漏?说白了就是:你向系统申请了一块内存,用完之后没还回去。系统以为你还在用,就一直留着。你不断申请、不归还,可用内存就越来越少。
为什么会泄漏?常见原因就这几个:
- 忘记 delete——最常见,没有之一
- 异常导致跳过 delete——函数中途抛异常,后面的 delete 没执行
- 指针被覆盖——原来指向堆内存的指针被赋了新值,旧地址找不到了
- 容器中的指针未释放——vector 里存了 raw pointer,clear 时只清指针本身,没释放指向的内存
我曾经维护过一个老项目,里面有个模块每次处理请求都会 new 一个对象,但只在正常路径下 delete。如果请求参数非法,函数提前 return 了,delete 就被跳过了。结果线上跑了三天,内存占用从 200MB 涨到 8GB,运维直接报警了。
预防内存泄漏的实用技巧
嗯,这里我总结几个实战中验证过的方法:
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_unique 和 std::make_shared,而不是直接 new。它们更安全、更高效,而且代码更简洁。
3. 养成检查习惯
如果你不得不使用 raw pointer,建议遵循这个流程:
- 写 new 的同时,立刻写对应的 delete
- 考虑所有可能的执行路径(包括异常路径)
- 代码审查时重点检查 new/delete 配对
- 用 Valgrind、AddressSanitizer 等工具定期检测
知识体系总览
下面这张图把本章的核心逻辑串起来了:
一个完整的例子
最后,咱们看一个综合例子。假设你要写一个函数,读取文件中的整数并返回它们的平均值:
#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++ 的核心能力,也是最容易出坑的地方。记住三点:理解堆栈差异、养成 RAII 习惯、善用智能指针。做到这三点,内存泄漏基本就离你远去了。
公众号:蓝海资料掘金营,微信deep3321