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

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

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

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在原來對象上重新創建一個對象也行。最後說一句,寫代碼,不是寫得越複雜越高深纔好,而是越通俗易懂越好,少用一些奇奇怪怪的寫法用法。畢竟代碼多數是需要維護的,公司招的人每個人的水平都不一樣,通俗的代碼則更容易維護。

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