首页 > 代码库 > 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>
这里B的foo函数签字与A的foo函数签字不同,所以B的虚表中有两行名为foo的函数地址,一个A的,一个是B的。显然主函数中指针a不具有访问B的foo的作用域权限,所以这里不会歧义,就是调用A的foo。
这也是以前所说的多态的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++之虚函数的原理