动态内存管理
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/delete是 C++ 的运算符 ,负责 分配内存 + 构造/析构对象 ;
malloc/calloc/free是 C 的库函数 ,只负责 分配/释放原始内存 ,不管对象生命周期。
🔍 区别一览表:
| 特性 | 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 清零 |
| 是否可混用 | ❌ 绝对禁止 混用(new 配 free,malloc 配 delete) |
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++ 没有自动内存管理,查找泄漏相对困难。主要有两类方法:
-
人工审查
通读代码,找出所有
new/malloc后未配对delete/free的地方。 -
借助工具
使用 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++ 程序中的内存泄漏:
-
Valgrind
开源、跨平台的内存调试与性能分析利器。可检测泄漏、越界访问、使用已释放内存等问题,并给出详细源码级报告(泄漏大小、申请栈、调用链)。
-
AddressSanitizer(ASan)
现代 GCC/Clang 内置的快速内存错误检测器。
无需额外安装,只要编译器版本够新,加
-fsanitize=address即可在运行时捕获泄漏及其栈回溯。 -
LeakSanitizer(LSan)
专注于"仅泄漏"场景的编译器工具,通常与 ASan 一起工作。
GCC 4.9+ 默认集成,加
-fsanitize=leak即可启用,报告简洁、定位精准。 -
Visual Studio 诊断工具
VS 自带"诊断工具"窗口,可对进程拍内存快照,对比两次快照的堆块差异,一键定位增长最快的泄漏点。
-
Deleaker
商业插件,深度集成到 Visual Studio。在调试运行期间实时分析堆分配,可视化展示泄漏模块、大小、调用栈,支持原生 C++ 与 .NET 混合项目。
一句话选型:
- Linux / macOS:Valgrind 或 ASan(编译器自带,零成本)
- 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"。