在《 C++ 编程思想》一书中对虚函数的实现机制有详细的描述,通常的编译器经过虚函数表,在编译时插入一段隐藏的代码,保存类型信息和虚函数地址,而在调用时,这段隐藏的代码能够找到和实际对象一致的虚函数实现。编程
咱们在这里提供一个 C 中的实现,模仿 VTABLE 这种机制,但一切都须要咱们本身在代码中装配。数组
以前在网上看到一篇描述 C 语言实现虚函数和多态的文章,谈到在基类中保存派生类的指针、在派生类中保存基类的指针来实现相互调用,保障基类、派生类在使用虚函数时的行为和 C++ 相似。我以为这种方法有很大的局限性,不说继承层次的问题,单单是在基类中保存派生类指针这一作法,就已经违反了虚函数和多态的本意——多态就是要经过基类接口来使用派生类,若是基类还须要知道派生类的信息……。框架
个人基本思路是:函数
在“基类”中显式声明一个 void** 成员,做为数组保存基类定义的全部函数指针,同时声明一个 int 类型的成员,指明 void* 数组的长度。spa
“基类”定义的每一个函数指针在数组中的位置、顺序是固定的,这是约定,必须的.net
每一个“派生类”都必须填充基类的函数指针数组(可能要动态增加),没有重写虚函数时,对应位置置 0指针
“基类”的函数实现中,遍历函数指针数组,找到继承层次中的最后一个非 0 的函数指针,就是实际应该调用的和对象相对应的函数实现orm
好了,先来看一点代码:对象
[cpp] view plain copyblog
struct base {
void ** vtable;
int vt_size;
void (*func_1)(struct base *b);
int (*func_2)(struct base *b, int x);
};
struct derived {
struct base b;
int i;
};
struct derived_2{
struct derived d;
char *name;
};
上面的代码是咱们接下来要讨论的,先说一点,在 C 中,用结构体内的函数指针和 C++ 的成员函数对应, C 的这种方式,全部函数都天生是虚函数(指针能够随时修改哦)。
注意,derived 和 derived_2 并无定义 func_1 和 func_2 。在 C 的虚函数实现中,若是派生类要重写虚函数,不须要在派生类中显式声明。要作的是,在实现文件中实现你要重写的函数,在构造函数中把重写的函数填入虚函数表。
咱们面临一个问题,派生类不知道基类的函数实如今什么地方(从高内聚、低耦合的原则来看),在构造派生类实例时,如何初始化虚函数表?在 C++ 中编译器会自动调用继承层次上全部父(祖先)类的构造函数,也能够显式在派生类的构造函数的初始化列表中调用基类的构造函数。怎么办?
咱们提供一个不那么优雅的解决办法:
每一个类在实现时,都提供两个函数,一个构造函数,一个初始化函数,前者用户生成一个类,后者用于继承层次紧接本身的类来调用以便正确初始化虚函数表。依据这样的原则,一个派生类,只须要调用直接基类的初始化函数便可,每一个派生类都保证这一点,一切均可以进行下去。
下面是要实现的两个函数:
[cpp] view plain copy
struct derived *new_derived();
void initialize_derived(struct derived *d);
new 开头的函数做为构造函数, initialize 开头的函数做为 初始化函数。咱们看一下 new_derived 这个构造函数的实现框架:
[cpp] view plain copy
struct derived *new_derived()
{
struct derived * d = malloc(sizeof(struct derived));
initialize_base((struct base*)d);
initialize_derived(d);/* setup or modify VTABLE */
return d;
}
若是是 derived_2 的构造函数 new_derived_2,那么只须要调用 initialize_derived 便可。
说完了构造函数,对应的要说析构函数,并且析构函数要是虚函数。在删除一个对象时,须要从派生类的析构函数依次调用到继承层次最顶层的基类的析构函数。这点在 C 中也是能够保障的。作法是:给基类显式声明一个析构函数,基类的实现中查找虚函数表,从后往前调用便可。函数声明以下:
[cpp] view plain copy
struct base {
void ** vtable;
int vt_size;
void (*func_1)(struct base *b);
int (*func_2)(struct base *b, int x);
void (*deletor)(struct base *b);
};
说完构造、析构,该说这里的虚函数表究竟是怎么回事了。咱们先画个图,仍是以刚才的 base 、 derived 、derived_2 为例来讲明,一看图就明白了:
咱们假定 derived 类实现了三个虚函数, derived_2 类实现了两个,func_2 没有实现,上图就是 derived_2 的实例所拥有的最终的虚函数表,表的长度( vt_size )是 9 。若是是 derived 的实例,就没有表中的最后三项,表的长度( vt_size )是 6 。
必须限制的是:基类必须实现全部的虚函数,只有这样,这套实现机制才能够运转下去。由于一切的发生是从基类的实现函数进入,经过遍历虚函数表来找到派生类的实现函数的。
当咱们经过 base 类型的指针(实际指向 derived_2 的实例)来访问 func_1 时,基类实现的 func_1 会找到 VTABLE 中的 derived_2_func_1 进行调用。
好啦,到如今为止,基本说明白了实现原理,至于 初始化函数如何装配虚函数表、基类的虚函数实现,能够根据上面的思路写出代码来。按照个人这种方法实现的虚函数,经过基类指针访问,行为基本和 C++ 一致。
回顾一下:
网友评论:
initialize 开头的初始化函数是如何实现的?
就是给vtable分配空间,给函数指针赋值。主要干这两件事情。vtable是动态的,根据继承层次多少能够动态增加。