学而时习之:C++中的动态内存管理

动态内存管理

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

new 与 delete 运算符

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

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

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

1.new 运算符

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

示例:

cpp 复制代码
#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 运算符也可用来动态分配一整块内存(即数组),语法如下:

cpp 复制代码
new 数据类型[元素个数];

该语句会在堆上为指定类型分配可容纳 n 个元素的连续空间。

分配时还可以直接初始化数组。

示例:

cpp 复制代码
#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。因此,使用前最好先检查返回的指针是否有效。

cpp 复制代码
int *p = new (nothrow) int;
if (!p) {
    cout << "内存分配失败\n";
}

4.delete 运算符

在 C++ 中,delete 运算符用于释放new 动态分配的内存。

它把先前 new 申请到的内存归还给系统。

语法:

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

示例:

cpp 复制代码
#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;
}
css 复制代码
*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/deleteC++ 的运算符 ,负责 分配内存 + 构造/析构对象
malloc/calloc/freeC 的库函数 ,只负责 分配/释放原始内存不管对象生命周期

🔍 区别一览表:

特性 new / delete malloc / 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
cpp 复制代码
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)。 一旦忘记释放,这块内存就永远丢失 ------程序运行期间无法再被其他代码使用,这就是内存泄漏

示例:

cpp 复制代码
#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)

bash 复制代码
g++ -fsanitize=address -g leak.cpp -o leak
./leak

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

2.快速上手:Valgrind

bash 复制代码
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

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). 编译(带调试信息)

bash 复制代码
g++ -g -o memory_leak memory_leak.cpp

(2). 运行 Valgrind

bash 复制代码
valgrind --leak-check=full ./memory_leak

Valgrind 输出中文解读:

yaml 复制代码
==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"
相关推荐
云知谷2 小时前
【经典书籍】《代码整洁之道》第六章“对象与数据结构”精华讲解
c语言·开发语言·c++·软件工程·团队开发
仰泳的熊猫3 小时前
1013 Battle Over Cities
数据结构·c++·算法·pat考试
渡我白衣4 小时前
字符串的陷阱与艺术——std::string全解析
网络·c++·人工智能·自然语言处理·智能路由器·信息与通信·caffe
吃不饱的得可可4 小时前
C++17常用新特性
开发语言·c++
_OP_CHEN4 小时前
算法基础篇:(七)基础算法之二分算法 —— 从 “猜数字” 到 “解难题” 的高效思维
c++·算法·蓝桥杯·二分查找·acm·二分答案·二分算法
一匹电信狗4 小时前
【C++11】Lambda表达式+新的类功能
服务器·c++·算法·leetcode·小程序·stl·visual studio
煤球王子4 小时前
学而时习之:C++中的枚举
c++
楼田莉子4 小时前
C++/Linux小项目:自主shell命令解释器
linux·服务器·开发语言·c++·后端·学习
草莓火锅5 小时前
用c++求第n个质数
开发语言·c++·算法