10、动态内存管理:new与delete、动态数组、内存泄漏与避免
说到动态内存管理,很多初学者会觉得有点抽象。说白了,就是程序运行时,你主动跟操作系统要一块内存来用,用完了再还回去。C++里干这事儿的工具就是 new 和 delete。我刚开始学的时候,总觉得这玩意儿跟普通变量没啥区别,直到有一次写了个服务端程序,跑了三天三夜,内存暴涨到几个G……嗯,从那以后我再也不敢小看内存管理了。
为什么需要动态内存?
你想想看,写代码的时候,很多时候你根本不知道程序运行时会需要多少内存。比如一个聊天软件,用户发多少条消息?存多少个联系人?这些在编译时都是未知的。动态内存就是用来解决这个问题的——运行时按需分配。
静态分配(比如 int arr[100])是在栈上,大小固定,函数结束自动释放。动态分配是在堆上,大小灵活,但必须手动释放。我个人的习惯是:能用栈就别用堆,除非你真的需要动态大小或者对象生命周期需要跨函数。
new 和 delete 的基本用法
先看个最简单的例子:
int* p = new int; // 分配一个 int 大小的内存
*p = 42;
cout << *p << endl;
delete p; // 释放内存
这里 new int 返回一个指针,指向堆上的一块内存。用完之后必须 delete,否则内存就泄漏了。我见过不少新手写代码,new 完了就忘了 delete,程序跑着跑着内存就炸了。
还可以在 new 的时候直接初始化:
int* p = new int(100); // 分配并初始化为 100
double* d = new double(3.14);
string* s = new string("hello");
动态数组
动态数组用 new[] 和 delete[] 来管理。注意,这里的方括号不能省:
int n = 10;
int* arr = new int[n]; // 分配 n 个 int 的数组
for (int i = 0; i < n; ++i) {
arr[i] = i * 2;
}
delete[] arr; // 必须用 delete[],不能用 delete
这里有个坑,我踩过不止一次:new[] 必须配 delete[],new 必须配 delete。混用的话,行为是未定义的。说白了就是程序可能崩溃,也可能不崩溃,但内存管理已经乱套了。
动态数组也可以初始化:
int* arr = new int[5]{1, 2, 3, 4, 5}; // C++11 起支持
内存泄漏——看不见的杀手
内存泄漏是什么?就是你 new 了一块内存,但忘了 delete。这块内存就永远无法被回收了,直到程序退出。如果程序长期运行,泄漏的内存越积越多,最终会耗尽系统资源。
我曾经维护过一个后台服务,每处理一次请求就泄漏 100 字节。刚开始没事,跑了两个月后,内存占用从 50MB 涨到了 2GB,最后系统 OOM 把进程杀了。排查起来特别痛苦,因为泄漏点藏得很深。
常见的泄漏场景
我总结了几种最容易踩的坑:
- 提前 return 忘了 delete:函数中间 return 了,后面的 delete 没执行到。
- 异常导致跳过 delete:new 之后抛了异常,delete 没机会执行。
- 指针被覆盖:给指针重新赋值,原来的内存地址丢了,再也找不回来。
- 容器里的指针没释放:vector 里存了 raw pointer,clear 的时候只清掉了指针本身,没释放指向的内存。
看个典型的错误例子:
void func() {
int* p = new int(42);
if (someCondition) {
return; // 泄漏了!delete 没执行
}
delete p;
}
正确的做法是:要么保证每个出口都有 delete,要么用智能指针。我个人强烈推荐后者。
如何避免内存泄漏?
嗯,这里要重点说说。避免泄漏有几种思路:
- 用智能指针:
std::unique_ptr和std::shared_ptr会自动管理生命周期。这是现代 C++ 的首选方案。 - RAII 思想:资源获取即初始化。把内存管理封装到类里,构造时 new,析构时 delete。
- 成对编写:new 和 delete 写在一起,中间不要插太多逻辑。
- 使用工具检测:Valgrind、AddressSanitizer 等工具可以帮你找出泄漏点。
看个智能指针的例子:
#include <memory>
void func() {
std::unique_ptr<int> p = std::make_unique<int>(42);
// 不用手动 delete,离开作用域自动释放
if (someCondition) {
return; // 没问题,智能指针会处理
}
// 这里也不用操心
}
说实话,自从 C++11 普及了智能指针,我写代码就很少直接 new/delete 了。除非是底层库或者性能极度敏感的场景,否则智能指针完全够用。
动态内存管理的核心逻辑
下面这张图帮你理清思路:
避坑指南
最后,分享几个我亲身踩过的坑:
我曾经犯过的错:
- 用
delete去释放new[]分配的内存。程序没崩,但 valgrind 报了一堆错。 - 在构造函数里 new,析构函数里 delete,但忘了写拷贝构造函数。结果两个对象指向同一块内存,析构时 double free。
- 用
malloc分配内存,然后用delete释放。C 和 C++ 的内存管理不能混用,切记。
嗯,动态内存管理就讲这么多。记住一句话:谁分配,谁释放。只要遵循这个原则,配合智能指针,大部分内存问题都能避免。
-fsanitize=address,运行时就能检测内存错误。我每次调试内存问题都靠它。