C语言中如何实现跟踪内存分配的机制?

摘要:内存是非常重要的东西 知道你的程序什么时候分配内存 特别是堆内存 是很有用的 如果知道程序在哪里分配内存 就有可能减少它 从而优化程序 也可以更好地了解程序是如何工作的 需要重写new运算符 来检测发生的内存分配 我们可以通过在operat
内存是非常重要的东西 知道你的程序什么时候分配内存 特别是堆内存 是很有用的 如果知道程序在哪里分配内存 就有可能减少它 从而优化程序 也可以更好地了解程序是如何工作的 需要重写new运算符 来检测发生的内存分配 我们可以通过在operator new中加入一个断点 来追踪这些内存分配的来源 #include <iostream> void* operator new(size_t size) { std::cout << "Allocating " << size << " bytes\n"; return malloc(size); } struct Object { int x, y, z; }; int main() { Object* obj = new Object; std::string string = "Miku"; } 在return malloc(size);这一行(第7行)设置断点 查看调用堆栈 Project_test.exe!operator new(unsigned __int64 size) 行 7 Project_test.exe!main() 行 17 所以就是Object* obj = new Object; 这一行调用了new std::string string = "Miku"; 这就不会发生堆分配 因为这是小字符串 但是debug模式下仍然会发生分配 查看调用堆栈 Project_test.exe!operator new(unsigned __int64 size) 行 7 Project_test.exe!std::_Default_allocate_traits::_Allocate(const unsigned __int64 _Bytes) 行 87 对调用堆栈的第2行 右键 - 转到源代码 可以看到 // 来自于<xmemory> struct _Default_allocate_traits { __declspec(allocator) static #ifdef __clang__ // Clang and MSVC implement P0784R7 differently; see GH-1532 _CONSTEXPR20 #endif // defined(__clang__) void* _Allocate(const size_t _Bytes) { return ::operator new(_Bytes); } 是在这里调用了operator new 如果把调用堆栈的显示外部代码关掉 就会变成 Project_test.exe!operator new(unsigned __int64 size) 行 7 [外部代码] Project_test.exe!main() 行 17 [外部代码] 如果使用智能指针std::unique_ptr obj = std::make_unique(); 而不是显式地调用new Project_test.exe!operator new(unsigned __int64 size) 行 8 Project_test.exe!std::make_unique<Object,0>() 行 3465 对调用堆栈的第2行转到源代码 // 来自于<memory> _EXPORT_STD template <class _Ty, class... _Types, enable_if_t<!is_array_v<_Ty>, int> = 0> _NODISCARD_SMART_PTR_ALLOC _CONSTEXPR23 unique_ptr<_Ty> make_unique(_Types&&... _Args) { // make a unique_ptr return unique_ptr<_Ty>(new _Ty(_STD forward<_Types>(_Args)...)); } make_unique是调用了new #include <iostream> #include <memory> void operator delete(void* memory) { free(memory); } struct Object { int x, y, z; }; int main() { { std::unique_ptr<Object> obj = std::make_unique<Object>(); } } 在free(memory);这行设置断点 查看调用堆栈 Project_test.exe!operator delete(void * memory) 行 6 Project_test.exe!operator delete(void * block, unsigned __int64 __formal) 行 32 Project_test.exe!std::default_delete::operator()(Object * _Ptr) 行 3170 Project_test.exe!std::unique_ptr<Object,std::default_delete>::~unique_ptr<Object,std::default_delete>() 行 3282 对调用堆栈的第4行查看源代码 这是unique_ptr的析构函数 // 来自于<memory> _CONSTEXPR23 ~unique_ptr() noexcept { if (_Mypair._Myval2) { _Mypair._Get_first()(_Mypair._Myval2); } } 对_Mypair速览定义 _Compressed_pair<_Dx, pointer> _Mypair; 对_Dx速览定义 定位到了 _EXPORT_STD template <class _Ty, class _Dx /* = default_delete<_Ty> */> class unique_ptr { // ... 稍微往下几行也找到了 using deleter_type = _Dx; 说明_Dx是个删除器(deleter)类型 所以_Mypair._Get_first()(_Mypair._Myval2)就是调用删除器删除了指针 我们现在就需要找到删除器的具体实现 这样才能到达下一个调用堆栈 注意到对于_Dx的注释/* = default_delete<_Ty> */ 我们猜想实现删除器的类名字应该就叫default_delete 但假如没有这个注释 大概就只能依靠直觉 或者ctrl+F搜索delete 慢慢找 struct default_delete { // default deleter for unique_ptr constexpr default_delete() noexcept = default; template <class _Ty2, enable_if_t<is_convertible_v<_Ty2*, _Ty*>, int> = 0> _CONSTEXPR23 default_delete(const default_delete<_Ty2>&) noexcept {} _CONSTEXPR23 void operator()(_Ty* _Ptr) const noexcept /* strengthened */ { // delete a pointer static_assert(0 < sizeof(_Ty), "can't delete an incomplete type"); delete _Ptr; } }; 注释中写到 这确实是unique_ptr的默认删除器 在operator()发生了delete 现在我们对调用堆栈的第3行查看源代码 这正是default_delete的operator() _CONSTEXPR23 void operator()(_Ty* _Ptr) const noexcept /* strengthened */ { // delete a pointer static_assert(0 < sizeof(_Ty), "can't delete an incomplete type"); delete _Ptr; } 当你写delete _Ptr 编译器会根据对象类型和上下文 选择合适的operator delete重载 从C++17开始 如果编译器知道对象的大小 (比如有类型信息) 它就会优先调用带size_t参数的operator delete(void, size_t) 而不是operator delete(void) 我们在使用 std::make_unique()分配对象时 编译器已经能确定Object的大小 所以在delete时会选择带有size的重载 对调用堆栈的第2行查看源代码 // 来自于delete_scalar_size.cpp _CRT_SECURITYCRITICAL_ATTRIBUTE void __CRTDECL operator delete(void* const block, size_t const) noexcept { operator delete(block); } 这个delete_scalar_size.cpp是一个很短的文件 是C++17 新增的重载 在这个含有size的operator delete内部 实际上还是调用了不含size的operator delete 所以它最终还是会调用我们在main.cpp重载的operator delete 这就是转发 调用堆栈的第1行 正是我们在main.cpp里自己重载的delete 至此 我们终于完成了一次delete 既然C++17的operator delete支持size_t参数 那么可以在我们的main.cpp里重载delete 增加对于size的输出 operator delete(void* memory, size_t size) { std::cout << "Deleting " << size << " bytes\n"; free(memory); } 现在再去查看调用栈 就没有调用delete_scalar_size.cpp的operator delete(void, size_t) 这是因为编译器优先调用了我们重载的这个operator delete(void, size_t)版本 struct AllocationMetrics { uint32_t TotalAllocated = 0; // 总共分配的内存 uint32_t TotalFreed = 0; // 总共释放的内存 uint32_t CurrentUsage() { return TotalAllocated - TotalFreed; } }; static AllocationMetrics s_AllocationMetrics; // 静态实例 void* operator new(size_t size) { s_AllocationMetrics.TotalAllocated += size; return malloc(size); } void operator delete(void* memory, size_t size) { s_AllocationMetrics.TotalFreed -= size; free(memory); } static void PrintMemoryUsage() { std::cout << "Memory Usage: " << s_AllocationMetrics.CurrentUsage() << " bytes\n"; } 现在你可以随时随地查看分配了多少内存 只需要调用PrintMemoryUsage();