c++中巧用对象辅助内存回收

165 阅读4分钟
原文链接: blog.yuccn.net

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;
    }

    char *p2 = new char[100];
    strcpy(p2, "string2");
    printf("p2:%s\n", p2);
    if (strcmp(p2, "exit") == 0) {
        delete p1;
        delete p2;
        return;
    }

    char *p3 = new char[100];
    strcpy(p3, "string3");
    printf("p3:%s\n", p3);
    if (strcmp(p3, "exit") == 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_lpBuffer = 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);
    char *p1 = (char *)helper1.GetBuffer();
    strcpy(p1, "string1");
    printf("p1:%s\n", p1);
    if (strcmp(p1, "exit") == 0) {
        return;
    }

    CRecycleHelper helper2(100);
    char *p2 = (char *)helper2.GetBuffer();
    strcpy(p2, "string2");
    printf("p2:%s\n", p2);
    if (strcmp(p2, "exit") == 0) {
        return;
    }

    CRecycleHelper helper3(100);
    char *p3 = (char *)helper3.GetBuffer();
    strcpy(p3, "string3");
    printf("p3:%s\n", p3);
    if (strcmp(p3, "exit") == 0) {
        return;
    }

    // 其他会应用到p1,p2,p3 到code
}

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

5 相关的其他设计

利用局部变量的这些特点,也可以写出其他很好玩的工具类。比如在多线程工程开发时候,访问全局资源时候需要对某个临界区锁进行上锁,在函数退出时候对临界区锁解锁,如果函数出口很多,那么解锁的地方就很多了,这个如果忘记解锁是个比较严重问题的。如果设计一个类似的助手类,在构造函数传入临界区锁进行上锁,在其析构函数时候,对临界区锁进行解锁。这样就不用担心中场退出而忘记解锁问题了。