設計實現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;

所以使用時,我們必須保證父對象被銷燬,也就是說我們必須要保存父對象的析構函數要被執行,才能達到我們的內存自動釋放的目標,所以這樣的設計實現的內存自動釋放只是半自動的。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章