【CPP】内存泄漏详解
一、内存泄漏概念
内存泄漏(Memory Leak)是指程序在运行过程中申请了内存而未能释放,导致这部分内存无法被系统回收,从而造成内存资源的浪费。内存泄漏通常会逐渐积累,导致程序占用过多的内存资源,最终可能引发程序崩溃、系统卡顿,甚至在长时间运行的系统中发生内存溢出。内存泄漏问题在开发过程中必须引起足够重视,因为它会显著降低系统性能并影响程序的稳定性。
在 C++ 中,内存泄漏通常是由于开发者在使用动态内存分配(如 new
/ malloc
)时未能正确释放内存(如未使用 delete
/ free
)所导致。内存泄漏不仅仅发生在堆内存分配时,还可能发生在其他资源(如文件句柄、数据库连接等)未正确释放时。
二、内存泄漏常见场景
1)忘记释放堆内存
当使用 new
或 malloc
分配堆内存时,如果没有使用 delete
或 free
释放这部分内存,就会导致内存泄漏。这种情况是最常见的内存泄漏问题。忘记释放内存会导致程序持续占用内存,最终耗尽系统资源。
#include <iostream>void leakMemory() {int* p = new int(10); // 动态分配内存// 忘记释放内存
}int main() {leakMemory();return 0;
}
- 在上面的代码中,
new int(10)
分配了堆内存,但没有释放内存,导致内存泄漏。程序退出时,系统无法回收这部分内存。
2)异常路径未释放资源
如果程序中发生异常,且没有适当地释放已分配的资源(例如堆内存、文件句柄等),也会导致内存泄漏。特别是在使用裸指针或手动资源管理时,容易漏掉释放的步骤。异常发生时未进行资源清理,常常会导致程序不稳定。
#include <iostream>
#include <stdexcept>void functionWithException() {int* p = new int(20); // 分配内存throw std::runtime_error("An error occurred"); // 异常发生delete p; // 这行代码不会被执行
}int main() {try {functionWithException();} catch (const std::exception& e) {std::cout << e.what() << std::endl;}return 0;
}
- 在
functionWithException
函数中,分配了堆内存并抛出异常。由于delete p
没有执行,导致分配的内存没有被释放。
3)容器中指针未清理
C++ 容器(如 std::vector
, std::list
, std::map
)存储的是指向对象的指针。如果在容器中存储指针类型数据而没有正确释放对象的内存,会导致内存泄漏。容器在销毁时并不会自动释放容器内存中的指针对象。
#include <iostream>
#include <vector>class MyClass {
public:MyClass() { std::cout << "MyClass constructor" << std::endl; }~MyClass() { std::cout << "MyClass destructor" << std::endl; }
};void containerMemoryLeak() {std::vector<MyClass*> vec;for (int i = 0; i < 10; ++i) {vec.push_back(new MyClass()); // 分配内存}// 忘记调用 delete 删除指针
}int main() {containerMemoryLeak();return 0;
}
- 在
containerMemoryLeak
函数中,std::vector
存储了MyClass
类型的裸指针。由于没有调用delete
释放内存,这些指针指向的对象不会被销毁,导致内存泄漏。
4)循环引用
循环引用是指两个或多个对象互相持有对方的引用,导致它们的生命周期无法结束。这种情况通常发生在使用智能指针时,尤其是 std::shared_ptr
,因为 shared_ptr
会引用计数,只要引用计数不为零,对象就不会被销毁。
#include <iostream>
#include <memory>class A;class B {
public:std::shared_ptr<A> a;~B() { std::cout << "B destroyed" << std::endl; }
};class A {
public:std::shared_ptr<B> b;~A() { std::cout << "A destroyed" << std::endl; }
};void circularReference() {std::shared_ptr<A> a = std::make_shared<A>();std::shared_ptr<B> b = std::make_shared<B>();a->b = b; // A 拥有 Bb->a = a; // B 拥有 A// 循环引用导致 A 和 B 永远不会被销毁
}int main() {circularReference();return 0;
}
- 在
circularReference
函数中,A
类和B
类相互持有std::shared_ptr
,造成了循环引用。由于shared_ptr
会递增引用计数,导致A
和B
永远无法被销毁,内存泄漏发生。
5)系统资源泄漏
除此之外,程序中还可能发生其他类型的资源泄漏,特别是系统资源,如文件描述符、网络连接、数据库连接等。系统资源泄漏指的是在程序运行时,分配了某些资源但未能及时释放,导致这些资源无法再被使用,可能会导致系统性能下降或甚至崩溃。
常见的系统资源泄漏场景包括:
- 文件描述符泄漏:在程序中打开文件后,没有及时关闭文件描述符,导致文件描述符在程序结束时依然保持打开状态。随着时间的推移,打开的文件描述符数量会累积,最终可能达到系统的最大文件描述符限制,导致程序无法再打开新的文件。
- 网络连接泄漏:在程序中创建了网络连接(例如,套接字连接)后,如果没有适当关闭这些连接,会导致连接池资源被耗尽,从而影响程序的网络通信功能。
- 数据库连接泄漏:在使用数据库时,如果没有关闭数据库连接池中的连接,会导致连接泄漏,最终导致数据库连接池中的连接数量达到上限,影响系统的数据库操作。
#include <iostream>
#include <fstream>void fileLeak() {std::ofstream file("example.txt"); // 打开文件// 文件没有关闭,会导致文件描述符泄漏// 由于 file 对象是局部变量,程序结束时会自动析构,但如果没有显示调用 close(),会造成文件描述符泄漏
}int main() {fileLeak();return 0;
}
三、内存泄漏避免
1) 使用智能指针
智能指针是 C++11 引入的一种指针类型,用于自动管理内存的分配和释放。std::unique_ptr
和 std::shared_ptr
是两种常用的智能指针,它们能够自动在超出作用域时释放内存,避免忘记释放的风险。
std::unique_ptr
:用于独占所有权的智能指针。一个unique_ptr
只能有一个所有者,因此当其作用域结束时,内存会自动释放。std::shared_ptr
:用于共享所有权的智能指针。多个shared_ptr
可以共享同一个资源,只有当最后一个shared_ptr
被销毁时,内存才会被释放。
#include <memory>void exampleUniquePtr() {std::unique_ptr<int> p(new int(10)); // 使用 unique_ptr 自动管理内存// 不需要手动调用 delete,内存会在作用域结束时自动释放
}void exampleSharedPtr() {std::shared_ptr<int> p1 = std::make_shared<int>(20); // 使用 shared_ptr 自动管理内存std::shared_ptr<int> p2 = p1; // p1 和 p2 共享内存// 内存会在 p1 和 p2 被销毁时自动释放
}
2) RAII 原则
RAII(Resource Acquisition Is Initialization)是 C++ 中的一种资源管理模式,意味着资源(如内存、文件句柄、网络连接等)在对象的构造函数中分配,并且在析构函数中释放。通过这种方式,可以确保资源在对象生命周期结束时被正确释放,从而避免内存泄漏。
#include <iostream>class ResourceManager {
public:ResourceManager() {p = new int(10); // 构造函数中分配资源}~ResourceManager() {delete p; // 析构函数中释放资源}private:int* p;
};void exampleRAII() {ResourceManager rm; // 自动管理内存,RAII原理// 不需要手动释放内存,析构函数会自动调用
}
3) 避免裸指针长期持有资源
裸指针容易导致内存泄漏,特别是在长时间持有资源时。如果在某些情况下必须使用裸指针,确保有合适的释放逻辑。通常情况下,推荐使用智能指针来管理内存,避免裸指针长期持有资源。
#include <memory>class MyClass {
public:void* ptr;MyClass() { ptr = malloc(100); // 分配内存}~MyClass() {if (ptr) {free(ptr); // 确保资源被释放}}
};
4) 规范编码
确保每个 new
对应一个 delete
,每个 malloc
对应一个 free
。为了避免内存泄漏,必须确保每次使用 new
或 malloc
分配内存时,都会有相应的 delete
或 free
语句释放内存。在复杂的函数中,特别是有多个分支的函数中,需要格外注意确保每条路径都能正确释放资源。
#include <iostream>void safeMemoryManagement() {int* p = new int(10); // 使用 new 分配内存if (p == nullptr) {// 内存分配失败的处理return;}// 确保在所有路径中都能释放内存delete p; // 对应的 delete
}
5) 避免循环引用
循环引用通常发生在智能指针(特别是 std::shared_ptr
)之间,导致对象永远不能被销毁。为了避免循环引用,可以使用 std::weak_ptr
,它不会增加引用计数,从而避免循环引用带来的内存泄漏。
#include <iostream>
#include <memory>class A;
class B {
public:std::weak_ptr<A> a; // 使用 weak_ptr 避免循环引用
};class A {
public:std::shared_ptr<B> b;
};void fixCircularReference() {std::shared_ptr<A> a = std::make_shared<A>();std::shared_ptr<B> b = std::make_shared<B>();a->b = b;b->a = a; // 使用 weak_ptr 解决循环引用
}int main() {fixCircularReference();return 0;
}
四、内存泄漏排查
内存泄漏的排查和定位是开发过程中重要的调试任务。尽早发现并修复内存泄漏能够提高程序的稳定性,减少系统资源的浪费。以下是一些常用的内存泄漏排查方法:
1) 使用内存检测工具
内存检测工具可以帮助开发人员检测和定位内存泄漏。最常用的工具包括:
Valgrind
:Valgrind
是一个开源的内存调试工具,它能够在程序运行时监控内存的分配和释放,帮助开发者检测内存泄漏、越界访问、未初始化内存的使用等问题。Valgrind
提供了memcheck
工具来检测内存泄漏。AddressSanitizer
:AddressSanitizer
(简称 ASan)是一个快速的内存错误检测工具,能够检测内存泄漏、越界访问等问题。它通过编译时插桩,在程序运行时进行检查。heaptrack
:heaptrack
是一个专门用于跟踪 C++ 程序内存分配的工具,它能够提供详细的内存分配跟踪,并生成分析报告,帮助开发者精确定位内存泄漏问题。
2) 手动代码审查
除了工具,手动代码审查也是排查内存泄漏的重要手段。通过代码审查,开发人员可以查找可能的内存泄漏点,以下是常见的检查点:
- 确保每个
new
或malloc
都有相应的delete
或free
: 对于每次动态内存分配,必须确保有对应的释放操作,避免遗忘释放。 - 确保异常发生时的内存释放: 在函数中,特别是有异常处理的函数中,确保即使在异常发生时也能够释放已分配的内存。可以使用 RAII 原则(通过构造函数分配资源,析构函数释放资源)来避免异常未释放资源。
- 查找是否有未释放的容器内存: 特别是在容器中存储动态分配的内存时,确保容器的析构函数正确释放内存。
- 检查是否有裸指针的使用: 裸指针可能导致内存管理不当,使用智能指针(如
std::unique_ptr
和std::shared_ptr
)可以避免许多内存泄漏问题。 - 检查
shared_ptr
是否存在循环引用: 在使用std::shared_ptr
时,确保不存在循环引用的情况,循环引用会导致内存无法释放。可以使用std::weak_ptr
来避免这种问题。
3) 日志和堆栈跟踪
通过在代码中加入日志打印,可以帮助开发者跟踪内存的分配和释放过程。特别是在调试复杂的程序时,堆栈跟踪信息也能帮助定位内存泄漏的根源。
-
日志打印:可以在内存分配和释放的关键点添加日志,记录指针值和内存分配的上下文信息。
#include <iostream>void* operator new(size_t size) {std::cout << "Allocating memory of size " << size << " bytes\n";return malloc(size);}void operator delete(void* pointer) noexcept {std::cout << "Freeing memory at " << pointer << "\n";free(pointer);}void exampleMemoryLeak() {int* p = new int(10);// 忘记释放内存}int main() {exampleMemoryLeak();return 0;}
-
堆栈跟踪:使用调试器(如 GDB)时,启用调试符号并查看堆栈跟踪,帮助开发者更好地定位内存泄漏的根源。
g++ -g your_program.cpp -o your_program gdb ./your_program
4) 内存管理策略的优化
通过优化内存管理策略,可以减少内存泄漏的发生:
- 使用智能指针:在 C++ 中,尽量使用智能指针(如
std::unique_ptr
和std::shared_ptr
),它们能够自动管理内存的分配和释放,避免手动管理带来的内存泄漏问题。 - 使用容器和算法:在适当的情况下,使用标准库中的容器和算法(如
std::vector
,std::map
,std::string
等),这些容器能够自动管理内存,减少内存泄漏的风险。
五、Valgrind
1)Valgrind使用
-
valgrind [选项] <可执行程序>
valgrind --leak-check=full --show-leak-kinds=all ./your_program
--leak-check=full
:启用内存泄漏检测,并显示详细的泄漏信息。full
选项会提供有关内存泄漏的详细报告,包括泄漏的内存量、泄漏发生的代码行以及调用栈等信息。--show-leak-kinds=all
:显示所有类型的内存泄漏,包括直接泄漏、间接泄漏、可能泄漏等。--track-origins=yes
:追踪未初始化内存的来源。这对于调试未初始化内存访问非常有帮助。--tool=memcheck
:选择memcheck
工具来进行内存检查,memcheck
是Valgrind
默认的内存检查工具。--log-file=<file>
:将Valgrind
的输出记录到指定的文件中。对于大型程序或长时间运行的程序,这个选项非常有用。
- 编译代码时要使用
-g
选项
2)Valgrind输出
Valgrind
的输出可以帮助开发者详细了解程序中的内存问题,特别是内存泄漏的情况。以下是一个典型的 Valgrind
输出示例:
==1208228== Memcheck, a memory error detector
==1208228== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al.
==1208228== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info
==1208228== Command: ./a.out
==1208228==
==1208228==
==1208228== HEAP SUMMARY:
==1208228== in use at exit: 20 bytes in 1 blocks
==1208228== total heap usage: 1 allocs, 0 frees, 20 bytes allocated
==1208228==
==1208228== 20 bytes in 1 blocks are definitely lost in loss record 1 of 1
==1208228== at 0x4848899: malloc (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so)
==1208228== by 0x1091B0: make_copy(char const*) (in /home/Raizeroko/code/MemoryLeak/a.out)
==1208228== by 0x1091E4: main (in /home/Raizeroko/code/MemoryLeak/a.out)
==1208228==
==1208228== LEAK SUMMARY:
==1208228== definitely lost: 20 bytes in 1 blocks
==1208228== indirectly lost: 0 bytes in 0 blocks
==1208228== possibly lost: 0 bytes in 0 blocks
==1208228== still reachable: 0 bytes in 0 blocks
==1208228== suppressed: 0 bytes in 0 blocks
==1208228==
==1208228== For lists of detected and suppressed errors, rerun with: -s
==1208228== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
- HEAP SUMMARY:
in use at exit: 20 bytes in 1 blocks
:表示程序退出时,有 20 字节的内存依然在使用中,且是 1 个内存块。total heap usage: 1 allocs, 0 frees, 20 bytes allocated
:表示程序共进行了 1 次内存分配操作,且没有进行内存释放,分配了 20 字节的内存。
- 内存泄漏详细信息:
20 bytes in 1 blocks are definitely lost in loss record 1 of 1
:表明程序确实发生了 20 字节的内存泄漏,这块内存没有被释放。at 0x4848899: malloc
:泄漏发生的位置是malloc
函数内存分配的地方。
- 泄漏发生的调用堆栈:
by 0x1091B0: make_copy(char const*)
:表示泄漏的内存是在make_copy
函数中分配的。by 0x1091E4: main
:泄漏发生在main
函数中调用make_copy
时。
- LEAK SUMMARY:
definitely lost
:真·泄漏,程序没有任何指针指向这块内存了indirectly lost
:间接泄漏,一块泄漏的内存引用了另一块possibly lost
:可能泄漏,Valgrind
不确定程序是否还保留了这块内存的地址still reachable
:程序结束时仍有指针指向,不一定是 bug,比如全局缓存suppressed
:被你通过配置 suppress 文件显式屏蔽的问题,Valgrind 不再报告它们。
- ERROR SUMMARY:
1 errors from 1 contexts (suppressed: 0 from 0)
:表明总共检测到 1 个错误,且没有被抑制。
definitely lost
、indirectly lost
、possibly lost
和still reachable
之间的区别
definitely lost
(确定泄漏)这表示程序没有任何指针指向该内存块,内存已经丢失。也就是说,程序员再也无法访问到这块内存,它完全“失去了控制”。
int main() {int* ptr = (int*)malloc(sizeof(int)); // 动态分配内存// 这里忘了释放内存return 0; }
泄漏信息
==12345== 4 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==12345== at 0x4C2FB55: malloc (vg_replace_malloc.c:299) ==12345== by 0x4011F3: main (main.cpp:5)
indirectly lost
(间接泄漏)这种情况通常发生在某个指针指向的内存块无法释放,而该指针本身是由另一个对象或结构体所指向的。间接泄漏意味着某块内存可能是由于其他内存块的丢失而无法释放的。
struct Node {int* data; };int main() {Node* node = (Node*)malloc(sizeof(Node));node->data = (int*)malloc(sizeof(int));// 这里忘了释放内存free(node);return 0;}
泄漏信息
==12345== 4 bytes in 1 blocks are indirectly lost ==12345== at 0x4C2FB55: malloc (vg_replace_malloc.c:299) ==12345== by 0x4011F3: main (main.cpp:6) ==12345== by 0x401207: free (vg_replace_malloc.c:299)
possibly lost
(可能泄漏)这种情况表示程序中有一个指针指向了这块内存,但在程序运行时,Valgrind 无法确定是否程序仍然能访问到这块内存。程序可能仍然有机会访问它,但不确定。
int main() {int* ptr = (int*)malloc(sizeof(int)); // 动态分配内存ptr = (int*)malloc(sizeof(int)); // 重新分配,原来的内存被覆盖free(ptr);return 0; }
泄漏信息
==12345== 4 bytes in 1 blocks are possibly lost ==12345== at 0x4C2FB55: malloc (vg_replace_malloc.c:299) ==12345== by 0x4011F3: main (main.cpp:5)
still reachable
(仍可访问)这种情况表示程序仍然有指针能够访问到这块内存,这块内存没有被泄漏,程序可以正常释放它。通常这是程序正常退出时的情况。
int main() {int* ptr = (int*)malloc(sizeof(int)); // 动态分配内存free(ptr); // 正常释放return 0; }
泄漏信息
==12345== 4 bytes in 1 blocks are still reachable ==12345== at 0x4C2FB55: malloc (vg_replace_malloc.c:299) ==12345== by 0x4011F3: main (main.cpp:5)
supressed
(被抑制的)很多时候,一些库或系统函数会在你无法控制的地方分配小量内存(比如系统内部的 glibc、C++标准库,或者 Qt、Boost、OpenSSL 等)。这些泄漏你也许无法修复,但每次都出现在报告里很烦,这时候可以写 suppression 规则文件,告诉 Valgrind 忽略这些。
suppression文件示例:
{libc-startMemcheck:Leakfun:mallocfun:__libc_start_main }
然后运行 valgrind 加上
--suppressions=<supression文件>
选项valgrind --suppressions=mysuppress.supp ./your_program
3)借助valgrind查询泄漏
- 为了简化代码,部分泄漏处理不太合理,只是为了方便讲解。
1. 内存分配后忘记释放
在 C++ 中,如果你为一个对象或数组动态分配了内存,但没有在适当的位置释放这些内存,就会导致内存泄漏。示例:
class MyClass {
public:MyClass() {ptr = new int[100];}~MyClass() {}private:int* ptr;
};int main() {MyClass* obj = new MyClass();return 0;
}
-
valgrind输出:
Ubuntu ❯❯❯ valgrind --leak-check=full ./a.out ==1245080== Memcheck, a memory error detector ==1245080== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. ==1245080== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info ==1245080== Command: ./a.out ==1245080== ==1245080== ==1245080== HEAP SUMMARY: ==1245080== in use at exit: 408 bytes in 2 blocks ==1245080== total heap usage: 3 allocs, 1 frees, 73,112 bytes allocated ==1245080== ==1245080== 408 (8 direct, 400 indirect) bytes in 1 blocks are definitely lost in loss record 2 of 2 ==1245080== at 0x4849013: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==1245080== by 0x109201: main (leak1.cc:17) ==1245080== ==1245080== LEAK SUMMARY: ==1245080== definitely lost: 8 bytes in 1 blocks ==1245080== indirectly lost: 400 bytes in 1 blocks ==1245080== possibly lost: 0 bytes in 0 blocks ==1245080== still reachable: 0 bytes in 0 blocks ==1245080== suppressed: 0 bytes in 0 blocks ==1245080== ==1245080== For lists of detected and suppressed errors, rerun with: -s ==1245080== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
-
分析
- Heap Summary (堆内存总结):
in use at exit: 408 bytes in 2 blocks
:程序结束时,仍有 408 字节的内存在使用中。这个值表明程序退出时存在未释放的内存块。total heap usage: 3 allocs, 1 frees, 73,112 bytes allocated
:在整个程序的执行过程中,进行了 3 次内存分配(allocs
)和 1 次内存释放(frees
)。这说明在程序结束时,分配的内存没有完全被释放。
- Leak Details (泄漏详细信息):
408 (8 direct, 400 indirect) bytes in 1 blocks are definitely lost in loss record 2 of 2
:这里报告了内存泄漏的详细信息:- 8 字节的内存被“直接丢失”(
direct lost
)。 - 400 字节的内存被“间接丢失”(
indirect lost
)。这通常是由于内存块引用了其他内存,而这些引用没有被释放,导致这些内存无法被正确回收。 - 这一行提供了泄漏发生的精确位置,指出是某个内存块的丢失,且是由程序中的特定代码引起的。
- 8 字节的内存被“直接丢失”(
- 泄漏发生位置:
at 0x4849013: operator new(unsigned long)
:泄漏发生的位置是new
操作符的调用,它显示了分配内存时的堆栈地址。by 0x109201: main (in /home/Raizeroko/code/MemoryLeak/a.out)
:泄漏发生的具体代码位置是在main
函数中。
- Leaked Memory Summary (泄漏内存汇总):
definitely lost: 8 bytes in 1 blocks
:这意味着 8 字节内存丢失的部分是“确定丢失”(definitely lost
),即程序已经没有任何指针指向该内存区域,这块内存完全无法访问和释放。indirectly lost: 400 bytes in 1 blocks
:这块 400 字节的内存间接丢失,通常是因为它依赖于已丢失的内存块(例如通过指针引用)。间接丢失的内存一般是由内存泄漏的某个其他部分引起的。
- Heap Summary (堆内存总结):
-
改进
class MyClass { public:MyClass() {data = new int[100];}~MyClass() {delete[] data; // 析构函数释放}private:int* data; };void createObject() {MyClass* obj = new MyClass();// 释放(实际一般不在这里delete,这里为了方便)delete obj; }int main() {for (int i = 0; i < 10; ++i) {createObject();}return 0; }
2. 异常路径导致的内存泄漏
如果在某个操作抛出异常,并且没有相应的内存释放机制,程序就会发生内存泄漏。示例:
void processData() {int* data = new int[50];if (rand() % 2 == 0) {throw std::runtime_error("Error during data processing");}delete[] data;
}void createObject() {try {processData();} catch (const std::exception& e) {std::cout << "Caught exception: " << e.what() << std::endl;}
}int main() {for (int i = 0; i < 5; ++i) {createObject();}return 0;
}
-
valgrind信息
Ubuntu ❯❯❯ valgrind --leak-check=full ./a.out ==1245306== Memcheck, a memory error detector ==1245306== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. ==1245306== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info ==1245306== Command: ./a.out ==1245306== Caught exception: Error during data processing ==1245306== ==1245306== HEAP SUMMARY: ==1245306== in use at exit: 200 bytes in 1 blocks ==1245306== total heap usage: 9 allocs, 8 frees, 74,925 bytes allocated ==1245306== ==1245306== 200 bytes in 1 blocks are definitely lost in loss record 1 of 1 ==1245306== at 0x484A2F3: operator new[](unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==1245306== by 0x109301: processData() (leak2.cc:5) ==1245306== by 0x10939C: createObject() (leak2.cc:14) ==1245306== by 0x10944D: main (leak2.cc:22) ==1245306== ==1245306== LEAK SUMMARY: ==1245306== definitely lost: 200 bytes in 1 blocks ==1245306== indirectly lost: 0 bytes in 0 blocks ==1245306== possibly lost: 0 bytes in 0 blocks ==1245306== still reachable: 0 bytes in 0 blocks ==1245306== suppressed: 0 bytes in 0 blocks ==1245306== ==1245306== For lists of detected and suppressed errors, rerun with: -s ==1245306== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
-
分析
- Heap Summary (堆内存总结):
in use at exit: 200 bytes in 1 blocks
:程序退出时仍有 200 字节内存未被释放。这个值表明存在未释放的内存。total heap usage: 9 allocs, 8 frees, 74,925 bytes allocated
:程序总共进行了 9 次内存分配(allocs
)和 8 次内存释放(frees
)。这表明有一次内存分配未被释放。
- 泄漏详细信息:
200 bytes in 1 blocks are definitely lost in loss record 1 of 1
:200 字节内存是“确定丢失”(definitely lost
),说明这部分内存已经无法访问,也没有其他指针指向它。at 0x484A2F3: operator new[](unsigned long)
:泄漏发生在new[]
操作符的位置。by 0x109301: processData() (leak2.cc:5)
:泄漏发生在processData
函数中的第 5 行,即new int[50]
这一行。by 0x10939C: createObject() (leak2.cc:14)
:processData
被createObject
函数调用,而createObject
位于第 14 行。by 0x10944D: main (leak2.cc:22)
:最终,createObject
函数被main
函数调用,main
函数位于第 22 行。
- Heap Summary (堆内存总结):
-
改正
#include <memory>void processData() {auto data = std::make_unique<int[]>(50); // 使用智能指针管理内存if (rand() % 2 == 0) {throw std::runtime_error("Error during data processing");}// 内存会在智能指针超出作用域时自动释放 }void createObject() {try {processData();} catch (const std::exception& e) {std::cout << "Caught exception: " << e.what() << std::endl;} }int main() {for (int i = 0; i < 5; ++i) {createObject();}return 0; }
3. 循环引用
循环引用通常发生在两个或多个对象互相持有对方的引用,从而形成一个环形结构。当这种环形结构的对象没有显式的释放机制时,就会导致内存无法被回收,从而引发内存泄漏。示例:
class A;
class B {
public:A* a;B() : a(nullptr) {}
};class A {
public:B* b;A() : b(nullptr) {}
};void createObjects(std::vector<A*> &vecA, std::vector<B*> &vecB) {A* objA = new A();B* objB = new B();objA->b = objB;objB->a = objA;vecA.push_back(objA);vecB.push_back(objB);
}int main() {std::vector<A*> objectsA;std::vector<B*> objectsB;for (int i = 0; i < 5; ++i) {createObjects(objectsA, objectsB);}return 0;
}
-
Valgrind信息
Ubuntu ❯❯❯ valgrind --leak-check=full ./a.out ==1245671== Memcheck, a memory error detector ==1245671== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. ==1245671== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info ==1245671== Command: ./a.out ==1245671== A created B created ==1245671== ==1245671== HEAP SUMMARY: ==1245671== in use at exit: 64 bytes in 2 blocks ==1245671== total heap usage: 4 allocs, 2 frees, 73,792 bytes allocated ==1245671== ==1245671== 64 (32 direct, 32 indirect) bytes in 1 blocks are definitely lost in loss record 2 of 2 ==1245671== at 0x4849013: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==1245671== by 0x10A929: __gnu_cxx::new_allocator<std::_Sp_counted_ptr_inplace<A, std::allocator<A>, (__gnu_cxx::_Lock_policy)2> >::allocate(unsigned long, void const*) (new_allocator.h:127) ==1245671== by 0x10A622: std::allocator_traits<std::allocator<std::_Sp_counted_ptr_inplace<A, std::allocator<A>, (__gnu_cxx::_Lock_policy)2> > >::allocate(std::allocator<std::_Sp_counted_ptr_inplace<A, std::allocator<A>, (__gnu_cxx::_Lock_policy)2> >&, unsigned long) (alloc_traits.h:464) ==1245671== by 0x10A15F: std::__allocated_ptr<std::allocator<std::_Sp_counted_ptr_inplace<A, std::allocator<A>, (__gnu_cxx::_Lock_policy)2> > > std::__allocate_guarded<std::allocator<std::_Sp_counted_ptr_inplace<A, std::allocator<A>, (__gnu_cxx::_Lock_policy)2> > >(std::allocator<std::_Sp_counted_ptr_inplace<A, std::allocator<A>, (__gnu_cxx::_Lock_policy)2> >&) (allocated_ptr.h:98) ==1245671== by 0x109E65: std::__shared_count<(__gnu_cxx::_Lock_policy)2>::__shared_count<A, std::allocator<A>>(A*&, std::_Sp_alloc_shared_tag<std::allocator<A> >) (shared_ptr_base.h:648) ==1245671== by 0x109DA5: std::__shared_ptr<A, (__gnu_cxx::_Lock_policy)2>::__shared_ptr<std::allocator<A>>(std::_Sp_alloc_shared_tag<std::allocator<A> >) (shared_ptr_base.h:1342) ==1245671== by 0x109CA4: std::shared_ptr<A>::shared_ptr<std::allocator<A>>(std::_Sp_alloc_shared_tag<std::allocator<A> >) (shared_ptr.h:409) ==1245671== by 0x109AF5: std::shared_ptr<A> std::allocate_shared<A, std::allocator<A>>(std::allocator<A> const&) (shared_ptr.h:863) ==1245671== by 0x109781: std::shared_ptr<A> std::make_shared<A>() (shared_ptr.h:879) ==1245671== by 0x109290: main (leak4.cc:21) ==1245671== ==1245671== LEAK SUMMARY: ==1245671== definitely lost: 32 bytes in 1 blocks ==1245671== indirectly lost: 32 bytes in 1 blocks ==1245671== possibly lost: 0 bytes in 0 blocks ==1245671== still reachable: 0 bytes in 0 blocks ==1245671== suppressed: 0 bytes in 0 blocks ==1245671== ==1245671== For lists of detected and suppressed errors, rerun with: -s ==1245671== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
-
分析
- Heap Summary (堆内存总结):
in use at exit: 64 bytes in 2 blocks
:程序退出时仍然有 64 字节内存未被释放。这表示程序创建了 2 个对象,但没有正确释放。total heap usage: 4 allocs, 2 frees, 73,792 bytes allocated
:程序进行了 4 次内存分配和 2 次内存释放,意味着有 2 次内存分配没有相应的释放。
- 泄漏详细信息:
64 (32 direct, 32 indirect) bytes in 1 blocks are definitely lost in loss record 2 of 2
:内存泄漏发生在两个对象之间的循环引用中,其中一个对象(A
)和另一个对象(B
)互相引用,导致它们的内存无法被释放。at 0x4849013: operator new(unsigned long)
:泄漏发生在new
操作符的位置,即在内存分配时。- 该泄漏涉及的是
A
和B
类的实例,它们通过直接和间接引用(循环引用)互相持有指针
- Heap Summary (堆内存总结):
-
改进
class B; // 前向声明class A { public:std::shared_ptr<B> b_ptr;~A() { std::cout << "A destroyed\n"; } };class B { public:std::weak_ptr<A> a_ptr; // 用 weak_ptr 打破循环~B() { std::cout << "B destroyed\n"; } };
4)进阶泄漏解决
1. 构造函数中隐式形成引用环(通过回调)
#include <iostream>
#include <memory>
#include <functional>class CallbackOwner;class Callback {
public:std::function<void()> func;~Callback() { std::cout << "~Callback()\n"; }
};class CallbackOwner : public std::enable_shared_from_this<CallbackOwner> {
public:std::shared_ptr<Callback> cb;void bind() {cb = std::make_shared<Callback>();cb->func = [self = shared_from_this()]() {std::cout << "Using CallbackOwner\n";};} ~CallbackOwner() {std::cout << "~CallbackOwner()\n";}
};int main() {auto owner = std::make_shared<CallbackOwner>();owner->bind();return 0;
}
-
valgrind输出
Ubuntu ❯❯❯ valgrind --leak-check=full ./a.out ==1233846== Memcheck, a memory error detector ==1233846== Copyright (C) 2002-2017, and GNU GPL'd, by Julian Seward et al. ==1233846== Using Valgrind-3.18.1 and LibVEX; rerun with -h for copyright info ==1233846== Command: ./a.out ==1233846== ==1233846== ==1233846== HEAP SUMMARY: ==1233846== in use at exit: 112 bytes in 3 blocks ==1233846== total heap usage: 4 allocs, 1 frees, 72,816 bytes allocated ==1233846== ==1233846== 112 (48 direct, 64 indirect) bytes in 1 blocks are definitely lost in loss record 3 of 3 ==1233846== at 0x4849013: operator new(unsigned long) (in /usr/libexec/valgrind/vgpreload_memcheck-amd64-linux.so) ==1233846== by 0x10C832: __gnu_cxx::new_allocator<std::_Sp_counted_ptr_inplace<CallbackOwner, std::allocator<CallbackOwner>, (__gnu_cxx::_Lock_policy)2> >::allocate(unsigned long, void const*) (new_allocator.h:127) ==1233846== by 0x10C464: std::allocator_traits<std::allocator<std::_Sp_counted_ptr_inplace<CallbackOwner, std::allocator<CallbackOwner>, (__gnu_cxx::_Lock_policy)2> > >::allocate(std::allocator<std::_Sp_counted_ptr_inplace<CallbackOwner, std::allocator<CallbackOwner>, (__gnu_cxx::_Lock_policy)2> >&, unsigned long) (alloc_traits.h:464) ==1233846== by 0x10C031: std::__allocated_ptr<std::allocator<std::_Sp_counted_ptr_inplace<CallbackOwner, std::allocator<CallbackOwner>, (__gnu_cxx::_Lock_policy)2> > > std::__allocate_guarded<std::allocator<std::_Sp_counted_ptr_inplace<CallbackOwner, std::allocator<CallbackOwner>, (__gnu_cxx::_Lock_policy)2> > >(std::allocator<std::_Sp_counted_ptr_inplace<CallbackOwner, std::allocator<CallbackOwner>, (__gnu_cxx::_Lock_policy)2> >&) (allocated_ptr.h:98) ==1233846== by 0x10BAEB: std::__shared_count<(__gnu_cxx::_Lock_policy)2>::__shared_count<CallbackOwner, std::allocator<CallbackOwner>>(CallbackOwner*&, std::_Sp_alloc_shared_tag<std::allocator<CallbackOwner> >) (shared_ptr_base.h:648) ==1233846== by 0x10B83B: std::__shared_ptr<CallbackOwner, (__gnu_cxx::_Lock_policy)2>::__shared_ptr<std::allocator<CallbackOwner>>(std::_Sp_alloc_shared_tag<std::allocator<CallbackOwner> >) (shared_ptr_base.h:1342) ==1233846== by 0x10B45E: std::shared_ptr<CallbackOwner>::shared_ptr<std::allocator<CallbackOwner>>(std::_Sp_alloc_shared_tag<std::allocator<CallbackOwner> >) (shared_ptr.h:409) ==1233846== by 0x10AF25: std::shared_ptr<CallbackOwner> std::allocate_shared<CallbackOwner, std::allocator<CallbackOwner>>(std::allocator<CallbackOwner> const&) (shared_ptr.h:863) ==1233846== by 0x10AA59: std::shared_ptr<CallbackOwner> std::make_shared<CallbackOwner>() (shared_ptr.h:879) ==1233846== by 0x10A2D0: main (leak6.cc:30) ==1233846== ==1233846== LEAK SUMMARY: ==1233846== definitely lost: 48 bytes in 1 blocks ==1233846== indirectly lost: 64 bytes in 2 blocks ==1233846== possibly lost: 0 bytes in 0 blocks ==1233846== still reachable: 0 bytes in 0 blocks ==1233846== suppressed: 0 bytes in 0 blocks ==1233846== ==1233846== For lists of detected and suppressed errors, rerun with: -s ==1233846== ERROR SUMMARY: 1 errors from 1 contexts (suppressed: 0 from 0)
-
分析
-
从输出中可以看到,在程序退出时,存在 112 字节的内存没有被释放,并且 Valgrind 确认这是直接泄漏。泄漏的内存块包括了
CallbackOwner
和Callback
对象的内存。特别是,在CallbackOwner
的内存分配过程中(具体在shared_ptr
的分配阶段),我们发现内存的引用计数始终无法归零。在调用
shared_from_this()
获取CallbackOwner
的共享指针时,CallbackOwner
会持有一个Callback
的shared_ptr
,而Callback
中的回调函数又持有CallbackOwner
的shared_ptr
。这就形成了一个循环引用,使得这两个对象的引用计数始终无法减为 0,因此内存无法被释放。 -
循环引用:
CallbackOwner
和Callback
对象相互持有shared_ptr
,形成了一个闭环。在CallbackOwner
的构造函数中,bind
函数通过shared_from_this()
获取CallbackOwner
的共享指针并将其赋值给Callback
的回调函数。而在回调函数中,Callback
又持有一个指向CallbackOwner
的shared_ptr
,这就导致了CallbackOwner
和Callback
相互引用,永远不会释放内存。 -
引用计数无法归零:由于循环引用的存在,
CallbackOwner
和Callback
对象的引用计数无法归零,导致它们的内存不会被释放,从而造成内存泄漏。
-
-
解决
void bind() {cb = std::make_shared<Callback>();// 使用weak_ptr防止循环引用std::weak_ptr<CallbackOwner> weak_self = shared_from_this();cb->func = [weak_self]() {std::cout << "Using CallbackOwner\n";}; }