C++手動調用析構函數無效問題排查

  在學習C++的時候,都知道不要手動調用析構函數,也不要在構造函數、析構函數裏調用虛函數。工做這麼多年,這些冷門的知識極少用到,漸漸被繁雜的業務邏輯淹沒掉。html

  不過,最近項目裏出現了析構函數沒有被正確地調用,致使內存泄漏。代碼大概以下:app

class base_mem_alloc
{
public:
  base_mem_alloc() {}
  virtual ~base_mem_alloc() {}
};
class mem_alloc : public base_mem_alloc { public: ~mem_alloc() {} virtual ~mem_alloc() { // 釋放內存 } }; class test_conf { public: bool reload(); private: class mem_alloc _alloc; } bool test_conf::reload() { // 正常寫法,釋放全部內存,但沒有這個接口 // _alloc.release() // 有問題的寫法 class mem_alloc tmp; _alloc.~mem_alloc(); // 經過析構函數釋放內存 _alloc = tmp; // 經過拷貝構造函數將內部變量初始化 // 使用_alloc分配內存 }

  公司的框架要求使用統一的內存分配器。像讀取配置這種邏輯,在配置不須要的時候(也就是關掉進程)是直接從分配器統一釋放的,但這框架有點年代了,以前沒有考慮遊戲熱更配置的問題。如今要求能從新加載配置,那麼就少了一個釋放內存的接口。因而,便經過析構函數釋放內存,而後再用拷貝構造函數把一個臨時分配器拷貝過來。雖然這種寫法極其少見,但咋一看,好像也沒問題。而後不幸的是,這種寫法真的有問題,~mem_alloc()這個析構函數是沒法正常調用的。框架

  代碼中的內存分配器用了多態,C++的多態是依賴虛函數表實現的,虛函數表是在構造函數的時候一步步建立,在析構函數一步步銷燬。之因此說是一步步,由於C++在調用構造函數時,會從基類構造函數--子類構造函數構造,析構時從子類析構函數--基類析構函數。在這個過程當中,對象的類型也是會變的,調用基類構造函數的時候,他的類型就是基類,調用子類構造函數時,就是子類。析構時則反過來,因此析構完成後,對象的類型是基類(理論上講,再也不存在這個對象,但他的數據遺留是基類)。參考:https://www.artima.com/cppsource/nevercall.html函數

    During base class construction of a derived class object, the type of the object
is that of the base class. Not only do virtual functions resolve to the base class, 
but the parts of the language using runtime type information (e.g., dynamic_cast (see Item 27) 
and typeid) treat the object as a base class type.An object doesn't become a derived class object 
until execution of a derived class constructor begins.

    The same reasoning applies during destruction. Once a derived class destructor has run, 
the object's derived class data members assume undefined values, so C++ treats them as if 
they no longer exist. Upon entry to the base class destructor, the object becomes a base class
object, and all parts of C++—virtual functions, dynamic_casts, etc.—treat it that way.

 

  因爲虛函數表在構造、析構過程當中是變化的,所以在這時調用虛函數可能不會獲得正確的結果。而像dynamic_cast這種依賴運行時的轉換,也不可用。上面出問題的代碼中,手動調用析構函數,第一次是可以正常調用的,而後就變成了基類。在使用拷貝構造函數時,會拷貝臨時對象的數據,可是並不會重建虛函數表。因爲在咱們項目的代碼中大部分功能是由基類完成的,使用拷貝構造函數後,對象的內存數據也沒有被破壞。所以運行起來並無什麼太大的問題,再加上這個地方是須要屢次從新加載配置才能重現,致使這個問題被隱藏了一段時間。學習

  其實這個問題很好解決。加個釋放函數就OK,或者換用指針,delete掉再new就能夠了。用placement new在原來對象上從新建立一個對象也行。最後說一句,寫代碼,不是寫得越複雜越高深纔好,而是越通俗易懂越好,少用一些奇奇怪怪的寫法用法。畢竟代碼多數是須要維護的,公司招的人每一個人的水平都不同,通俗的代碼則更容易維護。spa

相關文章
相關標籤/搜索