C++中的多态机制----虚函数

版权声明:本文为博主原创文章,未经博主允许不得转载。 https://blog.csdn.net/recall_yesterday/article/details/52830445

自以为c++对虚函数比较了解了,但是最近面试时被问到了,才发现是那么的含糊其辞,纸老虎一只,现在好好的整理下,算是亡羊补牢之作;

C++中存在两种多态,一种是重载函数一种是虚函数机制;
而虚函数的实现编译器不同而不同,最主要的是gcc和vs两个的实现的区别;

二者都是利用虚函数表来记录虚函数的地址,这个虚函数的位置在数据段中.
单继承情况时:

class A
{
public:
virtual int funA()
{};
int a;
};

class B:public A
{
public:
    int  funA()
    {}
    virtual funB()
    {}
   int b;
 };

其中sizeof(A) = 8,sizeof(B) = 12
原因就是A中成员占4个字节外还有一个虚表指针,这个虚表指针是指向存有虚函数地址的虚函数表的;
B中出了有自己的成员和从A中继承的成员外,还有一个和A共享的一个虚表指针;同时自己新定义的虚函数也会放到虚表指针指向的虚函数表中;

对象的内存布局为:
{
void *vfptr;//虚表指针
int a;
…………..
int b;
}
此种情况在vs和gcc中的实现情况都是一样的;
对于多继承例如

class A
{
public:
virtual int funA()
{};
int a;
};
class B
{
public:
    virtual void funB(){}
    int b;
};
class C:public A,public B
{
public:
    int  funA()
    {}
    void funB()
    {}
    void funC(){}
   int c;
 };

此时vs和gcc中的实现都是一样的,C对象会有两个虚表指针,分别是A的虚表指针和B的虚表指针.如果自己也有新的虚函数,那么会加入到第一个虚表中去,内存布局如下://是根据继承顺序的
{
void *vfptr;//a的虚表指针
int a;
…….
void *vfptr;//b的虚表指针
int b;
………
int c;
}

vs和gcc中最不同的是虚继承时二者的实现;

根据资料维基百科中的说法,
{
g++编译器生成的C++类实例,虚函数与虚基类地址偏移值共用一个虚表(vtable)。类实例的开始处即为指向所属类的虚指针(vptr)。实际上,一个类与它的若干祖先类(父类、祖父类、…)组成部分共用一个虚表,但各自使用的虚表部分依次相接、不相重叠。
g++编译下,一个类实例的虚指针指向该类虚表中的第一个虚函数的地址。如果该类没有虚函数(或者虚函数都写入了祖先类的虚表,覆盖了祖先类的对应虚函数),因而该类自身虚表中没有虚函数需要填入,但该类有虚继承的祖先类,则仍然必须要访问虚表中的虚基类地址偏移值。这种情况下,该类仍然需要有虚表,该类实例的虚指针指向类虚表中一个值为0的条目。
该类其它的虚函数的地址依次填在虚表中第一个虚函数条目之后(内存地址自低向高方向)。虚表中第一个虚函数条目之前(内存地址自高向低方向),依次填入了typeinfo(用于RTTI)、虚指针到整个对象开始处的偏移值、虚基类地址偏移值。因此,如果一个类虚继承了两个类,那么对于32位程序,虚继承的左父类地址偏移值位于vptr-0x0c,虚继承的右父类地址偏移值位于vptr-0x10.
一个类的祖先类有复杂的虚继承关系,则该类的各个虚基类偏移值在虚表中的存储顺序尊重自该类到祖先的深度优先遍历次序
}
对于基类中没有虚函数时虚继承时情况时,这时会只有一个虚基类指针而没有虚表指针,如果派生类中出现虚函数,那么也会有虚表指针,只不过这是彻头彻尾属于派生类的,而不是基类中继承的,所以这时虚函数和虚基类表会合并,用一个指针即可;

class A
{
public:
 int funA(){};
 virtual void funa();
long a;
};
class B:virtual public A
{
public:
    virtual void funB(){}
    long  b;
};

在gcc中其中B的对象中有虚表指针(与虚基类共享虚表指针),有个虚基类指针,sizeof(B) = 32内存布局如下:
{
void * vptr;//虚函数表指针
long b;
…….
void * vptr;//虚函数表指针
long a;
}
由于这里的g++采用的虚继承的策略是通过偏移量来解决的,那么会在每个虚函数表的第一个虚函数的前面加上虚基类的相对当前虚表指针的偏移量。所以在64位g++中编译后的sizeof为32;如果是虚基类中没有虚函数,那么就会少基类中的虚表指针了,此刻sizeof就是24了,在上面的百科中已经说的很好了,更为详细的可以看深度探索c++对象模型一书;

而在vs中内存布局为
{
void * vfptr;
void *vbptr;
int b;
……
void *vfptr;
int a;
}
vs的采用方案是通过单独的指针来指向虚基类的位置的。

最重要的一种情况那就是如果是菱形继承,采用虚继承会出现什么情况,内存布局如何?

class A
{
public:
 virtual int funA()
{};
long a;
};
class B:virtual public A
{
public:
    virtual void funB(){}
    long b;
};
class C:virtual public A
{
public:
    virtual void funC(){}
   long c;
 };

class D:public C,public B
{
    public:
    virtual void fund(){}
    long d;
};

在g++中,内存对象布局如下,还是采用了偏移量的策略,虚函数表和虚基类偏移量还是在一个表,但是由于每个类中都有虚函数,所以都会有一个虚表指针

{
     void *vptr;
     long c;
     ............
     void *vptr;
     long b;
     ............
     long d;
     ............
     void *vptr;
     long a;
}
sizeof(D)=56 64bit g++
在vs中,内存布局为:
{
     void *vfptr;
     void *vbptr;
     long c;
     ............
     void *vfptr;
      void *vbptr;
     long b;
     ............
     long d;
     ............
     void *vfptr;
     long a;
}sizeof(D)=72 64bit vs2015

可以看出二者的区别,就是vs对于虚继承中并不是共享虚函数表的,同时也会有独立的虚基类指针来存储虚基类的偏移量;

翻了好多资料都没有让我觉得看完很清晰的,还是维基百科中的解释让我能有领悟.可能自己的理解有偏差或者文章有问题,欢迎指出.

猜你喜欢

转载自blog.csdn.net/recall_yesterday/article/details/52830445