动态内存管理

C++ 允许通过指针以及动态内存分配/释放运算符进行底层内存操作。

new 与 delete 运算符

C++ 中的 new 与 delete 运算符:动态内存管理

在 C++ 中,栈内存会在编译时自动分配给变量,大小固定。若想获得更大的灵活性与控制权,可使用 堆(heap)上的动态内存分配:用 new 手动申请,用 delete 手动释放。

这样,程序就能在运行时向系统请求内存,适用于编译时尚不知大小的场景,例如变长数组、链表、树等动态数据结构。

1.new 运算符

new自由存储区(Free Store,堆的一部分) 分配内存。若内存充足,它会按类型默认值初始化该内存,并返回其地址。

示例:

#include <iostream>
#include <memory>
using namespace std;

int main() {
    // 声明指针,用于保存申请到的内存地址
    int *nptr;

    // 申请并初始化内存
    nptr = new int(6);

    // 打印值
    cout << *nptr << endl;  // 输出 6

    // 打印内存块地址
    cout << nptr;           // 输出 0xb52dc20(示例地址)
    return 0;
}

2.分配内存块(数组)

new 运算符也可用来动态分配一整块内存(即数组),语法如下:

new 数据类型[元素个数];

该语句会在堆上为指定类型分配可容纳 n 个元素的连续空间。
分配时还可以直接初始化数组。

示例:

#include <iostream>
#include <memory>
using namespace std;

int main() {
    // 声明指针,用于保存申请到的内存地址
    int *nptr;

    // 分配并初始化一个包含 5 个整数的数组
    nptr = new int[5]{1, 2, 3, 4, 5};

    // 打印数组
    for (int i = 0; i < 5; i++)
        cout << nptr[i] << " ";  // 输出:1 2 3 4 5
    return 0;
}
1 2 3 4 5 

3.如果在运行时没有足够的内存怎么办?

如果运行时堆空间不足,无法分配内存,new抛出 std::bad_alloc 类型的异常来表示分配失败。
不过,若给 new 加上 nothrow 参数,它就不会再抛异常,而是返回空指针 nullptr。因此,使用前最好先检查返回的指针是否有效。

int *p = new (nothrow) int;
if (!p) {
    cout << "内存分配失败n";
}

4.delete 运算符

在 C++ 中,delete 运算符用于释放new 动态分配的内存。
它把先前 new 申请到的内存归还给系统。

语法:

delete 指针;          // 释放单个对象
delete[] 数组指针;     // 释放数组
  • 指针:指向动态分配的内存
  • 数组指针:指向动态分配的数组

示例:

#include <iostream>
using namespace std;

int main() {
    int *ptr = NULL;

    // 用 new 申请一个整数的内存
    ptr = new int(10);
    if (!ptr) {
        cout << "内存分配失败";
        exit(0);
    }

    cout << "*p 的值: " << *ptr << endl;

    // 使用完后释放
    delete ptr;

    // 再申请一个包含 3 个元素的数组
    ptr = new int[3];
    ptr[2] = 11;
    ptr[1] = 22;
    ptr[0] = 33;
    cout << "数组: ";
    for (int i = 0; i < 3; i++)
        cout << ptr[i] << " ";

    // 使用完后释放数组
    delete[] ptr;

    return 0;
}
*p 的值: 10
数组: 33 22 11

5.动态内存常见错误

尽管动态内存分配功能强大,它也是 C++ 中最容易引入严重问题的环节之一。主要错误包括:

(1). 内存泄漏(Memory Leaks)
分配的内存使用完后未被释放,且若指向该内存的指针丢失,则这块内存会一直占用到程序结束。
解决方法:尽量使用智能指针(std::unique_ptr / std::shared_ptr),离开作用域时自动释放。

(2). 悬垂指针(Dangling Pointers)
内存已被 delete,但指针仍被继续使用,导致未定义行为(崩溃、脏数据等)。
解决方法:指针定义时初始化为 nullptr,释放后立刻再赋 nullptr

(3). 重复释放(Double Deletion)
对同一块内存执行两次 delete,程序可能直接崩溃或内存损坏。
解决方法:释放后立即将指针置为 nullptr

(4). 混用 new/delete 与 malloc/free
C++ 兼容 C 的 malloc()/calloc()/free(),但它们与 new/delete 不能混用
new 申请的内存不可用 free() 释放;用 malloc() 申请的内存不可用 delete 释放。

区别一览表:

特性new / deletemalloc / calloc / free
语言层级C++ 运算符C 库函数
是否调用构造函数/析构函数 会调用 不会
返回类型具体类型指针(如 int*void*(需手动强转)
内存大小计算自动计算(如 new int[5]手动计算(如 malloc(5 * sizeof(int))
失败时行为抛出 std::bad_alloc(或 nullptr 若用 nothrow返回 nullptr
是否支持重载 可以全局或类作用域重载 不支持
是否支持数组构造 new Type[n] 会调用 n 次构造函数 只分配原始内存
是否支持初始化 可用初始化列表(如 new int{5} malloc 不初始化,calloc 清零
是否可混用 绝对禁止 混用(newfreemallocdelete
struct Foo {
    Foo()  { std::cout << "构造n"; }
    ~Foo() { std::cout << "析构n"; }
};

Foo* p1 = new Foo;      // 输出:构造
delete p1;              // 输出:析构

Foo* p2 = (Foo*)malloc(sizeof(Foo)); // 无输出,不会构造
free(p2);                            // 无输出,不会析构

(5). 定位 new(Placement new)
普通 new先分配内存再构造对象;而 placement new 把这两步分开:
程序员先准备好一块已存在的内存块,再用 placement new 在该 指定地址 上构造对象。

内存泄漏

C++ 中的内存泄漏,内存泄漏是指:程序为某项任务动态分配了内存,但在使用完毕后没有释放,导致这块内存直到程序结束都无法再被使用,从而造成内存浪费

1.为什么 C++ 会出现内存泄漏?

C++ 没有自动垃圾回收机制。 所有用 new/malloc 等手动申请的内存,都必须由程序员显式释放delete/free)。 一旦忘记释放,这块内存就永远丢失——程序运行期间无法再被其他代码使用,这就是内存泄漏

示例:

#include <stdlib.h>

void f() {
    // 申请内存
    int* ptr = new int[10];

    // 函数直接返回,没有 delete[] ptr
    return;
}

int main() {
    // 执行一些任务
}

结果
为 10 个整数分配的内存既没有被释放,也无法再被访问,造成内存泄漏。

2.内存泄漏的后果

当发生内存泄漏时,会引发一系列问题:

(1). 性能下降
泄漏的内存无法再被程序其他部分或系统其他进程使用。若泄漏量大、时间长,程序甚至整个系统都会因可用内存不足而变慢。

(2). 程序崩溃
如果程序持续泄漏,最终可能耗尽所有物理内存,导致进程不稳定、行为异常或直接崩溃。

(3). 资源枯竭
内存是有限的系统资源,长期泄漏会造成资源枯竭,影响同一台机器上的所有任务。

(4). 长生命周期程序受害最深
短时间运行的小程序泄漏一点内存影响有限;但服务器、守护进程等长时间运行的程序,泄漏的内存会累积并长期占用,最终成为致命问题。

3.如何发现内存泄漏?

C++ 没有自动内存管理,查找泄漏相对困难。主要有两类方法:

  1. 人工审查
    通读代码,找出所有 new/malloc未配对 delete/free 的地方。

  2. 借助工具
    使用 Valgrind、AddressSanitizer、Dr.Memory 等工具,可自动定位泄漏点,无需逐行审计。

C++ 如何检测内存泄漏

内存泄漏是 C++ 开发中最常见、也最致命的问题之一:用 new/malloc 从堆申请了内存,却忘了用 delete/free 归还,导致资源耗尽、性能下降甚至程序崩溃。本文介绍如何系统化地检测这类泄漏。

泄漏根因先理清, 在动手排查前,先回顾导致泄漏的典型原因:

(1). 只 new 不 delete 申请后压根儿没有释放。

(2). 配对的形式写错delete 释放 new[] 申请的数组,或用 free 释放 new 的对象。

(3). 指针丢失 指针被覆盖或提前出作用域,再也找不到那块内存。

(4). 异常路径遗漏 try/catch 后忘记在所有分支里释放资源。

(5). 智能指针误用 shared_ptr 形成循环引用,导致引用计数永远降不到 0。

检测手段概览

方法说明推荐工具
静态检查编译期规则扫描Clang Static Analyzer、Cppcheck
运行时插桩程序跑起来后跟踪每一次申请/释放Valgrind、AddressSanitizer (ASan)、Dr.Memory
重载全局 new/delete自己记账:在全局运算符里记录调用栈、大小、是否配对适合单元测试集成
CRT 调试堆(Windows)_CrtDumpMemoryLeaks() 输出泄漏清单Visual Studio 调试器
智能指针审计weak_ptr 打破循环引用;开启 shared_ptr 自定义 deleter 日志结合代码审查

1.快速上手:AddressSanitizer(Linux / macOS)

g++ -fsanitize=address -g leak.cpp -o leak
./leak

运行结束即给出精确行号泄漏大小申请栈回溯,无需改动源码。

2.快速上手:Valgrind

valgrind --leak-check=full ./your_program

会报告:

  • 绝对泄漏(definitely lost)
  • 间接泄漏(indirectly lost)
  • 可能泄漏(possibly lost)

C++ 内存泄漏检测工具一览

以下工具均可用于定位 C++ 程序中的内存泄漏:

  1. Valgrind
    开源、跨平台的内存调试与性能分析利器。可检测泄漏、越界访问、使用已释放内存等问题,并给出详细源码级报告(泄漏大小、申请栈、调用链)。

  2. AddressSanitizer(ASan)
    现代 GCC/Clang 内置的快速内存错误检测器。
    无需额外安装,只要编译器版本够新,加 -fsanitize=address 即可在运行时捕获泄漏及其栈回溯。

  3. LeakSanitizer(LSan)
    专注于“仅泄漏”场景的编译器工具,通常与 ASan 一起工作。
    GCC 4.9+ 默认集成,加 -fsanitize=leak 即可启用,报告简洁、定位精准。

  4. Visual Studio 诊断工具
    VS 自带“诊断工具”窗口,可对进程拍内存快照,对比两次快照的堆块差异,一键定位增长最快的泄漏点。

  5. Deleaker
    商业插件,深度集成到 Visual Studio。在调试运行期间实时分析堆分配,可视化展示泄漏模块、大小、调用栈,支持原生 C++ 与 .NET 混合项目。

一句话选型:

  • Linux / macOS:ValgrindASan(编译器自带,零成本)
  • Windows:VS 诊断工具 日常够用,Deleaker 深度可付费
  • CI 集成:ASan + LSan 最快最轻,报错即中止构建

1.使用 Valgrind 检测 C++ 内存泄漏示例

下面这段代码动态申请了一个数组,却忘了 delete[],造成明显泄漏。我们用 Valgrind 一步步揪出它。

源文件:memory_leak.cpp

#include <iostream>
using namespace std;

void createLeak()          // 故意泄漏函数
{
    int* arr = new int[10]; // 申请 10 个 int(40 B × 10 = 400 B)
    // 忘记 delete[] arr;
}

int main()
{
    createLeak();
    cout << "Program finished." << endl;
    return 0;
}

检测步骤:

(1). 编译(带调试信息)

g++ -g -o memory_leak memory_leak.cpp

(2). 运行 Valgrind

valgrind --leak-check=full ./memory_leak

Valgrind 输出中文解读:

==12345== Memcheck, a memory error detector
==12345== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==12345== Using Valgrind-3.14.0 and LibVEX; rerun with -h for copyright info
==12345== Command: ./leaky_program
==12345== 
==12345== 
==12345== HEAP SUMMARY: 堆摘要
==12345==     in use at exit: 400 bytes in 1 blocks
==12345==   total heap usage: 1 allocs, 0 frees, 400 bytes allocated
==12345== 
==12345== 400 bytes in 1 blocks are definitely lost in loss record 1 of 1
==12345==    at 0x4C2AB80: operator new[](unsigned long) (vg_replace_malloc.c:423)
==12345==    by 0x1091A9: createMemoryLeak() (leaky_program.cpp:7)
==12345==    by 0x1091BF: main (leaky_program.cpp:11)
==12345== 
==12345== LEAK SUMMARY: 泄漏汇总
==12345==    definitely lost: 400 bytes in 1 blocks
==12345==    indirectly lost: 0 bytes in 0 blocks
==12345==      possibly lost: 0 bytes in 0 blocks
==12345==    still reachable: 0 bytes in 0 blocks
==12345==         suppressed: 0 bytes in 0 blocks
==12345== 
==12345== For counts of detected and suppressed errors, rerun with: -v
==12345== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)

结论:

  • 400 字节确定丢失 → 100% 泄漏
  • 泄漏点精确指向 createLeak() 第 7 行
  • 修复方法:在函数末尾加 delete[] arr; 即可让 Valgrind 报告 “0 errors”
本站提供的所有下载资源均来自互联网,仅提供学习交流使用,版权归原作者所有。如需商业使用,请联系原作者获得授权。 如您发现有涉嫌侵权的内容,请联系我们 邮箱:[email protected]