10、动态内存管理:new与delete、动态数组、内存泄漏与避免

说到动态内存管理,很多初学者会觉得有点抽象。说白了,就是程序运行时,你主动跟操作系统要一块内存来用,用完了再还回去。C++里干这事儿的工具就是 newdelete。我刚开始学的时候,总觉得这玩意儿跟普通变量没啥区别,直到有一次写了个服务端程序,跑了三天三夜,内存暴涨到几个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,就像写 if 的时候立即写大括号一样。这样不容易漏掉。

动态数组

动态数组用 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,要么用智能指针。我个人强烈推荐后者。

如何避免内存泄漏?

嗯,这里要重点说说。避免泄漏有几种思路:

  1. 用智能指针std::unique_ptrstd::shared_ptr 会自动管理生命周期。这是现代 C++ 的首选方案。
  2. RAII 思想:资源获取即初始化。把内存管理封装到类里,构造时 new,析构时 delete。
  3. 成对编写:new 和 delete 写在一起,中间不要插太多逻辑。
  4. 使用工具检测:Valgrind、AddressSanitizer 等工具可以帮你找出泄漏点。

看个智能指针的例子:

#include <memory>

void func() {
    std::unique_ptr<int> p = std::make_unique<int>(42);
    // 不用手动 delete,离开作用域自动释放
    if (someCondition) {
        return;      // 没问题,智能指针会处理
    }
    // 这里也不用操心
}

说实话,自从 C++11 普及了智能指针,我写代码就很少直接 new/delete 了。除非是底层库或者性能极度敏感的场景,否则智能指针完全够用。

动态内存管理的核心逻辑

下面这张图帮你理清思路:

动态内存管理核心逻辑 静态分配(栈) 动态分配(堆) 编译时确定大小 new / new[] delete / delete[] 常见问题 • 内存泄漏(忘了 delete) • 野指针(delete 后继续用) • 重复释放(double free) 解决方案:智能指针 + RAII + 工具检测

避坑指南

最后,分享几个我亲身踩过的坑:

我曾经犯过的错:

  • delete 去释放 new[] 分配的内存。程序没崩,但 valgrind 报了一堆错。
  • 在构造函数里 new,析构函数里 delete,但忘了写拷贝构造函数。结果两个对象指向同一块内存,析构时 double free。
  • malloc 分配内存,然后用 delete 释放。C 和 C++ 的内存管理不能混用,切记。

嗯,动态内存管理就讲这么多。记住一句话:谁分配,谁释放。只要遵循这个原则,配合智能指针,大部分内存问题都能避免。

推荐工具: 如果你用的是 GCC/Clang,编译时加上 -fsanitize=address,运行时就能检测内存错误。我每次调试内存问题都靠它。

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