在《 C++ 编程思想》一书中对虚函数的实现机制有详细的描述,一般的编译器通过虚函数表,在编译时插入一段隐藏的代码,保存类型信息和虚函数地址,而在调用时,这段隐藏的代码可以找到和实际对象一致的虚函数实现。
我们在这里提供一个 C 中的实现,模仿 VTABLE 这种机制,但一切都需要我们自己在代码中装配。
之前在网上看到一篇描述 C 语言实现虚函数和多态的文章,谈到在基类中保存派生类的指针、在派生类中保存基类的指针来实现相互调用,保障基类、派生类在使用虚函数时的行为和 C++ 类似。我觉得这种方法有很大的局限性,不说继承层次的问题,单单是在基类中保存派生类指针这一做法,就已经违反了虚函数和多态的本意——多态就是要通过基类接口来使用派生类,如果基类还需要知道派生类的信息……。
我的基本思路是:
好了,先来看一点代码:
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++ 中编译器会自动调用继承层次上所有父(祖先)类的构造函数,也可以显式在派生类的构造函数的初始化列表中调用基类的构造函数。怎么办?
我们提供一个不那么优雅的解决办法:
每个类在实现时,都提供两个函数,一个构造函数,一个初始化函数,前者用户生成一个类,后者用于继承层次紧接自己的类来调用以便正确初始化虚函数表。依据这样的原则,一个派生类,只需要调用直接基类的初始化函数即可,每个派生类都保证这一点,一切都可以进行下去。
下面是要实现的两个函数:
struct derived *new_derived(); void initialize_derived(struct derived *d);new 开头的函数作为构造函数, initialize 开头的函数作为 初始化函数。我们看一下 new_derived 这个构造函数的实现框架:
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 中也是可以保障的。做法是:给基类显式声明一个析构函数,基类的实现中查找虚函数表,从后往前调用即可。函数声明如下:
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); };
我们假定 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++ 一致。
回顾一下: