C++ 实现内存泄露检查

36 篇文章 0 订阅

        内存泄漏一直是 C++ 中比较令人头大的问题, 即便是很有经验的 C++程序员有时候也难免因为疏忽而写出导致内存泄漏的代码。除了基本的申请过的内存未释放外,还存在诸如异常分支导致的内存泄漏等等。本项目将使用 C++ 实现一个内存泄漏检查器。

要检测一个长期运行的程序是否发生内存泄露通常是在应用中设置检查点,分析不同检查点中内存是否长期保持占用而未释放,从本质上来说,这与对一个短期运行的程序进行内存泄露检查是非常类似的。所以本项目中的内存泄漏检查器将实现一个用于短期内存泄露的检查器。

我们不妨编写下面的测试代码用于检测我们的内存泄露,在这个代码中,我们刻意的不去释放某个申请的内存,并刻意的去制造一个异常分支产生的内存泄露,在 /home/shiyanlou/ 目录下新建 LeakDetector 文件夹,并在文件夹下新建 main.cpp 文件:

//
//  main.cpp
//  LeakDetector
//
#include <iostream>

// 在这里实现内存泄露检查
#include "LeakDetector.hpp"

// 测试异常分支泄露
class Err {
public:
    Err(int n) {
        if(n == 0) throw 1000;
        data = new int[n];
    }
    ~Err() {
        delete[] data;
    }
private:
    int *data;
};

int main() {
    
    // 忘记释放指针 b 申请的内存,从而导致内存泄露
    int *a = new int;
    int *b = new int;
    delete a;
    
    // 0 作为参数传递给构造函数将发生异常,从而导致异常分支的内存泄露
    try {
        Err* e = new Err(0);
        delete e;
    } catch (int &ex) {
        std::cout << "Exception catch: " << ex << std::endl;
    };
    return 0;
    
}

内存泄露检查器的设计

要实现内存泄露的检查,我们可以从下面几个点来思考:

  1. 内存泄露产生于 new 操作进行后没有执行 delete
  2. 最先被创建的对象,其析构函数永远是最后执行的

对应这两点,我们可以做下面的操作:

  1. 重载 new 运算符
  2. 创建一个静态对象,用于在原始程序退出时候才调用这个静态对象的析构函数

 这样两个步骤的好处在于:无需修改原始代码的情况下,就能进行内存检查。这同时也是我们希望看到的。所以,我们可以在 LeakDetector/LeakDetector.hpp 里首先实现:

//
//  LeakDetector.hpp
//  LeakDetector
//
#ifndef __LEAK_DETECTOR__
#define __LEAK_DETECTOR__

void* operator new(size_t _size, char *_file, unsigned int _line);
void* operator new[](size_t _size, char *_file, unsigned int _line);
// 此处宏的作用下一节实现 LeakDetector.cpp 时说明
#ifndef __NEW_OVERLOAD_IMPLEMENTATION__
#define new    new(__FILE__, __LINE__)
#endif

class _leak_detector
{
public:
    static unsigned int callCount;
    _leak_detector() noexcept {
        ++callCount;
    }
    ~_leak_detector() noexcept {
        if (--callCount == 0)
            LeakDetector();
    }
private:
    static unsigned int LeakDetector() noexcept;
};
static _leak_detector _exit_counter;
#endif

为什么要设计 callCountcallCount 保证了我们的 LeakDetector 只调用了一次。考虑下面的简化代码:

// main.cpp
#include <iostream>
#include "test.h"
int main() {
    return 0;
}

// test.hpp
#include <iostream>
class Test {
public:
    static unsigned int count;
    Test() {
        ++count;
        std::cout << count << ", ";
    }
};
static Test test;

// test.cpp
#include "test.hpp"
unsigned int Test::count = 0;

下面我们来逐步实现这个内存泄露检查器。

既然我们已经重载了 new 操作符,那么我们很自然就能想到通过手动管理内存申请和释放,如果我们 delete 时没有将申请的内存全部释放完毕,那么一定发生了内存泄露。接下来一个问题就是,使用什么结构来实现手动管理内存?

不妨使用双向链表来实现内存泄露检查。原因在于,对于内存检查器来说,并不知道实际代码在什么时候会需要申请内存空间,所以使用线性表并不够合理,一个动态的结构(链表)是非常便捷的。而我们在删除内存检查器中的对象时,需要更新整个结构,对于单向链表来说,也是不够便利的。

为此,我们可以继续实现这样一个结构,并创建好 LeakDetector/LeakDetector.cpp 文件:

//
//  LeakDetector.cpp
//  LeakDetector
//
#include <iostream>
#include <cstring>

// 在此处定义 _DEBUG_NEW_ 宏, 
// 从而在这个实现文件中不再继续重载 new 运算符, 
// 从而防止编译冲突
#define __NEW_OVERLOAD_IMPLEMENTATION__
#include "LeakDetector.hpp"

typedef struct _MemoryList {
    struct  _MemoryList *next, *prev;
    size_t     size;       // 申请内存的大小
    bool    isArray;    // 是否申请了数组
    char    *file;      // 存储所在文件
    unsigned int line;  // 保存所在行
} _MemoryList;
static unsigned long _memory_allocated = 0;     // 保存未释放的内存大小
static _MemoryList _root = {
    &_root, &_root, // 第一个元素的前向后向指针均指向自己
    0, false,               // 其申请的内存大小为 0, 且不是数组
    NULL, 0                 // 文件指针为空, 行号为0
};

unsigned int _leak_detector::callCount = 0;

现在我们来使用这个结构来管理内存分配:

//
//  LeakDetector.cpp
//  LeakDetector
//
// 从 _MemoryList 头部开始分配内存
void* AllocateMemory(size_t _size, bool _array, char *_file, unsigned _line) {
    // 计算新的大小
    size_t newSize = sizeof(_MemoryList) + _size;
    
    // 由于 new 已经被重载,我们只能使用 malloc 来分配内存
    _MemoryList *newElem = (_MemoryList*)malloc(newSize);
    
    newElem->next = _root.next;
    newElem->prev = &_root;
    newElem->size = _size;
    newElem->isArray = _array;
    newElem->file = NULL;
    
    // 如果有文件信息,则保存下来
    if (_file) {
        newElem->file = (char *)malloc(strlen(_file)+1);
        strcpy(newElem->file, _file);
    }
    // 保存行号
    newElem->line = _line;
    
    // 更新列表
    _root.next->prev = newElem;
    _root.next = newElem;
    
    // 记录到未释放内存数中
    _memory_allocated += _size;
    
    // 返回申请的内存,将 newElem 强转为 char* 来严格控制指针每次 +1 只移动一个 byte
    return (char*)newElem + sizeof(_MemoryList);
}

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Thomas_Lbw

欣赏我就赏赐给我吧

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值