首页 > 代码库 > C++之虚函数的原理

C++之虚函数的原理

1. 看看一个类中如果有虚函数,它的内存布局:

class A{
       double i;
       int j;
       virtual void foo(){}
       virtual void fun(){}
};

内存布局:

1>  class A       size(24):
1>        +---
1>   0   | {vfptr}
1>   8   | i
1>  16  | j
1>        | <alignment member> (size=4)
1>        +---

可以看出,A中有一个vfptr,这个是指向一个virtual table的指针。

C++会给每个有虚函数的类创建一张virtualtable。大小是这个类中虚函数的个数+1/2个slot(用于runtime type identify)。这张virtual table里的每行内容是这个类声明的虚函数的地址。

 

注意:vfptr在类的位置是固定的,一般放在最前面。它会影响字节对齐的最大字节。

 

2. 看看如果一个类继承了这个有虚函数的类。它的内存布局:

class Point
{
public:
       virtual ~Point(){};
       virtual float mult(float)=0;
       virtual float y(){return 0;}
       virtual float z(){return 0;}
       float _x;
};
class Point2d:Point
{
public:
       virtual ~Point2d(){};
       virtual float mult(float){return 0;}
       virtual float y(){return 1;}
       float _y;
};
class Point3d:Point2d
{
       virtual ~Point3d(){};
       virtual float mult(float){return 0;}
       virtual float z(){return 0;}
       float _z;
};


注意下图中的vptr放在了类的最后,一般是放在最前面的(VS中),也有放在最后的。这个知道就好了。


可以看出Point的virtual table有4个虚函数。

Point2d的virtualtable有4个虚函数,而看Point2d的类的定义发现,没有修改Point中的z()函数,所以Point2d的virtualtable还保留着Point中的z()函数地址。

每一个子类都有一个虚函数表。

此时注意:我们也可以在子类中定义新的virtual函数(父类中没有的函数),这就会在这个子类的虚函数表中增加一行。

(这里特别注意:什么叫新的virtual函数,如果子类中的virtual函数与父类中的某一virtual函数同名且函数签字完全相同,此时就修改父类中virtual函数地址。否则即使同名,函数签字不相同,也会在虚表中增加新的一行。)

<span style="color:#333333;">class A
{
public: virtual void foo()const{cout<<"A"<<endl;}
};
class B:public A
{
public: virtual voidfoo(){cout<<"B"<<endl;}
};
int _tmain(int argc, _TCHAR* argv[])
{
       A *a=new B;
       a->foo();//A
       return 0;
}</span>

这里Bfoo函数签字与Afoo函数签字不同,所以B的虚表中有两行名为foo的函数地址,一个A的,一个是B的。显然主函数中指针a不具有访问Bfoo的作用域权限,所以这里不会歧义,就是调用Afoo

这也是以前所说的多态的3要素:继承,virtual函数,同名函数签字相同才能是多态。

更加注意:这样的原理只适用于非虚继承,虚继承下,就不是这样的原理了。

 

多态是实质:

Point * p=new Point2d();

p->y();

此时p指向的地址是Point2d的一个对象,显然它的virtual table是Point2d的虚表。

虚函数的调用是通过vptr来调用的。P-y();C++实际的代码是(* p->vptr[3])(p);

p->vptr[3]用来找到y函数地址,而p指向的地址是Point2d的地址,这决定了调用的y函数是Point2d中的y函数。这就实现了多态。

注意此时p虽然指向了Point2d的对象地址,但是此时的p是有作用范围的,它不能调用不属于Point的东西,例如:p->_y;。

 

3. 多重继承下的虚函数

C继承了A,C继承B,A和B没有关系:

class A{
public:
       char i;
       virtual voidfoo(){cout<<"A"<<endl;}
};
class B
{
public:virtual void foo(int i){cout<<"B"<<endl;}
      virtual void fun(int i){cout<<"B"<<endl;}
 
};
class C:public A,B
{
};

此时因为A和B中都有一个vfptr,则C中就有两个vfptr了。而两个vfptr在内部是不同名称的。例如:vfptr_A和vfptr_B

内存布局:

1>  class C       size(12):
1>        +---
1>        | +--- (base class A)
1>   0   | | {vfptr}-------------------------------A::foo
1>   4   | | i
1>        | | <alignment member> (size=3)
1>        | +---
1>        | +--- (base class B)
1>   8   | | {vfptr}-------------------------------B::foo、B::fun
1>        | +---
1>        +---


此时如果:A *a=new C(); a->foo();显然是调用A::foo

                    B* b=new C();b->foo(2);显然是调用B::foo。注意此时b指向的是C中的B的开始位置,不是C的开始位置。

如果Cc;c.foo()/c.foo(2);都是错的。因为C++的寻找机制。首先在C中寻找,没有找到,然后去父类寻找,但是A和B都是父类,且A和B没有先后关系,所以C++同时去寻找,找到了两个foo。注意此时C++寻找机制不关心foo的参数。

 

如果Cc;c.fun(2);这是对的。因为编译器会遍历两个vfptr,只会找到一个(*c.vfptr_B[2])(&c);它就知道了应该按照这个进行下去。

 

总结:多重继承下,不要调用多个父类中同名的函数。注意这里即使参数列表不同也不能构成重载函数。其实也很显然,毕竟这些函数属于不同的类。

 

4. 虚继承下的虚函数

做两个实验:

class A
{
       public:virtual void foo(){cout<<"A"<<endl;}
};
class B:virtual public A
{
       public:virtual void foo(){cout<<"B"<<endl;}
};

内存布局:

1>  class B       size(8):
1>        +---
1>   0   | {vbptr}
1>        +---
1>        +--- (virtual base A)
1>   4   | {vfptr}
1>        +---


class A
{
       public:virtual void foo(){cout<<"A"<<endl;}
};
class B:virtual public A
{
       public:virtual void foo(inti){cout<<"B"<<endl;}
};

内存布局:

1>  class B       size(12):
1>        +---
1>   0   | {vfptr}
1>   4   | {vbptr}
1>        +---
1>        +--- (virtual base A)
1>   8   | {vfptr}
1>        +---


比较可知:虚继承下,如果子类里的虚函数都是重写了虚基类的虚函数,则子类不会单独创建一个vfptr。

如果子类里的虚函数不都是重写了虚基类的虚函数,还有别的虚函数,则子类会单独创建一个vfptr。

注意重写的含义:函数名和函数参数都相同,多态的。

 

这里还得出一些结论,如果vfptr和vbptr同属于一类时,vfptr在前面,因为vfptr的位置一定是固定的。而vbptr的位置不一样是固定的。但是注意:第一个实验的内存分布,因为vfptr和vbptr不属于同一个类。

 

注意这些内存布局是在VS下成立的,其他编译器不确定。

 

 

 

 

C++之虚函数的原理