2013-09-26 95 views
1

比方說,我有以下代碼:如何清理資源

class BaseMember 
{ 
}; 

class DerivedMember : public BaseMember 
{ 
}; 

class Base 
{ 
private: 
    BaseMember* mpMember; 
protected: 
    virtual BaseMember* initializeMember(void) 
    { 
     return new BaseMember[1]; 
    } 

    virtual void cleanupMember(BaseMember* pMember) 
    { 
     delete[] pMember; 
    } 
public: 
    Base(void) 
     : mpMember(NULL) 
    { 
    } 

    virtual ~Base(void) 
    { 
     cleanupMember(mpMember); 
    } 

    BaseMember* getMember(void) 
    { 
     if(!mpMember) 
      mpMember = initializeMember(); 
     return mpMember; 
    } 
}; 

class Derived : public Base 
{ 
protected: 
    virtual BaseMember* initializeMember(void) 
    { 
     return new DerivedMember; 
    } 

    virtual void cleanupMember(BaseMember* pMember) 
    { 
     delete pMember; 
    } 
}; 

基地和BaseMember是API的一部分,並可以通過API的用戶被繼承,就像它通過示例代碼中的Derived和DerivedMember完成的一樣。

Base通過調用它的虛擬工廠函數initializeMember()來初始化mpBaseMember,以便派生類可以重寫工廠函數以返回DerivedMember實例,而不是BaseMember實例。

但是,當從基類構造函數中調用虛函數時,調用基本實現而不是派生類覆蓋。 因此,我正在等待mpMember的初始化,直到它第一次被訪問(這當然意味着基類和任何派生類,可能會進一步得到它本身,不允許從內部訪問該成員構造函數)。

現在的問題是:調用基本析構函數中的虛擬成員函數將導致該函數的基類實現調用,而不是派生類覆蓋。 這意味着我不能簡單地從基類析構函數中調用cleanupMember(),因爲它會將其稱爲基類實現,它可能無法正確地清理東西,initializeMember()的派生實現已初始化。 例如,基類和派生類可以使用不兼容的分配器,這些分配器在混合時可能會導致未定義的行爲(如在示例代碼中 - 派生類通過new分配成員,但基類使用delete []來釋放它)。

所以我的問題是,我該如何解決這個問題? 我想到的是: a)API的用戶必須在Derived實例被破壞之前顯式調用某些清理函數。這可能會被遺忘。 b)(大部分)派生類的析構函數必須調用清理函數來清理初始化由基類觸發的東西。由於所有權責任被混淆,基本類觸發器分配,但派生類必須觸發釋放,這是非常不直觀的,並且派生類的作者無法知道,除非他讀取API文檔足以找到這些信息。 我真的希望以比使用用戶內存或他的可靠性來徹底閱讀文檔更加不可靠的方式來做到這一點。

有沒有其他方法?

注意:由於派生類可能不存在於基類的編譯時,所以靜態多態不是此處的選項。

+0

工作解決方案值得一提的明確規定,你不能調用基類的析構函數派生類的功能派生類將已經被破壞。 – Bathsheba

+0

您的實施違反了「資源獲取初始化」習慣用法。我寧願問一下在基本或派生構造函數中如何分配內存(+1)。 – cpp

+1

智能指針有什麼問題? – doctorlove

回答

0

https://stackoverflow.com/a/19033431/404734通過思想的啓發我想出了:-)

class BaseMember 
{ 
}; 

class DerivedMember : public BaseMember 
{ 
}; 

class BaseMemberFactory 
{ 
public: 
    virtual ~BaseMemberFactory(void); 

    virtual BaseMember* createMember(void) 
    { 
     return new BaseMember[1]; 
    } 

    virtual void destroyMember(BaseMember* pMember) 
    { 
     delete[] pMember; 
    } 
}; 

class DerivedMemberFactory : public BaseMemberFactory 
{ 
    virtual BaseMember* createMember(void) 
    { 
     return new DerivedMember; 
    } 

    virtual void destroyMember(BaseMember* pMember) 
    { 
     delete pMember; 
    } 
}; 

class Base 
{ 
private: 
    BaseMemberFactory* mpMemberFactory; 
    BaseMember* mpMember; 
protected: 
    virtual BaseMemberFactory* getMemberFactory(void) 
    { 
     static BaseMemberFactory fac; 
     return &fac; 
    } 
public: 
    Base(void) 
     : mpMember(NULL) 
    { 
    } 

    virtual ~Base(void) 
    { 
     mpMemberFactory->destroyMember(mpMember); 
    } 

    BaseMember* getMember(void) 
    { 
     if(!mpMember) 
     { 
      mpMemberFactory = getMemberFactory(); 
      mpMember = mpMemberFactory->createMember(); 
     } 
     return mpMember; 
    } 
}; 

class Derived : public Base 
{ 
protected: 
    virtual BaseMemberFactory* getMemberFactory(void) 
    { 
     static DerivedMemberFactory fac; 
     return &fac; 
    } 
}; 
0

永遠不要永遠不會在構造函數/析構函數中調用虛擬方法,因爲它會產生奇怪的結果(編譯器會讓你看不到的黑暗和奇怪的東西)。

析構函數調用順序是孩子,然後父

你可以這樣做(但有probalby一個更好的方法):

private: 
    // private destructor for prevent of manual "delete" 
    ~Base() {} 

public: 
    // call this instead use a manual "delete" 
    virtual void deleteMe() 
    { 
     cleanupMember(mpMember); 
     delete this; // commit suicide 
    } 

更多的相關信息自殺: https://stackoverflow.com/a/3150965/1529139http://www.parashift.com/c++-faq-lite/delete-this.html

PS:爲什麼析構函數是虛擬的?

+0

「永遠不會永遠不會調用構造函數/析構函數中的虛擬方法,因爲它會產生奇怪的結果」 這是非常清楚的。否則我首先不會問這個問題,因爲整個問題是「我知道,我不能在構造函數和析構函數中調用這些虛函數,我可以很容易地避免構造函數中的調用,但是如何避免析構函數的調用,並保證所有資源都被釋放?「 – Kaiserludi

+0

「(破壞者是虛擬的!?)」 是的。你的問題的意圖是什麼? – Kaiserludi

+0

這是一個普遍的問題,我會寫「?」相反「!?」 ;) – 56ka

1

如何修改包含清理方法的工廠模式?意思是,添加一個像memberFactory這樣的屬性,這是一個提供創建,清理以及訪問成員的類的實例。虛擬初始化方法將提供並初始化正確的工廠,析構函數~Base將調用工廠的清理方法並將其銷燬。

(當然,這是從工廠模式很遠......也許這是另一個名字知道的?)

+0

並在哪裏清理工廠本身? – Kaiserludi

+0

我有更多的想法:我不需要爲每個類實例創建一個工廠實例,而只需要爲整個類創建一個工廠實例,所以讓Factory可以返回靜態函數成員的地址,而且我不必動態分配工廠,因此我不必擔心如何清理它。 我剛剛試圖實現你的想法,它似乎工作得很好(請參閱我自己的實現答案)。 – Kaiserludi

+0

是的,這幾乎是我的想法,再加上靜態實例的巧妙技巧。我的想法是,desctructor'〜Base'只會做'刪除mpMemberFactory'(這裏沒有虛擬方法的問題)。 – volferine

0

首先,用C開發時必須使用RAII成語++。你必須必須解析器中釋放你所有的資源,當然如果你不想與內存泄漏作鬥爭。

你可以創建一些cleanupMember()函數,但是你應該在析構函數中檢查你的資源,並且如果它們沒有被刪除(因爲cleanupMember永遠不會被調用,例如由於例外)而釋放它們。因此,請將析構函數添加到您的派生類中:

virtual ~Derived() 
{ 
    Derived::cleanupMember(mpMember); 
} 

並管理類本身中的成員指針。

我也建議你在這裏使用智能指針。

+0

問題是:如果在基類析構函數中檢測到cleanupMember()尚未被調用,那麼已經晚於做派生類的清理工作。 「我也建議你在這裏使用智能指針。」 他們在這裏會有什麼不同? 作爲基類成員的智能指針會在基類析構函數被調用時釋放其有效負載,但是由於已經調用了派生類構造函數,因此我已經遲到了,無法調用虛函數的派生實現。 – Kaiserludi

+0

同樣,你不能從'〜Base'調用任何虛函數。它必須在RAII規則中工作,所以當你進入'〜Base'時,'〜Derived'已經被調用,並且資源被釋放。 智能指針只是給你一個晚上睡覺的機會,而不是調試內存泄漏。 –

0

mpMember受保護,讓它在派生類構造函數中初始化並在派生析構函數中釋放。

1

如果你真的想要做這樣的事情,你可以做這樣的:

class Base { 
    BaseMember* mpMember; 

    protected: 
    Base(BaseMember *m) : mpMember(m) {} 

    virtual void doCleanupMember(BaseMember *m) { delete [] m; } 

    void cleanupMember() { 
     // This gets called by every destructor and we only want 
     // the first call to do anything. Hopefully this all gets inlined. 
     if (mpMember) { 
     doCleanupMember(pmMember); 
     mpMember = nullptr; 
     } 
    } 

    public: 
    Base() : mpMember(new BaseMember[1]) { } 
    virtual ~Base(void) { cleanupMember(); } 
}; 

class Derived : public Base { 
    virtual void doCleanupMember(BaseMember *m) override { delete m; } 

    public: 
    Derived() : Base(new DerivedMember) {} 
    ~Derived() { cleanupMember(); } 
}; 

但也有原因,這是一個壞主意。

首先是成員應該由基地獨家管理。試圖將Base成員的責任劃分爲派生類是複雜的,只是要求麻煩。

其次,你初始化mpMember的方式意味着成員有不同的接口,取決於誰初始化它。你已經遇到的部分問題是,你初始化成員的信息已被銷燬到~Base()的類型。同樣,試圖爲同一個變量設置不同的接口只是要求麻煩。

class Base { 
    std::shared_ptr<BaseMember> mpMember; 
    public: 
    Base(std::shared_ptr<BaseMember> m) : mpMember(m) { } 
    Base() : mpMember(std::make_shared<BaseMember>()) { } 
    virtual ~Base() {} 
}; 

class Derived : virtual public Base {  
    public: 
    Derived() 
     : Base(std::shared_ptr<BaseMember>(new DerivedMember[1], 
             [](BaseMember *m){delete [] m;}) {} 
}; 

這隻隱藏在成員的接口的破壞部分的區別:

我們至少可以使用類似shared_ptr它允許了指定缺失者解決的第一個問題。如果您有更多元素的數組,則該成員的不同用戶仍然必須能夠確定mpMember[2]是否合法。

+0

+1,在此實現中,內存在構造函數中分配並在析構函數中釋放。 – cpp

+0

如果Derived的作者錯誤地讓它的構造函數調用Base的默認構造方法,那麼此解決方案將無法工作, Base私有的默認構造函數,因爲這也會阻止它在其他地方的使用,所以這很大程度上依賴於與派生作者溝通,他必須做一些特殊的事情。如果我可以在我的代碼庫中使用std :: shared_ptr,那麼這仍然是迄今爲止最好的選擇。 – Kaiserludi

+0

@Kaiserludi「與派生作者溝通,他必須做一些特別的事情」這對於永遠正確地使用繼承是非常必要的。這是組合優於繼承的原因之一。但是,我沒有看到問題在這裏與一些派生類調用默認的基礎構造函數。 '派生類:virtual public Base {Derived():Base(){}};'仍然正確清理成員。 – bames53