前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >C++编写代码跟踪内存分配的简单方法

C++编写代码跟踪内存分配的简单方法

原创
作者头像
晨星成焰
修改2024-04-29 19:37:13
2770
修改2024-04-29 19:37:13
举报

为什么要跟踪内存分配?

关于内存的事情是很重要的,计算机和内存是紧密相连的,如果你只有一个cpu,而没有ram没有内存就什么都做不了。

而在C++中跟踪内存分配的重要性主要体现在以下几个方面:

避免内存泄漏:

C++中的动态内存分配(通过new和delete操作符)需要程序员手动管理内存。如果不正确地释放已分配的内存,可能会导致内存泄漏,尤其是在长时间运行的程序中。内存泄漏会随着时间的推移而累积,最终可能导致程序崩溃或系统资源耗尽。

优化内存使用:

例如在嵌入式系统中,内存资源通常有限。频繁的动态内存分配和释放可能会导致堆碎片化,从而影响程序的性能和稳定性。通过跟踪内存分配,可以更好地理解内存使用模式,从而优化内存管理策略,例如合理使用内存池或者预分配内存等。

提高程序性能:

跟踪内存分配可以找出不必要的内存分配和释放,从而减少不必要的开销。例如,如果发现某个对象频繁地分配和释放内存,可能是因为该对象的生命周期管理不当,通过优化其生命周期管理,可以提高程序的性能。

保证程序稳定性:

在复杂的软件系统中,内存管理错误可能会导致程序崩溃或者未定义的行为。通过跟踪内存分配,可以及时发现和修复这些问题,从而提高程序的稳定性和可靠性。

总之知道程序什么时候分配内存,特别是堆内存,因为堆上分配代码并不是最好的做法,尤其是性能关键的代码中。除此之外看到内存被分配到哪里,还可以更好的理解程序是如何工作的,即使这个程序的是你写的。

另外该文章中探讨,展示的所有东西,都可以很容易的插入到你现有的应用程序中!

最简单的演示例

代码语言:cpp
复制
#include <iostream>

struct Object {
  	int x, y, z;  
};

int main() {
    Object a;	//栈分配
    Object *b = new Object;		//堆分配
}

这篇文章的重点就是如何检测堆分配或栈分配,方法就是重写new运算符。

new操作符的new关键字实际上是一个函数,它被调用时带有特定的大小,可能还有其他参数。

现在我们

加入重载new

代码语言:cpp
复制
#include <iostream>

void *operator new(size_t size) {
    std::cout << "堆分配内存:"<< size << "bytes\n";
	return malloc(size);	//分配特定数量的内存并返回一个指向该内存的指针
}

struct Object {
  	int x, y, z;  
};

int main() {
    Object a;	//栈分配
    Object *b = new Object;		//堆分配
}
  • 通过这段额外的重载new代码:将不使用标准库中的new操作符,连接器实际上会链接到这个函数中。
  • 这个函数是返回一个void指针,它只是一个内存地址,因为不想影响程序的行为,便简单输入return malloc(size)

这里重写的好处有很多

  • 可以在重载的new函数中设置一个断点,则程序会在堆分配的地方停下来,便于查找程序中堆分配的语句,从而更好的去优化它们!
  • 也可以在其中输出一点东西来计数

现在运行一下程序

追踪堆分配

运行程序
运行程序

可以很明显的看出该程序在return处停住了,并且通过调用堆栈这个visual提供的窗口点击告诉了我们堆分配来自于何处。

当然这个例子是非常明显的,如果我们加入一个字符串呢?

加入字符串

代码语言:cpp
复制
#include <iostream>

void* operator new(size_t size) 
{
    std::cout << "堆分配内存:" << size << "bytes\n";
    
    return malloc(size);	//分配特定数量的内存并返回一个指向该内存的指针
}

struct Object {
    int x, y, z;
};

int main() {

    std::string lcc = "lcc";

    Object a;	//栈分配
    Object* b = new Object;		//堆分配
}

我们有一个很小的字符串,它不会在堆里分配内存来存储这些字符,但在调试模式下,仍然会分配一些内存给它

追踪一下内存分配

内存分配追踪
内存分配追踪
调用堆栈的追踪
调用堆栈的追踪

当然这并不是百分百体验其作用,如果使用智能指针,而不是显式调用new呢?

加入智能指针

代码语言:cpp
复制
#include <iostream>
#include <memory>

void* operator new(size_t size) 
{
    std::cout << "堆分配内存:" << size << "bytes\n";
    
    return malloc(size);	//分配特定数量的内存并返回一个指向该内存的指针
}

struct Object {
    int x, y, z;
};

int main() {

    std::unique_ptr<Object> obj = std::make_unique<Object>();

    std::string lcc = "Cherno";

    Object a;	//栈分配
    Object* b = new Object;		//堆分配

    return 0;
}

显然智能指针仍然会分配内存,但我们可以看到这发生在make_unique内部,因为unique会调用new分配内存

希望通过这些简单的使用例,你可以看到在重载的new函数中插入一个断点,并精确地追踪这些内存分配来源的方法。提高内存利用的方法我就不细讲了,内存池或者一个不断调整大小的vector,或者使用一些不怎么分配内存的东西都是解决办法。

同理,delete也可以重载

现在我们加入

delete的重载

代码语言:cpp
复制
#include <iostream>
#include <memory>

void* operator new(size_t size) 
{
    std::cout << "堆分配内存:" << size << "bytes\n";
    
    return malloc(size);	//分配特定数量的内存并返回一个指向该内存的指针
}

void operator delete(void* memory, size_t size) {
    std::cout << "释放了" << size << "bytes\n";
    free(memory);
}

struct Object {
    int x, y, z;
};

int main() {
    {
        std::unique_ptr<Object> obj = std::make_unique<Object>();
    }
    std::string lcc = "Cherno";

    Object a;	//栈分配
    Object* b = new Object;		//堆分配

    return 0;
}

在free处放一个断点,把unique_ptr放到一个小的作用域内,你可以看到重载的delete被调用,在main函数中的unique_ptr被销毁之后

堆栈追踪
堆栈追踪

实际上是这个unique_ptr的析构函数,它实际删除了底层的原始指针

另外通过下面这张运行截图你可以发现,我们少释放了

Object* b = new Object; //堆分配

这是因为缺少delete b;导致的,可以看出追踪内存分配的重要性。

运行截图
运行截图

关于动态申请的数组

这里的 new delete对动态申请的数组没有作用

这是因为C++中的动态数组分配是通过new[]操作符完成的,而释放则是通过delete[]操作符。因此,需要为这两个操作符提供重载版本。

代码语言:cpp
复制
// 重载的new[]和delete[]操作符
void* operator new[](size_t size) 
{
    std::cout << "堆分配数组内存:" << size << "bytes\n";
    return malloc(size);
}

void operator delete[](void* memory, size_t size) {
    std::cout << "释放了数组内存:" << size << "bytes\n";
    free(memory);
}

如果有检查动态申请数组的需求加入这两段就好了

内存分配追踪器

而现在利用这两个函数,便可以创建简单的内存分配跟踪器了,可以知道有多少内存被使用,分配,释放等等。

代码语言:cpp
复制
struct MemoryTracker {
    uint32_t TotalMemory = 0;	//总分配内存
    uint32_t TotalFreed = 0;	//总释放内存

    uint32_t CountMemory() { return TotalMemory - TotalFreed; } 	//写一个小函数来输出 当前用了多少内存
};
static MemoryTracker temp;	//创建一个静态实例,全局的!

void* operator new(size_t size) {
    temp.TotalMemory += size;	//?在每一个new里计算总共分配了多少内存

    return malloc(size);
}

void operator delete(void* memory, size_t size) {
    temp.TotalFreed += size;

    free(memory);
}

void* operator new[](size_t size)
{
        temp.TotalMemory += size;
        return malloc(size);
}

void operator delete[](void* memory, size_t size) 
{
    temp.TotalFreed += size;
    free(memory);
}

//可以用一个函数输出我们的内存使用情况
static void PrintMemoryUsage() {
    std::cout << "内存使用了:" << temp.CountMemory() << "\n";
}

内存分配器使用例

代码语言:cpp
复制
#include <iostream>
#include <memory>

struct MemoryTracker {
    uint32_t TotalMemory = 0;	//总分配内存
    uint32_t TotalFreed = 0;	//总释放内存

    uint32_t CountMemory() { return TotalMemory - TotalFreed; } 	//写一个小函数来输出 当前用了多少内存
};
static MemoryTracker temp;	//创建一个静态实例,全局的!

void* operator new(size_t size) {
    temp.TotalMemory += size;	//?在每一个new里计算总共分配了多少内存

    return malloc(size);
}

void operator delete(void* memory, size_t size) {
    temp.TotalFreed += size;

    free(memory);
}

void* operator new[](size_t size)
{
        temp.TotalMemory += size;
        return malloc(size);
}

void operator delete[](void* memory, size_t size) 
{
    temp.TotalFreed += size;
    free(memory);
}

//可以用一个函数输出我们的内存使用情况
static void PrintMemoryUsage() {
    std::cout << "内存使用了:" << temp.CountMemory() << "\n";
}


struct Object {
    int x, y, z;
};

int main() {
    PrintMemoryUsage();
    {
        std::unique_ptr<Object> obj = std::make_unique<Object>();
        PrintMemoryUsage();
    }
    PrintMemoryUsage();
    {
        std::string lcc = "Cherno";
        PrintMemoryUsage();
    }
    //作用域结束时,lcc对象将自动被销毁,其内存也将被自动释放。
    PrintMemoryUsage();//输出0
    Object a;	//栈分配
    Object* b = new Object;		//堆分配

    PrintMemoryUsage();
    delete b;
    PrintMemoryUsage();//释放了输出0
    return 0;
}
运行结果
运行结果

至此结束

总结

如果觉得这很有用,可以放在自己的程序里测试一下效果如何,当然也可以使用工具来解决这个,而不是使用代码,例如可以使用vs内置的内存分配跟踪分析工具外面有很多现成可用的工具,但是就个人而言这是一个快速简单的方法,有时会更有效XD

参考例

  1. Track MEMORY ALLOCATIONS the Easy Way in C++
  2. 跟踪内存分析的简单方法

我正在参与2024腾讯技术创作特训营最新征文,快来和我瓜分大奖!

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 为什么要跟踪内存分配?
    • 避免内存泄漏:
      • 优化内存使用:
        • 提高程序性能:
          • 保证程序稳定性:
            • 最简单的演示例
              • 加入重载new
              • 追踪堆分配
              • 加入字符串
              • 加入智能指针
              • delete的重载
              • 关于动态申请的数组
            • 内存分配追踪器
              • 内存分配器使用例
              • 参考例
          • 总结
          领券
          问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档
          http://www.vxiaotou.com