设计实现C++内存的半自动释放

C++的一大优点就直接提供了内存的申请和使用功能,让程序员可以根据自己的需要,通过alloc系列函数或new运算符来申请使用内存,但是C++却不像java或C#那样,提供了垃圾的自动回收机制,我们申请的内存要由自己来管理、自己来释放,也就是说,C++把内存管理的责任完全交给了程序员。申请资源是简单的,在需要的时候申请就可以了,然而请神容易送神难,内存的释放却是一个非常让人头痛的问题。有的程序员忘记了释放申请的内存(堆中的内存),有的程序员对一块释放了的内存,再次释放等等,都引发了一系列的错误。

内存管理是C++最令人痛恨的问题,也是C++最有争议的问题,C++高手从中获得了更好的性能,更大的性能,C++菜鸟的收获则是一遍一遍的检查代码和对C++的痛恨,但内存管理在C++中无处不在。难道使用C++就不能像使用C#或java那样,不管内存的释放吗?其实我们可以通过适当的设计来减轻我们对内存的管理任务。虽然不能像C#或java那样,完全不管内存的使用和释放,但是也能在很大程度上减轻我们对内存的管理压力。下面就以本人设计的一个基类说明一下,如何实现C++内存的半自动释放。至于为什么说是半自动释放,看完这篇文章你就会知道了,现在可以不必深究。

一、设计思想
我们知道,在C++中,当一个对象被释放时,它的析构函数必定会被调用,而如果该对象的类型是一个子类,则其先调用自己的析构函数,再调用其父类的析构函数。所以我们可以利用这个特性设计一个基类Object,它的责任就是负责释放它的子类申请的资源。而释放的操作,我们可以放在析构函数中。

在基类Object中保存一个链表childern,链表childern中的值是其new出来的对象的指针,我把该对象叫做父对象,由该对象new出来的对象叫做子对象,例如:
A a;
a.createObj();
类A是基类Object的子类,对象a通过调用其成员函数createObj来创建了10个B类(也是Object的派生类)的对象,则a就是父对象,而那10个B类的对象就是子对象。当父对象析构时,就会调用Object的析构函数,从而会把它的所有子对象释放掉。也就是说,当a被释放时,就会调用Object类的析构函数,从而释放它new出来的10个子对象(B类的对象)。这样,就可以简化我们对内存的管理。

PS:这个实现机制是参考Qt的内存管理机制。

二、实现代码
ps: 完整的实现代码可以点击这里下载

Object的头文件(object.h)如下:
#ifndef OBJECT_H
#define OBJECT_H
#include <list>
using std::list;
class Object;
typedef list<Object*> ObjectList;
class Object
{
    public:
        virtual ~Object();
        //重新设置对象的父对象
        void setParent(Object *parent);
        //获得当前对象的子对象列表
        inline const ObjectList& getChildern() const;
        //获得当前对象的父对象
        inline const Object* getParent() const;
    protected:
        explicit Object(Object *parent = NULL);
        Object(const Object& obj);
        Object& operator= (const Object &obj);
    private:
        //把当前对象插入到parent指向的父对象的子对象列表中
        inline void _appendObjectList(Object *parent);
        ObjectList childern;//子对象列表
        Object *this_parent;//指向对象的父对象
};
void Object::_appendObjectList(Object *parent)
{
    /***
    函数功能:把当前对象插入到parent指向的父对象的子对象列表中
    返回:无
    ***/
    this_parent = parent;//设置当前对象的父对象
    //若其父对象不为空,则加入到父对象的子对象列表中
    if(this_parent != NULL)
        this_parent->childern.push_back(this);
}
const ObjectList& Object::getChildern() const
{
    return childern;
}
const Object* Object::getParent() const
{
    return this_parent;
}
#endif // OBJECT_H


Object的实现文件(object.cpp)如下:
#include "object.h"
#include <algorithm>
Object::Object(Object *parent)
    :this_parent(parent)
{
    /***
    函数功能:创建一个Object对象,若其parent不为空,
        则把当前对象插入到其子对象列表中
    ***/
    if(this_parent != NULL)
        this_parent->childern.push_back(this);
}
Object::Object(const Object& obj)
{
    /***
    函数功能:根据对象obj,复制一个对象
    ***/
    //复制时,只是把当前对象和obj的父对象设置为同一个父对象
    //并不复制obj的子对象列表
    _appendObjectList(obj.this_parent);
}
Object& Object::operator= (const Object &obj)
{
    /***
    函数功能:若当前对象无父对象,则把obj的父对象设置成当前对象的父对象
    返回:当前对象的引用
    ***/
    if(this_parent == NULL)
    {
        _appendObjectList(obj.this_parent);
    }
    return *this;
}
Object::~Object()
{
    /***
    函数功能:释放由该对象new出来的子对象
    ***/
    //若当前对象有父对象,则将当前对象从共父对象的子对象列表中删除
    if(this_parent != NULL)
    {
        ObjectList::iterator it =
            find(this_parent->childern.begin(),
                 this_parent->childern.end(),
                 this);
        this_parent->childern.erase(it);
// this_parent->childern.remove(this);
    }
    //释放其new出来的子对象
    while(!childern.empty())
    {
        ObjectList::iterator it(childern.begin());
        delete *it;
    }
}
void Object::setParent(Object *parent)
{
    /***
    函数功能:重新设置对象的父对象
    返回:无
    ***/
    //若当前对象有父对象,则把当前对象从其父对象的子对象列表中删除
    if(this_parent != NULL)
    {
        ObjectList::iterator it =
            find(this_parent->childern.begin(),
                 this_parent->childern.end(),
                 this);
        this_parent->childern.erase(it);
// this_parent->childern.remove(this);
    }
    //插入当前对象到parent对象的子对象列表中
    _appendObjectList(parent);
}


三、代码分析
1、为什么构造函数的访问权限都是protected
从上面的代码中,我们可以看到类Object的构造函数是protected的,为什么不是public而是protected的呢?因为我不知道如果不用它作为基类,则实例化一个这样的类有什么意义,所以我把它声明为protected。也就是说,它只在在实例化其子类对象时,才能被调用实例化基类的部分。

2、如何实现复制构造函数
如何实现复制构造函数是一个我想了很久的问题,这个实现的难点是究竟要不要复制子对象列表,从上面的代码中,可以看出我并没有去复制子对象列表。为什么呢?因为我觉得子对象列表是一个对象自己私有的部分(不是指访问权限),其他对象在根据当前对象复制一个对象,根本没有必要去复制它的子对象列表。从另一个角度来解释,就是子对象列表childern是用来管理内存,释放其自身new出来的对象的,并不是用来记录该对象的有用的信息的,所以不必要复制子对象列表。再者,我希望这个基类Object的存在,对我们平时类的编写的影响减少到最低,就像类Object好像并不存在一样,例如,如果没有Object,我们定义一个类A,当我们实现其复制构造函数时,只会关心类A的成员;而当类A是继承于Object时,我希望还应该如此,类A在实现自己的复制构造函数时,应该只关心类A的成员变量,而不关心别的类的成员。

3、如何定义赋值操作函数
基于上述实现复制构造函数的思想,这里的实现与上面的实现差不多,只不过复制构造函数是用来创建对象的,而赋值操作函数是对一个已存在的对象的状态进行修改。所以同样地,我也没有复制其子对象列表。而对一个已有父对象的对象改写其父对象并没有多大的意义,所以为了管理的方便,在调用赋值操作函数时,如果当前对象没有设置父对象,则把当前对象的父对象设置为右值对象的父对象,若当前对象已经有父对象,则什么也不做。

4、创建销毁对象的三种广式是否都应用
我们可用使用三种方式来创建对象,那么是否这三种对象都没有问题呢?现在我们假设类A、B继承于类Object,在类B中创建类A的对象,则创建类A的方式有三种,如下:
A a(this);
A *p = new A(this);
A a1(a);
其实这三种方式都是没有问题的,他们都会调用自己析构函数,进行相同的操作,只是调用的时机不同而已。

第一种情况,对象a存在于栈内存中,当类B的对象b还未销毁时,它已经被释放掉,从b的子对象列表中删除,所以不会存在delete一个栈中的对象的情况。

第二种情况,指针p指向的对象,会在b被销毁调用其析构函数时,从其子对象列表中找到其地址,并被delete。对于这种情况,如果我们主动地使用delete,如delete p;则会不会有问题呢?答案是没有问题的。因为无论是自己使用delete还是在父对象销毁析构时对其子对象列表中的所有对象进行delete,都是一样的,只是调用的时间不同而已,其操作是一样的。

第三种情况,与第一种情况相同,只是创建对象的方式不一样而已。

注:我们可以看到在析构函数中的while循环中,只有delete,没有从childern中删除结点,而且每次delete的都是childern中的第1个结点,这样是否有问题呢?当然是没有问题的。因为当delete一个子对象列表中的对象时,因为其也是类Object的子类对象,所以会去调用子对象的析构函数,从而又会去调用Object的析构函数,把子对象从其父对象的子对象列表中删除。所以真正的删除结点的操作是在子对象中完成的,而不是在父对象中。

在写代码和阅读代码时,一定要搞清楚,哪些操作是当前对象的操作,哪些对象是其父对象的操作,不然思路会很混乱。

5、使用方式
使用方式非常简单,只要把Object或其子类作为你要定义的类的基类,在类中new对象时,把当前类的地址(this)或其他Object类的子类地址作为参数,传入到构造函数中即可。若不传入参数,则默认没有父对象,则不能使用自动释放内存的功能。示例代码如下:
#include <iostream>
#include "Object.h"
using namespace std;
class Student:public Object
{
    public:
        Student(Object * parent = NULL)
            :Object(parent){++new_count;}
        ~Student()
        {
            ++delete_count;
        }
        static int new_count;
        static int delete_count;
    private:
        int stu_id;
};
int Student::new_count = 0;
int Student::delete_count = 0;
class Teacher:public Object
{
    public:
        void createStudent()
        {
            for(int i = 0; i < 10; ++i)
                new Student(this);
        }
    private:
};
int main()
{
    {
        Teacher t;
        t.createStudent();
    }
    cout << Student::new_count<<endl;
    cout << Student::delete_count<<endl;
    return 0;
}
运行结果如下:

从运行的结果来看,Student类被实例化了10次,其析构函数也被调用了10次,也就是说实现了内存自动释放的功能。而在我们的测试代码中,我们是没有delete过任何对象的,我们只是new了10个Student对象,并把当前对象(t)的地址传给Student的对象,作为其父对象。main函数中的花括号的作用,只是为了让Teacher类的实例t出了作用域,运行其析构函数,方便我们查看结果而已。

四、性能分析
因为我打算把这个类作为所有类的基类,所以这个类的内存管理的效率是什么重要的,因为它将影响整个程序的效率,所以我对其进行了测试,在newStudent中创建1024*1024个对象,然后释放,其平均时间为1.1秒左右。可见其效率还是相当高的。

我们知道list有一个成员函数remove,它的原型如下:
void remove (const value_type& val);
为什么我在实现中没有使用这个方便的函数,而使用泛型算法find,查找到该对象的指针所在链表中的迭代器,再使用list的成员函数earse呢?我们可以先来看看remove的说明,如下:
Removes from the container all the elements that compare equal to val. This calls the destructor of these objects and reduces the container size by the number of elements removed.
它的意思就是说,遍历整个容器,找出与val相等的结点,并删除。所以调用remove总是遍历整个链表。而从我们的使用中,我们可以知道,子对象列表中的值都是不一样的,这样总是遍历整个链表并没有意义,而且非常耗时,所以,我改成了上面代码所写的那样,使用泛型find和list的成员函数earse。find的操作是,找到第一个与val相等的结点,返回其迭代器。所以其效率自然高得多。

其实我一开始也是使用remove的,当创建和销毁1024*1024个对象时,需要大约60分钟,这个时间长得不可忍受。

五、缺陷
这个设计也是有一定的缺陷的,就是它把内存的释放的任务都交给了析构函数,如果析构函数不被执行,则会发生内存泄漏,而且由于程序员在使用了该基类Object后,对内存的使用可能更加无道,所以如果析构函数不被执行,则其内存泄漏的数量可能是相当大的。例如,如果main函数改为如下:
int main()
{
    {
        Teacher *t = new Teacher;
        t->createStudent();
    }
    cout << Student::new_count<<endl;
    cout << Student::delete_count<<endl;
    return 0;
}
其运行结果如下:

从运行的结果来看,创建的10个Student对象并没有被释放,原因是t把指向的new出来的Teacher对象并没有被释放(析构),其析构函数并没有执行。若想释放内存,则要自己写代码执行:delete t;

所以使用时,我们必须保证父对象被销毁,也就是说我们必须要保存父对象的析构函数要被执行,才能达到我们的内存自动释放的目标,所以这样的设计实现的内存自动释放只是半自动的。


郑重声明:本站内容如果来自互联网及其他传播媒体,其版权均属原媒体及文章作者所有。转载目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。