C++虛擬函式解析(轉載)

weixin_33941350發表於2014-08-12

 虛擬函式詳解第一篇:物件記憶體模型淺析

C++中的虛擬函式的內部實現機制到底是怎樣的呢?
    鑑於涉及到的內容有點多,我將分三篇文章來介紹。
    第一篇:物件記憶體模型淺析,這裡我將對物件的記憶體模型進行簡單的實驗和總結。
    第二篇:繼承物件的構造和析構淺析,這裡我將對存在繼承關係的物件的構造和析構進行簡單的實驗和總結。
    第三篇:虛擬函式的內部機制淺析,這裡我將對虛擬函式內部的實現機制進行實驗總結。
    我使用的編譯器是VS2008,有不足或者不準確的地方,歡迎大家拍磚(我個人非常迫切的希望得到大家的指正),我會及時修正相關內容。
 
    開始正題:物件記憶體模型淺析:
    
  1. #include <tchar.h>
  2. #include <iostream>
  3. using namespace std;
  4.  
  5. #pragma pack (1)
  6.  
  7. class Person
  8. {
  9. private:
  10.     int m_nAge;
  11. };
  12.  
  13. class Man : public Person
  14. {
  15. private:
  16.     double m_dHeight;
  17. };
  18.  
  19. int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
  20. {
  21.     Person Jack;
  22.     Man Mike;
  23.     cout << sizeof(Jack) << endl;
  24.     cout << sizeof(Mike) << endl;
  25.     return 1;
  26. }
    首先解釋一下#pragma pack(1)這條語句的作用,它要求編譯器將位元組對齊的最小單位設定為1個位元組。
    關於位元組對齊,簡單的解釋就是,假定一個32位的CPU,讀取一個儲存在記憶體中的int型的變數,如果該int變數存放在記憶體中的首地址是偶地址,那麼CPU一個週期就能讀出這32bit的資料,如果該int變數存放在記憶體中的首地址是奇地址,就需要2個讀週期,並對兩次讀出的結果的高低位元組進行拼湊才能得到該32bit資料。所以,如果我們將位元組對齊設定為4個位元組,那麼理論上,CPU執行我們程式碼的速度要比將位元組對齊設定為1個位元組的速度要快。
    所以,如果我們將位元組對齊設定為8個位元組,那麼
    int nNum1;
    double dNum2;
將會佔用16個位元組的大小,而如果我們將位元組對齊設定為1個位元組,那麼它將會佔用12個位元組的大小,上述程式碼將位元組對齊設定為1個位元組,是為了防止位元組對齊干擾了我們對於物件記憶體模型的實驗。
    回到主題,上述程式碼的執行結果如下:
    4
    12
    我們看到,Person類物件佔用了4個位元組大小的記憶體空間,Man類物件佔用了12個位元組的大小的記憶體空間,所以,Man類中實際上有兩個成員變數,int m_nAge和double m_dHeight,所以可以得出一個結論:派生類物件中同時包含基類的成員變數。
    那麼它們在記憶體中的位置是怎樣的呢?
  1. #include <tchar.h>
  2. #include <iostream>
  3. using namespace std;
  4.  
  5. #pragma pack (1)
  6.  
  7. class Person
  8. {
  9. private:
  10.     int m_nAge;
  11. };
  12.  
  13. class Man : public Person
  14. {
  15. private:
  16.     double m_dHeight;
  17. };
  18.  
  19. class Woman : public Person
  20. {
  21. private:
  22.     double m_dWigth;
  23. };
  24.  
  25. int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
  26. {
  27.     Person Jack;
  28.     Man Mike;
  29.     Woman Susan;
  30.     cout << &Jack << endl;
  31.     cout << &Mike << endl;
  32.     cout << &Susan << endl;
  33.     return 1;
  34. }
上述程式碼輸出了Person類物件和Man類物件的地址,執行結果如下:
0012FF60
0012FF4C
0012FF38
我們知道,0012FF60和0012FF4C之間有14個位元組的記憶體空間,0012FF38和0012FF4C之間有14個位元組的記憶體空間,我們將Man類物件分成Person基類部分(int m_nAge)和Man派生類部分(double m_dHeight),將Woman類物件分成Person基類部分(int m_nAge)和Woman派生類部分(double m_dWeight)。那麼,Man類和Woman類的物件記憶體模型如下:
 
 
所以,
  • Person Jack;
  • Man Mike;
  • Woman Susan;
這三行程式碼實際上產生了3個Person基類部分、一個Man派生類部分和一個Woman派生類部分,而非像程式碼中寫的表意那樣,有一個Person基類部分,一個Man派生類部分和一個Woman派生類部分。
類的繼承和派生只是簡化方便我們程式設計師編寫程式碼,並不會簡化派生類物件佔用的記憶體大小。
 
C++中的虛擬函式的內部實現機制到底是怎樣的呢?
    鑑於涉及到的內容有點多,我將分三篇文章來介紹。
    第一篇:物件記憶體模型淺析,這裡我將對物件的記憶體模型進行簡單的實驗和總結。
    第二篇:繼承物件的構造和析構淺析,這裡我將對存在繼承關係的物件的構造和析構進行簡單的實驗和總結。
    第三篇:虛擬函式的內部機制淺析,這裡我將對虛擬函式內部的實現機制進行實驗總結。
    我使用的編譯器是VS2008,有不足或者不準確的地方,歡迎大家拍磚(我個人非常迫切的希望得到大家的指正),我會及時修正相關內容。
 
    開始正題:繼承物件的構造和析構淺析:
    在虛擬函式詳解第一篇中,我簡單的介紹了C++物件記憶體模型。我們瞭解到派生類物件是由基類部分和派生部分構成的,那麼該派生類物件是如何被構造和析構的呢?
    
  1. #include <tchar.h>
  2. #include <iostream>
  3. using namespace std;
  4.  
  5. class Person
  6. {
  7. public:
  8.     Person()
  9.     {
  10.         cout << _T("基類的建構函式被呼叫") << endl;
  11.     }
  12.  
  13.     ~Person()
  14.     {
  15.         cout << _T("基類的解構函式被呼叫") << endl;
  16.     }
  17. };
  18.  
  19. class Man : public Person
  20. {
  21. public:
  22.     Man()
  23.     {
  24.         cout << _T("派生類的建構函式被呼叫") << endl;
  25.     }
  26.  
  27.     ~Man()
  28.     {
  29.         cout << _T("派生類的解構函式被呼叫") << endl;
  30.     }
  31. };
  32.  
  33. int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
  34. {
  35.     Man Mike;
  36.     return 1;
  37. }
上述程式碼的執行結果如下:
我們可以看到:構造一個派生類物件的時候,先呼叫基類的建構函式,再呼叫派生類的建構函式,析構一個派生類物件的時候,先呼叫派生類的解構函式,再呼叫基類的解構函式。
 
    上述內容講述的是普通派生類的構造和析構過程,對於具有虛擬函式的派生類的構造和析構過程是怎樣的呢?
    
  1. #include <tchar.h>
  2. #include <iostream>
  3. using namespace std;
  4.  
  5. class Person
  6. {
  7. public:
  8.     Person()
  9.     {
  10.         cout << _T("基類的建構函式被呼叫") << endl;
  11.     }
  12.  
  13.     virtual void Height()
  14.     {
  15.         cout << _T("人類具有身高屬性") << endl;
  16.     }
  17.  
  18.     virtual ~Person()
  19.     {
  20.         cout << _T("基類的解構函式被呼叫") << endl;
  21.     }
  22. };
  23.  
  24. class Man : public Person
  25. {
  26. public:
  27.     Man()
  28.     {
  29.         cout << _T("派生類的建構函式被呼叫") << endl;
  30.     }
  31.  
  32.     virtual void Height()
  33.     {
  34.         cout << _T("男人具有身高屬性") << endl;
  35.     }
  36.  
  37.     virtual ~Man()
  38.     {
  39.         cout << _T("派生類的解構函式被呼叫") << endl;
  40.     }
  41.  
  42. private:
  43.  
  44.     double m_dHeight;
  45.     double m_dWeight;
  46. };
  47.  
  48. int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
  49. {
  50.     Person* pPersonObj = new Man;
  51.     delete pPersonObj;
  52.     return 1;
  53. }
上述程式碼的執行結果如下:
大家可能注意到了,上述程式碼中基類和派生類的解構函式都採用虛解構函式,而在_tmain函式中的呼叫方式也採用了Person* pPersonObj = new Man這種多型呼叫方式。當delete pPersonObj被執行來釋放派生類物件的時候,實際上呼叫的是派生類物件的虛解構函式,而派生類物件的虛解構函式會呼叫基類的解構函式,這樣就能將派生類物件完美的析構,如果這裡不採用虛解構函式,會是什麼結果呢?
 
  • #include <tchar.h>
  • #include <iostream>
  • using namespace std;
  • class Person
  • {
  • public:
  •     Person()
  •     {
  •         cout << _T("基類的建構函式被呼叫") << endl;
  •     }
  •     virtual void Height()
  •     {
  •         cout << _T("人類具有身高屬性") << endl;
  •     }
  •     ~Person()
  •     {
  •         cout << _T("基類的解構函式被呼叫") << endl;
  •     }
  • };
  • class Man : public Person
  • {
  • public:
  •     Man()
  •     {
  •         cout << _T("派生類的建構函式被呼叫") << endl;
  •     }
  •     virtual void Height()
  •     {
  •         cout << _T("男人具有身高屬性") << endl;
  •     }
  •     virtual ~Man()
  •     {
  •         cout << _T("派生類的解構函式被呼叫") << endl;
  •     }
  • private:
  •     double m_dHeight;
  •     double m_dWeight;
  • };
  • int _tmain(int argc, TCHAR* argv[], TCHAR* envp[])
  • {
  •     Person* pPersonObj = new Man;
  •     delete pPersonObj;
  •     return 1;
  • }
上述程式碼執行結果如下:
 
我們可以看到,當delete pPersonObj被執行的時候,只呼叫了基類的解構函式,並沒有呼叫派生類的解構函式,所以這個物件的派生部分的記憶體並沒有被釋放,從而造成記憶體洩露。
所以:當基類中包含有虛擬函式的時候,解構函式一定要寫成虛解構函式,否則會造成記憶體洩露。
為什麼一定要這麼做呢?我們在第三篇的內容裡尋找答案。
 
第三篇:自己打算寫,待續。。。

相關文章