C++基础篇--虚函数原理

虚函数算是C++最关键和核心的内容之一,是组件的基础。下面先列出一些相关名词,再围绕它们举例说明虚函数的本质实现原理。

基础概念(英文部分来自C++编程思想)

1)绑定:Connectinga
function call to a function body is called binding.(把函数调用和函数实现关联的过程)

2)早绑定:Whenbinding
is performed before the program is run (by the compiler and linker),it‘ s calledearly binding(程序运行前,即编译和链接阶段,完成的绑定即为早绑定)。

3)迟绑定:latebinding,
which means the binding occurs at runtime, based on the type of theobject.When a language implements late binding, there must be some mechanism todetermine the type of the object at runtime and call the appropriate memberfunction.(迟绑定发生在运行时,不同类型的对象绑定不同函数。实现迟绑定,必须有某种机制确定对象的具体类型然后调用合适的成员函数)。

4)虚函数表(VTable):一个存储于常量区的函数指针表,类似函数指针数组。每个含有虚函数的类(基类及派生类)各自包含一张虚函数表(一个类一张表),表中依次存放虚函数地址派生类vtable继承它各个基类的vtable,这里继承是指:基类vtable中包含某item,派生类vtable中也将包含同样item,但值可能不同。如派生类(override)重新实现了某虚函数,则它的vtable中该项item指向新写的虚函数,若未重新实现则沿用基类vtable中对应项的值。

5) 指向虚函数表的指针(vtptr):所有包含虚函数的类所实例化的对象里,都包含该指针,运行时对象借助于它寻址到虚函数表,从而完成后绑定。因此每个包含虚函数的对象,相比普通对象会额外多占用一个指针型的存储空间。

魔术与揭秘

class Base     //基类,包含virtual函数

{

public:

virtual void output(){ cout << "Base::output()" << endl;}

};

//派生两个类Drv0和Drv1

class Drv0 : public Base

{

public:

void output () { cout << "Drv0:: output ()" << endl;}

};

class Drv1 : public Base

{

public:

void output () { cout << "Drv1:: output ()" << endl;}

};

void main()

{

Base b;

Base* pb = &b;

pb-> output ();             
//输出Base::output()

Drv0 d1;

pb =reinterpret_cast<Base*>(&d1);

pb-> output ();               //输出Drv0:: output()

Drv0 d2;

pb =reinterpret_cast<Base*>(&d2);

pb-> output ();              //输出Drv0:: output()

Drv1 d3;

pb = reinterpret_cast<Base*>(&d3);

pb-> output ();            
//输出Drv1:: output()

}

经过一些中间封装变换,最终同样的”pb-> output ()”运行时选择了不同函数,得到不同结果。奇妙的魔术?别急,下面用结构体实现类似功能:

typedef void (*pvfun)( );

const pvfun pf_Base[1]= {Base_output};

const pvfun pf_Drv0[1]= {Drv0_output};

const pvfun pf_Drv1[1]= {Drv1_output};

typedef struct BASE

{

void *vtptr;

int mBase;

}Base;

void Base_output()    {  printf("Base::output()"); 
}

typedef struct DRV0

{

void *vtptr;

int mBase;

int mDrv0

}Drv0;

void Drv0_output()    {  printf("Drv0::output()"); 
}

typedef struct DRV1

{

void *vtptr;

int mBase;

int mDrv1

}Drv1;

void Drv1_output()    {  printf("Drv1::output()"); 
}

void main()

{

Base b;

b.vtptr =pf_Base  

Base* pb =&b; //

*((pvfun)(pb->vtptr+0))();      
//调用Base_output()

Drv0 d1;

d1.vtptr =
pf_Drv0   ②

pb = (Base*)(&d1);

*((pvfun)(pb->vtptr+0))();       //调用Drv0_output
()

Drv0 d2;

d2.vtptr =
pf_Drv0

pb = (Base*)(&d2);

*((pvfun)(pb->vtptr+0))()      
//调用Drv0_output ()

Drv1 d3;

d3.vtptr =
pf_Drv1   ③

pb = (Base*)(&d3);

*((pvfun)(pb->vtptr+0))()      //调用Drv1_output
()

}

上例同样实现了用相同形式调用不同函数,但这次能清楚看出猫腻所在:
①②③处分别为结构体成员vtptr赋了不同值。魔术揭穿了,还记得么:”所有软件问题都可以通过增加一个中间层解决”。表面的神奇是依靠VTable和vtptr组成的中间层在背后耍把戏。

例中pf_Base/pf_Drv0/pf_Drv1就是虚函数表VTable;各结构体的成员vtptr就是指向VTable的指针;①②③处是把vtptr与各自struct对应的VTable关联。只不过这些在C++中都隐藏不可见,由编译器自动生成和处理:

1)虚函数表与类关联:编译器在编译时自动为每个包含虚函数的类及其派生类各自单独生成一张虚函数表,用于存放虚函数指针。注意:基类与派生类各有各的虚表,独立存放于不同地址,唯的一关联是:派生类如果没重新实现某基类虚函数,其VTable对应条目中默认存放基类虚函数地址以做后备。

2)对象与虚函数表关联:对包含虚函数的类,C++编译器为其每个对象插入一个指针成员vtptr,指向该类的虚函数表,即同类对象的vtptr值相同。vtptr值在构造函数中初始化(编译器自动加入),即使该类没定义构造函数,默认构造函数也会初始化vtptr。

3)上面两步说明对象实例化一完毕,就已经和具体虚函数实现挂钩,调用时看似智能的选择不过是顺藤摸瓜:

Drv0 d1;                                                //这一步背后d1->vtptr=
VTable(Drv0),其中VTable[0]=(*Drv0::output)()

pb =reinterpret_cast<Base*>(&d1);    
//编译器支持指针强制向上类型转换,把派生类对象的地址赋给基类指针,pb值仍是&d1

pb-> output();                                       //d1->vtptr[0](),即调用Drv0::
output ()

总结虚函数实现原理:

编译期建立vtable表,设定表中元素;

执行期间在对象创建时的构造函数中关联vtptr和vtable表;

借助于指针支持的以小引大,通过强制转换将派生类对象的地址赋给基类指针;

通过基类指针调用虚函数,先取得对象中的vtptr(obj->vtptr),再找到其所指的对应于特定父类或子类的虚函数表(VTable=*(vtptr)),然后表头加偏移量寻址到相应函数指针(vfunptr
= VTable[offset]),最后执行*vfunptr()。

这就是C++通过虚函数实现多态的背后原理,多态使我们可统一用指向基类对象的指针调用所有基类/派生类的虚函数实现,到底会调哪个,关键看对象的vtptr指针指向了哪个类的VTable,而这点在对象实例化时会通过构造函数隐含设置好。

以一个问题结尾,可否在类的构造函数中调用虚函数,为什么?

时间: 2024-08-24 03:46:59

C++基础篇--虚函数原理的相关文章

虚函数原理解析

虚函数原理 虚函数的一般实现模型:每个类有一个虚函数表,内含该类中有作用的虚函数地址.每个 对象有一个vptr(虚函数表指针)指向虚函数表 如下Person类 class Person { public: virtual ~Person(); virtual string& getName(); virtual string& setName(); protected: string name_; }; 在Person的对象Jack中,有两个东西,一个是数据成员name_,一个是_Vptr

C++拾遗--虚函数原理

C++拾遗--虚函数原理 前言 C++的多态依赖虚函数来实现.若类存在虚函数,则每一个类的实例都维护了一个地址,这个地址指向虚函数表.虚函数表中存放的是类中所有虚函数的地址.下面我们找出虚函数表的地址,从而获得每个虚函数的地址,然后使用地址直接调用虚函数. 正文 1.空类的size #include <iostream> using namespace std; class MyClass { }; int main() { cout << "sizeof(MyClass

【C/C++学院】0823-静态联合编译与动态联合编译/父类指针子类指针释放/虚函数/纯虚函数概念以及虚析构函数/抽象类与纯虚函数以及应用/虚函数原理/虚函数分层以及异质链表/类模板的概念以及应用

静态联合编译与动态联合编译 #include <iostream> #include <stdlib.h> //散列 void go(int num) { } void go(char *str) { } //class //::在一个类中 class A { public: void go(int num) { } void go(char *str) { } }; void main() { ///auto p = go;编译的阶段,静态联编 void(*p1)(char *s

C++虚函数原理

类中的成员函数分为静态成员函数和非静态成员函数,而非静态成员函数又分为普通函数和虚函数. Q: 为什么使用虚函数 A: 使用虚函数,我们可以获得良好的可扩展性.在一个设计比较好的面向对象程序中,大多数函数都是与基类的接口进行通信.因为使用基类接口时,调用基类接口的程序不需要改变就可以适应新类.如果用户想添加新功能,他就可以从基类继承并添加相应的新功能. Q: 简述C++虚函数作用及底层实现原理 A: 要点是要答出虚函数表和虚函数表指针的作用. C++中虚函数使用虚函数表和虚函数表指针实现,虚函数

【核心基础】虚函数

本节研究虚函数的相关问题: 虚函数表 无继承 代码片段 class Animal { public: Animal(int age) : _age(age) { } virtual void f() { cout << "Animal::f " << _age << endl; } virtual void g() { cout << "Animal::g " << _age << endl;

虚函数原理

虚函数表的数量与位置:编译器会为每个有虚函数的类创建一个虚函数表,该虚函数表将被该类的所有对象共享.编译器将虚函数表存放在了目标文件或者可执行文件的常量段,即代码区. 虚函数表指针(vptr)的数量与位置:如果1个类中存在一个虚函数,那么第一个地址永远都是指向虚函数列表的指针.子类没有vptr,子类的虚函数存放在第一个父类的虚函数表的最后,如果有覆盖,则覆盖掉相应父类的虚函数. lass Base { public: virtual void f() { cout << "Base:

C++基础:虚函数、重载、覆盖、隐藏&lt;转&gt;

转自:http://www.2cto.com/kf/201404/291772.html 虚函数总是跟多态联系在一起,引入虚函数可以使用基类指针对继承类对象进行操作! 虚函数:继承接口(函数名,参数,返回值),但是实现不继承(函数体) 非虚函数:继承接口,也继承实现: 1)虚析构函数(当一个类打算作为基类使用时候,其析构函数必须是虚函数) 构造函数可以为虚函数吗? 不可以,在生成对象的时候,必须向编译器明确指定要生成什么类型的对象,因而不存在虚函数的问题:只有当对象已经存在,我用什么接口去操作它

python 基础篇 11 函数进阶----装饰器

11. 前??能-装饰器初识本节主要内容:1. 函数名的运?, 第?类对象2. 闭包3. 装饰器初识 一:函数名的运用: 函数名是一个变量,但他是一个特殊变量,加上括号可以执行函数. ?. 闭包什么是闭包? 闭包就是内层函数, 对外层函数(非全局)的变量的引?. 叫闭包 可以使用_clesure_检测函数是否是闭包  返回cell则是闭包,返回None则不是 闭包的好处: 由它我们可以引出闭包的好处. 由于我们在外界可以访问内部函数. 那这个时候内部函数访问的时间和时机就不?定了, 因为在外部,

JavaScript基础篇(四)— — 函数

一.函数基础 ??1.返回值:如果某个函数没有显式的return返回值,默认它的返回值为undefined ??2.参数:内建变量arguments,能返回函数所接收的所有参数 ???? ??3.预定义(内建)函数 -- isNaN: ????a.检测parseInt / parseFloat调用是否成功. ???? ????b.NaN不存在等值的概念, 也就是说表达式NaN === NaN 返回的是false 二.函数的变量作用域 ??1.变量提升:函数域优先于全局域,所有局部a会覆盖掉所有与