首页 > 代码库 > C++虚函数解析(转载)

C++虚函数解析(转载)

 虚函数详解第一篇:对象内存模型浅析

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被执行的时候,只调用了基类的析构函数,并没有调用派生类的析构函数,所以这个对象的派生部分的内存并没有被释放,从而造成内存泄露。
所以:当基类中包含有虚函数的时候,析构函数一定要写成虚析构函数,否则会造成内存泄露。
为什么一定要这么做呢?我们在第三篇的内容里寻找答案。
 
第三篇:自己打算写,待续。。。