虚函数

来源:互联网 发布:linux清除历史命令 编辑:程序博客网 时间:2024/06/05 03:51
虚函数的内存布局
一个拥有虚函数的类内部会有一个成员变量vptr,一个四字节大小的指针,指向虚函数表,虚函数表中记录了该类的各个虚函数的入口地址,如果该类重载了继承的虚函数,那么就存放自己的虚函数地址,否则就是父类的虚函数地址。
class A
{
public:
     virtualvoid f(){};
     virtual ~A(){};
};
 
class B:public A
{
     void f(){int i=0;};
};
 
A* pA=new B();
pA->f();
对于f的调用操作编译器有如下动作:
void B::f()函数解释为void f(B* this);
pA->f()解释为 (*pA->vptr[0])(this);//0是f函数在虚拟函数表格中的索引
 
所以我们可以看出,虽然指针pA静态类型为A类的指针,但是对于f的调用是依赖于B对象内部的vptr指向的虚拟函数表,而此时函数表内的f函数已经是B类的重载版本,因此这就构成了运行时多态,这个c++的基本特征。
        
         虚继承的内存布局,摘自《c++对话系列》。
class parent { /* whatever */ };
class child1 : public virtual parent { /* whatever */ };
class child2 : public virtual parent { /* whatever */ };
class multi : public child1, public child2 { /* whatever */ };
 
parent::vptr
parent data
child1::vptr
child1 data
child2::vptr
child2 data
multi::vptr
multi data
在这种复杂的情况下,最底层派生对象内部拥有三个vptr,指向三个虚函数表。
注意,经过我的实验,这种内存布局各种编译器表现不一样,比如vc就是先把child1放在最前面,然后是child2,最后是parent,并且至少vc中,我们可以通过调用static_cast获得各个类型的vptr值,这种典型的应用是在comQueryInterface函数的实现里面。
         经过上面的分析,我知道上面的虚继承会带来多个虚函数表以及多个vptr,这是内存上的额外的开销,当然避免了多个顶级父类的内存副本和模棱两可的继承,也有它的好处。
         我们可以看看com里面常常出现的多继承带来的对象内存布局
class CPenguin : public IBird, public ISnappyDresser {...};
IBird和ISnappyDresser接口都继承自IUnknown接口,内存布局如下图:
 
纯虚函数和虚函数的区别
         参考 <<Effective C++>> 3th Edition,这里作个简单的概括
 
纯虚函数分为函数定义和没有函数定义----
没有函数定义的纯虚函数目的是为了让子类继承接口,强制子类实现该函数;
有函数定义的纯虚函数目的是为了让子类继承接口,而父类的实现函数必须在子类重载函数中手动调用
 
非纯虚函数目的是让子类自动的继承接口和函数实现
 
虚函数与访问权限
         虚函数的重载机制和访问权限是相互独立的,互相没有干扰,但是可以结合使用。
一个private权限的虚函数可以被子类重载,但是子类不能访问父类的虚函数,但是父类可以通过运行时多态的方式来调用子类重载后的虚函数。
一个protected权限的虚函数可以被子类重载,子类也可以访问父类的虚函数
一个public权限的虚函数可以被重载,子类也可以访问
虚函数与设计模式
      虚函数和template method模式的结合
         这也被称为NVI(non virtual interface)手法。类只暴露非虚函数f,同时提供一个保护或者私有的d_f虚函数,在f函数的视线中调用do_f。派生类重载do_f从而提供自己的独特功能。
         Template Method模式的好处就是提供统一的调用框架,在f函数里面,而交给派生类自己订制的行为。比如f函数里面,可以在调用do_f之前与之后添加一些代码,执行内存分配和清除动作。
         虚拟函数应该和数据成员一样对待----让他们成为私有的,除非设计需求表明应该有较少的限制。提升它们到更高存取级别比把它们降到更私有的级别更容易些。
原创粉丝点击