使用C++为对象分配与释放内存时的几个好习惯

本文为大便一箩筐的原创内容,转载请注明出处,谢谢:http://www.cnblogs.com/dbylk/


最近在为公司的项目写内存泄漏定位工具,遇到一些关于C++构造与析构对象的问题,在此记录一下。




一、不要混用 new/delete 和 new[]/delete[]

在默认情况下,也就是不存在 operator new 的重载时,new一个自定义类型 ClassA 的对象时,C++ 会先调用 malloc 来申请一块 sizeof(ClassA) 大小的内存(操作系统会记录这块内存的首地址与大小),然后调用 ClassA 的构造函数在这块内存上初始化对象。此时,new 关键字会返回 malloc 得到的地址。调用delete时,会首先执行 ClassA 的析构函数,再调用 free 释放 malloc 得到的指针。


而new[]则稍微复杂一点,当你调用 new ClassA[nCount] 申请一个对象个数为 nCount 的 ClassA 数组时,编译器(MSVC)会调用 malloc 申请一块大小为 sizeof(ClassA) * nCount + 4 的内存,多出来的 4 bytes 被放在 new[] 关键字返回地址 ptr 的前面,其中记录了数组中元素的个数。当调用 delete[] 删除数组时,会根据数组首地址前 4 bytes 中记录元素的个数来依次调用数组中对象的析构函数(每次指针偏移 sizeof(ClassA) 大小),再调用 free 释放指针 (ptr - 4)。


因此,混用 new/delete 和 new[]/delete[] 通常会导致内存访问崩溃。然后这里用了“通常”,也就是说在某些特定情况下,混用 new/delete 和 new[]/delete[] 是不会有任何影响的:

  1. 创建和释放 C++ 的内建(build-in)类型时,即 int、char等。
  2. 创建和释放“自身和所有成员变量都不含自定义构造函数和析构函数”的类型。(这一条可能依赖于编译器的实现,至少在 MSVC 中此情况成立)

然而当项目代码一旦复杂起来,要分清什么时候上面两个条件能够成立就不是那么轻松的事了,因此最好的方法就是无论何时何地都不要混用 new/delete 和 new[]/delete[]。


二、不要 delete “void” 指针

在整理公司项目代码的过程中,发现有很多地方出现了类似于下面形式的代码:

// Author :大便一箩筐 2016-04-03

struct StructA
{
    char cData;
}

void* pBuffer = new StructA[nSize];
// do something...
delete pBuffer;

可能写过类似代码的同学会觉得这种写法并没有什么问题,事实也是如此,它能够正常工作,既不会产生内存泄漏,也不会运行报错。

但是,上面情况只能说是一种幸运的巧合,如果发生一些微小的改变,结果就会发生意想不到的变化:

// Author :大便一箩筐 2016-04-03

struct StructA
{
    string strData;
}

void* pBuffer = new StructA[nSize];
// do something...
delete pBuffer;

细心的同学可能已经看出来了,由于 pBuffer 是 void 指针,delete pBuffer 时,并不会调用 StructA 的析构函数,而这导致了 string 的析构函数也没有被调用,最终产生的结果就是 string 中的字符串缓冲泄漏。
有的同学可能会说,C++ 不是支持多态嘛,我把 StructA 的析构函数定义成虚函数不就好了。然而不幸的是,作为 C++ 的内建类型,void 并没有定义析构函数,因此寄希望于多态是行不通的。
还有的同学可能会说,如果想定义 void 类型的内存缓冲区怎么办?    —— 别忘记我们还有 malloc 和 free。
所以,任何时候都不要尝试 delete void 指针。


三、尽量不要手动调用析构函数

看下面的代码,你能看出程序输出结果是什么吗?

// Author :大便一箩筐 2016-03-31

class Base
{
public:
    Base() {};
    virtual ~Base()
    {
        cout << "Base has been destructed. " << endl;
    }

    void Release()
    {
        this->~Base();
    }

    int baseData;
};

class Derive : public Base
{
public:
    Derive() {};
    virtual ~Derive()
    {
        cout << "Derive has been destructed. " << endl;
    }

    void Releaase()
    {
        this->~Derive();
    }

    int deriveData;
};

int main()
{
    Base* pObject = new Derive();

    pObject->Release();
    delete pObject;

    system("pause");

    return 0;
}

我想很多同学会觉得答案是这个:

Derive has been destructed.
Base has been destructed.
Derive has been destructed.
Base has been destructed.

然而很不幸的,正确答案是这个:

Derive has been destructed.
Base has been destructed.
Base has been destructed.

因为在手动调用 pObject 的析构函数时,虽然 pObject 所指向的内存空间并没有被释放,但执行完 Derive 的析构函数后, pObject 所指向对象的虚函数表指针会从指向派生类 Derive 的虚函数表恢复为指向基类 Base 的虚函数表。
即手动调用析构函数之后,多态指针 pObject 失去了自己的多态特性,此时无法再通过 pObject 直接调用派生类中的虚函数。因此,最后 delete 时只会调用基类的析构函数。

所以,尽量不要在自己写的函数中手动调用析构函数是一个好习惯。

然而上面提到了“尽量”,那就是说事情并没有那么绝对,C++ 支持手动调用析构函数,自然有它的道理:当你需要自己写内存管理器时,手动调用析构函数是必须的。

posted @ 2016-03-31 17:33  大便一箩筐  阅读(13005)  评论(0编辑  收藏  举报