在《 C++ 编程思想》一书中对虚函数的实现机制有详细的描述,一般的编译器通过虚函数表,在编译时插入一段隐藏的代码,保存类型信息和虚函数地址,而在调用时,这段隐藏的代码可以找到和实际对象一致的虚函数实现。

我们在这里提供一个 C 中的实现,模仿 VTABLE 这种机制,但一切都需要我们自己在代码中装配。

之前在网上看到一篇描述 C 语言实现虚函数和多态的文章,谈到在基类中保存派生类的指针、在派生类中保存基类的指针来实现相互调用,保障基类、派生类在使用虚函数时的行为和 C++ 类似。我觉得这种方法有很大的局限性,不说继承层次的问题,单单是在基类中保存派生类指针这一做法,就已经违反了虚函数和多态的本意——多态就是要通过基类接口来使用派生类,如果基类还需要知道派生类的信息……。

我的基本思路是:

  • 在“基类”中显式声明一个 void** 成员,作为数组保存基类定义的所有函数指针,同时声明一个 int 类型的成员,指明 void* 数组的长度。
  • “基类”定义的每个函数指针在数组中的位置、顺序是固定的,这是约定,必须的
  • 每个“派生类”都必须填充基类的函数指针数组(可能要动态增长),没有重写虚函数时,对应位置置 0
  • “基类”的函数实现中,遍历函数指针数组,找到继承层次中的最后一个非 0 的函数指针,就是实际应该调用的和对象相对应的函数实现

好了,先来看一点代码:

[cpp] view plaincopy print?
  1. struct base {
  2. void ** vtable;
  3. int vt_size;
  4. void (*func_1)(struct base *b);
  5. int (*func_2)(struct base *b, int x);
  6. };
  7. struct derived {
  8. struct base b;
  9. int i;
  10. };
  11. struct derived_2{
  12. struct derived d;
  13. char *name;
  14. };
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 plaincopy print?
  1. struct derived *new_derived();
  2. void initialize_derived(struct derived *d);
struct derived *new_derived();
void initialize_derived(struct derived *d);

new 开头的函数作为构造函数, initialize 开头的函数作为 初始化函数。我们看一下 new_derived 这个构造函数的实现框架:

[cpp] view plaincopy print?
  1. struct derived *new_derived()
  2. {
  3. struct derived * d = malloc(sizeof(struct derived));
  4. initialize_base((struct base*)d);
  5. initialize_derived(d);/* setup or modify VTABLE */
  6. return d;
  7. }
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 plaincopy print?
  1. struct base {
  2. void ** vtable;
  3. int vt_size;
  4. void (*func_1)(struct base *b);
  5. int (*func_2)(struct base *b, int x);
  6. void (*deletor)(struct base *b);
  7. };
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++ 一致。

C语言面向对象编程(三):虚函数与多态相关推荐

  1. c 语言绘图函数,c语言图形编程(三、绘图函数-)(C language graphics programming (three, drawing function -)).doc...

    c语言图形编程(三.绘图函数-)(C language graphics programming (three, drawing function -)).doc c语言图形编程(三.绘图函数-01) ...

  2. C语言图形编程(绘图函数部分),C语言图形编程(三、绘图函数-02)12

    C语言图形编程(三.绘图函数-02)12 } 84. putimage() 输出图像函数 功能: 函数putimage()将一个先前保存在内存中的图像输出到屏幕上. 用法: 此函数调用方式为void ...

  3. C语言面向对象编程(四):面向接口编程

    Java 中有 interface 关键字,C++ 中有抽象类或纯虚类可以与 interface 比拟,C 语言中也可以实现类似的特性. 在面试 Java 程序员时我经常问的一个问题是:接口和抽象类有 ...

  4. C语言面向对象编程(二):继承详解

    在  C 语言面向对象编程(一)里说到继承,这里再详细说一下. C++ 中的继承,从派生类与基类的关系来看(出于对比 C 与 C++,只说公有继承): 派生类内部可以直接使用基类的 public .p ...

  5. 我所偏爱的 C 语言面向对象编程范式

    我所偏爱的 C 语言面向对象编程范式 面向对象编程不是银弹.大部分场合,我对面向对象的使用非常谨慎,能不用则不用.相关的讨论就不展开了. 但是,某些场合下,采用面向对象的确是比较好的方案.比如 UI ...

  6. c语言面向对象编程中的类_C ++中的面向对象编程

    c语言面向对象编程中的类 Object oriented programming, OOP for short, aims to implement real world entities like ...

  7. C/C++编程:虚函数与纯虚函数

    虚函数 VS 纯虚函数 虚函数 虚函数是应在派生类中重新定义的函数.当使用指针或者对基类的引用来引用派生类的对象时,可以为该对象调用虚函数并执行该派生类的版本. 虚函数的"虚",虚 ...

  8. 子类重写父类虚函数_C/C++编程笔记:关于C++的虚函数和多态,你真的了解吗?...

    前言 本章节主要针对于C++中的虚函数和多态做一个详细介绍. 虚函数 虚函数的长相其实很简单,在C++类型用virtual修饰的函数就是虚函数,如下代码: 虚函数对于本类的影响:存在虚函数类的内存会多 ...

  9. java 168转换成861_java实验-java语言面向对象编程基础

    java实验-java语言面向对象编程基础 (12页) 本资源提供全文预览,点击全文预览即可全文预览,如果喜欢文档就下载吧,查找使用更方便哦! 8.90 积分 广州大学学生实验报告广州大学学生实验报告 ...

最新文章

  1. JSP与servlets的区别
  2. 7、linux网络编程--广播
  3. 裸机篇 -- S5PV210的中断体系
  4. Python 常用函数 configparser模块
  5. [转] Java, 使用 Reactor 进行反应式编程
  6. java城市级联一次查询_我的城市没有任何设计活动,所以我自己组织了一次。...
  7. PyCharm光标变粗的解决办法
  8. 技术大神有话说,别让新业绩为旧设备背锅
  9. 关于Chrome出现Provisional headers are shown无法正常访问的解决方案(其他firefox,360, IE访问正常) (转)...
  10. java没有更新_java – JProgressBar没有更新,找不到线索
  11. 元素与集合的问题思考
  12. ​炸裂!万字长文拿下 HTTP 我在字节跳动等你!
  13. CUDA By Examples 0 - 准备工作
  14. freeswitch呼叫系统
  15. umts是移动还是联通_网络模式中的UMTS是什么意思?
  16. mysql-mmm高可用群集
  17. latch 深入理解(转载)
  18. Linux redis ipv6,linux centOS 开启ipv6
  19. android解决kotlin问题Expecting member declaration
  20. ubuntu关机、重启、注销命令行指令

热门文章

  1. jenkins 手动执行_我常用的SpringBoot+Jenkins自动化部署技巧,贼好用,推荐给大家...
  2. python最小公倍数 菜鸟_最小公倍数 golang + python
  3. php smart模板,vaphp整合smart模板有关问题
  4. java classloader_Java Classloader原理分析
  5. python watchdog 同时检测到多个事件_python中watchdog文件监控与检测上传功能
  6. 《mysql必知必会》学习_第11章_20180801_欢
  7. Dijkstra 最短路
  8. SQL Server强制使用特定索引 、并行度、锁
  9. js获取iframe里的元素
  10. 主机和虚拟机ping不通的原因