C++学习笔记 48 跟踪内存分配

内存是非常重要的东西 知道你的程序什么时候分配内存 特别是堆内存 是很有用的 如果知道程序在哪里分配内存 就有可能减少它 从而优化程序 也可以更好地了解程序是如何工作的

需要重写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();

posted @ 2026-01-02 11:10  超轶绝尘  阅读(6)  评论(0)    收藏  举报