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下成立的,其他编译器不确定。