Chinaunix首页 | 论坛 | 博客
  • 博客访问: 916357
  • 博文数量: 299
  • 博客积分: 0
  • 博客等级: 民兵
  • 技术积分: 2493
  • 用 户 组: 普通用户
  • 注册时间: 2014-03-21 10:07
个人简介

Linux后台服务器编程。

文章分类

全部博文(299)

文章存档

2015年(2)

2014年(297)

分类: C/C++

2014-08-22 21:22:58

C++中的虚函数的实现一般是通过虚函数表(C++规范并没有规定具体用哪种方法,但大部分的编译器厂商都选择此方法)。
类的虚函数表是一块连续的内存,每个内存单元中记录一个JMP指令的地址。
注意的是,编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享。类的每个虚成员占据虚函数表中的一行。如果类中有N个虚函数,那么其虚函数表将有N*4字节的大小。
虚函数(Virtual Function)是通过一张虚函数表(Virtual Table)来实现的。简称为V-Table。在这个表中,主要是一个类的虚函数的地址表,这张表解决了继承、覆盖的问题,保证其真实反应实际的函数。这样,在有虚函数的类的实例中分配了指向这个表的指针的内存,所以,当用父类的指针来操作一个子类的时候,这张虚函数表就显得尤为重要了,它就像一个地图一样,指明了实际所应该调用的函数。
编译器应该是保证虚函数表的指针存在于对象实例中最前面的位置(这是为了保证取到虚函数表的有最高的性能——如果有多层继承或是多重继承的情况下)。 这意味着可以通过对象实例的地址得到这张虚函数表,然后就可以遍历其中函数指针,并调用相应的函数。




总结一:

先看一个空的类占多少空间?

 


  1. class Base  
  2. {  
  3. public:  
  4.     Base();  
  5.     ~Base();  
  6.   
  7. };  


 

    注意到我这里显示声明了构造跟析构,但是sizeof(Base)的结果是1.

     因为一个空类也要实例化,所谓类的实例化就是在内存中分配一块地址,每个实例在内存中都有独一无二的地址。同样空类也会被实例化,所以编译器会给空类隐含的添加一个字节,这样空类实例化之后就有了独一无二的地址了。所以空类的sizeof为1。

   而析构函数,跟构造函数这些成员函数,是跟sizeof无关的,也不难理解因为我们的sizeof是针对实例,而普通成员函数,是针对类体的,一个类的成员函数,多个实例也共用相同的函数指针,所以自然不能归为实例的大小,这在我的另一篇博文有提到。

   接着看下面一段代码


  1. class Base  
  2. {  
  3. public:  
  4.     Base();                  
  5.     virtual ~Base();         //每个实例都有虚函数表  
  6.     void set_num(int num)    //普通成员函数,为各实例公有,不归入sizeof统计  
  7.     {  
  8.         a=num;  
  9.     }  
  10. private:  
  11.     int  a;                  //占4字节  
  12.     char *p;                 //4字节指针  
  13. };  
  14.   
  15. class Derive:public Base  
  16. {  
  17. public:  
  18.     Derive():Base(){};       
  19.     ~Derive(){};  
  20. private:  
  21.     static int st;         //非实例独占  
  22.     int  d;                     //占4字节  
  23.     char *p;                    //4字节指针  
  24.   
  25. };  
  26.   
  27. int main()   
  28. {   
  29.     cout<
  30.     cout<
  31.     return 0;  
  32. }  


 

结果自然是

12

20

 

Base类里的int  a;char *p;占8个字节。

而虚析构函数virtual ~Base();的指针占4子字节。

其他成员函数不归入sizeof统计。

Derive类首先要具有Base类的部分,也就是占12字节。

int  d;char *p;占8字节

static int st;不归入sizeof统计

所以一共是20字节。

 

在考虑在Derive里加一个成员char c;


  1. class Derive:public Base  
  2. {  
  3. public:  
  4.     Derive():Base(){};  
  5.     ~Derive(){};  
  6. private:  
  7.     static int st;  
  8.     int  d;  
  9.     char *p;  
  10.     char c;  
  11.   
  12. };  


这个时候,结果就变成了

12

24

 

一个char c;增加了4字节,说明类的大小也遵守类似class字节对齐,的补齐规则。

具体的可以看我那篇《5分钟搞定字节对齐》

 

 

至此,我们可以归纳以下几个原则:

1.类的大小为类的非静态成员数据的类型大小之和,也就是说静态成员数据不作考虑。

2.普通成员函数与sizeof无关。

3.虚函数由于要维护在虚函数表,所以要占据一个指针大小,也就是4字节。

4.类的总大小也遵守类似class字节对齐的,调整规则。


总结二:

上一篇文章研究了关于类大小的4条规则后,我们再结合虚函数表,来研究下类的大小。

 


  1. class Base  
  2. {  
  3. public:  
  4.     Base(){};  
  5.     virtual ~Base(){};  
  6.     void set_num(int num)  
  7.     {  
  8.         a=num;  
  9.     }  
  10.     virtual int get_num()  
  11.     {  
  12.         return a;  
  13.     }  
  14. private:  
  15.     int  a;  
  16.     char *p;  
  17. };  
  18.   
  19. class Derive:public Base  
  20. {  
  21. public:  
  22.     Derive():Base(){};  
  23.     ~Derive(){};  
  24.          virtual int get_num()  
  25.     {  
  26.         return d;  
  27.     }  
  28. private:  
  29.     static int st;  
  30.          int  d;  
  31.          char *p;  
  32.     char c;  
  33.   
  34. };  
  35.   
  36. int main()   
  37. {   
  38.     cout<
  39.     cout<
  40.     return 0;  
  41. }  


 

在Base类里添加了virtual int get_num()函数,而子类也重新实现了virtual int get_num()函数。

但是结果依然是

12

24

 

说明子类只是共用父类的虚函数表,因此一旦父类里有虚函数,子类的虚函数将不计入sizeof大小。

 

这可以认为是一个补充规则。





总结三:

看这段代码

 


  1. class   Top     
  2. {     
  3. protected:     
  4.     int   x;     
  5. public:     
  6.     Top(int   n):x(n){cout<<"Top"<
  7.     virtual   ~Top(){}     
  8.       
  9. };     
  10. class   Left:virtual   public   Top     
  11. {     
  12. protected:     
  13.     int   y;     
  14. public:     
  15.     Left(int   m,int   n):Top(m){y=n;cout<<"Left"<
  16. };     
  17. class   Right:public   virtual   Top     
  18. {     
  19. protected:     
  20.     int   z;     
  21. public:     
  22.     Right(int   m,int   n):Top(m){z=n;cout<<"Right"<
  23. };     
  24. class   Bottom:public   Left,public   Right     
  25. {     
  26.     int   w;     
  27. public:     
  28.     Bottom(int   i,int   j,int   k,int   m):Top(i),Left(i,j),Right(i,k),w(m)             
  29.     {     
  30.         cout<<"Bottom"<
  31.     }             
  32. };     
  33. int   main()     
  34. {     
  35.     Bottom   b(1,2,3,4);     
  36.     cout<<"sizeof(b)   "<<sizeof(b)<<","<<sizeof(Bottom)<
  37.     cout<<sizeof(Left)<<","<<sizeof(Right)<<","<<sizeof(Top)<
  38.     for(int   i=0;i<sizeof(b);i+=4)     
  39.         cout<<*(reinterpret_cast<int*>((&b)+i))<<"________"<
  40.     system("PAUSE");             
  41. }     


 

结果如下:

Top
Left
Right
Bottom
sizeof(b)   28,28
16,16,8
4657244________
2367460________
0________
746________
44________
5701724________
4________
请按任意键继续. . .

 

也就是说Top为占8字节,这好理解。

 int   x;            //4字节
 virtual   ~Top(){}      //基类的虚表入口,4字节

 

接着看Left跟Right都是16字节。

本来除了Top的8字节,Left里只有int   y; 占4字节,还有4字节占在那里?

 

由于是虚继承,虚继承的子类都要包含一个指向基类的指针,从而实现动态联编。

一次,要额外加4字节的空间。所以一共是8+4+4=16字节。

Right同理。

 

再看Bottom的大小为28字节,这个是怎么算的呢?

 

 

虚继承是棱形继承,基类大小为8字节.

而Bottom为普通多继承,因此,Bottom的大小应该是Bottom部分+Left部分+Right部分+各自指向基类的指针+基类大小(虚继承导致只有一个基类实例)。

                                        Top

                                     /      /

                                    /        /

                                Left       Right

                                 /         /

                                   /      /

                                     Bottom

现在算算,基类8字节+Left4字节+Right4字节+4字节指向基类的指针*2+Bottom4字节=28字节。

 

关于函数以及变量是否统计入sizeof实例,请参考我前两篇。

 

纯粹自己根据资料推导,欢迎指正。


阅读(2404) | 评论(0) | 转发(0) |
给主人留下些什么吧!~~