C++ 巧用对象辅助内存回收

C++ 巧用对象辅助内存回收

1 引子

使用c++ 开发时候,肯定会写过在函数内申请多块内存,在退出函数时候释放这些内存这样的逻辑代码。如果函数内部有多个出口(return),若忘记释放,就会出现内存泄漏问题了。本文介绍巧用对象生命周期机制规避函数内容易遗漏释放内存问题。(本文较为基础,熟练c++编程的老手无需阅读本文)

2 事例

来个简单的事例代码,如下:

void test() {
     char *p1 = new char[100];
     strcpy(p1, "string1");
     printf("p1:%s\n", p1);
     if (strcmp(p1, "exit") == 0) { // 视意判断,可能是其他逻辑
        delete p1;
        return;
     }
 
     int *p2 = new int;
     *p2 = 1;
     printf("p2:%d\n", *p2);
     if (*p2 == 0) {// 视意判断,可能是其他逻辑
          delete p1;
          delete p2;
          return;
     }
 
    long *p3 = new long;
    *p3 = 100;
    printf("p3:%ld\n", *p3);
    if (*p3 == 0) {// 视意判断,可能是其他逻辑
         delete p1;
         delete p2;
         delete p3;
         return;
    } 
 
    // 省略其他会引用到p1,p2,p3 的code
 
    delete p1;
    delete p2;
    delete p3;
}

上述代码是个说明的例子,无需追究代码实际的意义性,它只是说明函数有多个出口要进行多处delete 而已。

可以看到,上述代码在每个出口都需进行对申请的内存进行释放。如果经过时间长后,代码修改演变,可能会增加/调整其他逻辑的内存申请,这样会比较容易漏掉对内存的释放。如何使得申请的内存,在函数返回前能够不忘记释放?除了编码格外小心外还有没有其他办法?有,可以利用局部对象生命周期的特点来规避这个问题。

3 利用对象生命周期的特点构造内存助手

使用过c++类的程序员都了解对象的生命周期的一些特点:
1)全局对象的生命周期是和程序一致;
2)函数内局部对象生命周期和该函数调用周期一致;
3)对象的创建,必然调用到其构造函数;
4)对象的结束必然会调用到其析构函数。

利用上面的特点,就可以构造一个内存助手来辅助管理内存了。在助手类的构造函数进行内存申请,在其析构函数进行释放。而这个内存助手在函数内使用时候,就可以达到了函数内自动申请/释放内存了,无需担心遗漏对内存的释放。设计代码如下:

// 头文件内
class CRecycleHelper
{
public:
    CRecycleHelper(DWORD cbSize);
    ~CRecycleHelper();
    void *GetBuffer();

protected:
    char *m_pBuffer;
};

// 源文件内
CRecycleHelper::CRecycleHelper(DWORD cbSize)
{
    m_pBuffer = NULL;
    if (cbSize > 0) {
        m_pBuffer = new char[cbSize];
        memset(m_pBuffer, 0, cbSize);
    }
}

CRecycleHelper::~CRecycleHelper()
{
    if (m_pBuffer != NULL) {
        delete []m_pBuffer;
        m_pBuffer = NULL;
    }
}

void *CRecycleHelper::GetBuffer()
{
    return m_pBuffer;
}

4 应用助手改造代码

使用上述内存助手,由助手对象进行内存申请和回收,这样写代码就省心多了,在使用该助手后,上面的测试代码就可以简化为下面样子了:

void test2() {
    CRecycleHelper helper1(100 * sizeof(char));
    char *p1 = (char *)helper1.GetBuffer();
    strcpy(p1, "string1");
    printf("p1:%s\n", p1);
    if (strcmp(p1, "exit") == 0) {// 视意判断,可能是其他逻辑
        return;
    }
 
    CRecycleHelper helper2(sizeof(int));
    int *p2 = (int *)helper2.GetBuffer();
    *p2 = 1;
    printf("p2:%d\n", *p2);
    if (*p2 == 0) {// 视意判断,可能是其他逻辑
        return;
    }
 
    CRecycleHelper helper3(sizeof(long));
    long *p3 = (long *)helper3.GetBuffer();
    *p3 = 100;
    printf("p3:%ld\n", *p3);
    if (*p3 == 0) {// 视意判断,可能是其他逻辑
        return;
    } 
 
    // 省略其他会引用到p1,p2,p3 的code
}

上面调整后的代码,可以看到函数内没有相应的函数申请和释放了,全部都在助手对象做了处理。如果函数提前退出时候,再也不用担心内存没有释放问题了,而且代码也清爽了很多。使用这个助手进行内存辅助回收,在函数内变量越多/函数逻辑越多时候,优越性就越体现出来了。

5 相关的其他设计

利用局部变量的这些特点,可以写出其他很好玩的工具类。

比如在多线程工程开发时候,访问全局资源时候需要对某个临界区锁进行上锁,在函数退出时候对临界区锁解锁,如果函数出口很多,解锁的地方就很多了,如果忘记解锁是个比较严重问题的。

如果设计一个类似的助手类,在构造函数传入临界区锁进行上锁,在其析构函数时候,对临界区锁进行解锁。这样就不用担心中场退出而忘记解锁问题了。

发表评论

电子邮件地址不会被公开。 必填项已用*标注