C语言对象模型系列(二)从函数指针到虚函数表:彻底理解 C 的多态—— 为什么 device->ops->open() 看起来像 C++?
一、上一篇留下的最大问题
上一篇:
C语言对象模型系列(一)为什么 Linux / Android 系统里全是 struct + 函数指针?—— 一篇讲透 C 语言如何实现面向对象(OOP)
我们提到:
device->ops->open();
这种代码在:
- Linux 内核
- Android HAL
- 驱动
- JNI
- FFmpeg
里到处都是。
很多人第一次看到都会懵:
“这不是 C 吗?”
“为什么像对象调用方法一样?”
其实:
这一切的核心,都是函数指针。
而函数指针背后:
本质上就是 C 语言的“多态”。
二、先理解:什么叫“多态”?
很多人学 Java 时:
Animal animal = new Dog();
animal.run();
觉得很自然。
但其实这里发生了一件极其重要的事:“同一个调用,运行时决定具体执行哪个函数。”
比如:
animal.run();
真正执行的是:
Dog.run();
而不是:
Animal.run();
这就叫:
运行时动态分发(Dynamic Dispatch)
也就是:
多态(Polymorphism)
三、C 为什么没有多态?
因为 C 没有:
- virtual
- class
- override
- vtable
所以:
run();
在编译期就确定了。
也就是说:C 默认是静态调用。
四、但大型系统一定需要“动态调用”
比如:
不同驱动
摄像头驱动
蓝牙驱动
GPS驱动
都有:
open()
close()
read()
write()
但实现不同。
如果这样写:
if(type == CAMERA){
camera_open();
}else if(type == BLUETOOTH){
bluetooth_open();
}
会发生什么?
switch-case 地狱
问题:
- 耦合严重
- 扩展困难
- 新增设备必须修改原代码
这不符合:
开闭原则(OCP)
于是:
Linux 内核工程师想到了:“把函数当数据存起来。”
这就是:
函数指针。
五、函数指针到底是什么?
普通变量:
int a = 10;
保存的是:数据。
而函数:
void test(){}
其实也有地址。
函数名本质:就是函数地址。
比如:
void hello(){
printf("hello");
}
函数地址:
hello
于是:可以用变量保存函数地址。
六、最基础的函数指针
定义:
void (*func)(void);
看起来很吓人。
其实拆开:
| 部分 | 含义 |
|---|---|
| void | 返回值 |
| (*func) | func是指针 |
| (void) | 参数 |
意思:func 是一个“指向函数”的指针。
赋值:
func = hello;
调用:
func();
或者:
(*func)();
都可以。
七、现在开始进入“C 的多态”
第一步:定义统一接口
typedef struct {
void (*open)(void);
void (*close)(void);
} DeviceOps;
看到没有?
这里:函数指针被放进 struct 了。
这时候:
struct
+
function pointer
开始像:“对象 + 方法表”了。
八、不同设备绑定不同实现
摄像头
void camera_open(){
printf("camera open");
}
蓝牙
void bluetooth_open(){
printf("bluetooth open");
}
然后:
DeviceOps camera_ops = {
.open = camera_open
};
DeviceOps bluetooth_ops = {
.open = bluetooth_open
};
九、真正的关键来了
定义设备:
typedef struct {
DeviceOps* ops;
} Device;
绑定:
Device camera = {
.ops = &camera_ops
};
调用:
camera.ops->open();
输出:
camera open
再换:
Device bluetooth = {
.ops = &bluetooth_ops
};
调用:
bluetooth.ops->open();
输出:
bluetooth open
看到没有?
同样的调用:
device->ops->open();运行时执行不同函数。
这就是:C 语言版多态。
这其实就是“虚函数表”
很多人学 C++ 时:
class Animal {
virtual void run();
};
觉得 virtual 很神秘。
其实:底层本质和上面一模一样。
C++ 编译器偷偷帮你生成:
对象
↓
vptr
↓
vtable
↓
函数地址
而 C:是工程师自己手写。
所以:
device->ops->open();
本质其实就是:
对象 ↓ 函数表 ↓ 具体函数这就是:虚函数表(vtable)的核心思想。
十、为什么 Linux 特别喜欢这种设计?
因为它极其适合:“统一接口,不同实现”
例如:
file_operations
Linux 内核经典结构:
struct file_operations {
int (*open)(struct inode*, struct file*);
ssize_t (*read)(struct file*, char*, size_t);
};
不同驱动:
- read不同
- open不同
但:内核调用方式统一。
Android HAL 也是一样
比如:
camera module
gps module
audio module
Framework 根本不关心:
- 你是高通
- MTK
- 海思
它只管:
module->open();
十一、为什么这种设计比 switch-case 更高级?
因为:
它实现了:“行为和类型解耦”
以前:
类型决定行为
现在:
函数表决定行为
于是:
新增功能不需要改原代码。
这就是:
开闭原则(OCP)
十二、callback 本质也是这个思想
很多人学:
setOnClickListener()
觉得只是“回调”。
其实:
callback 本质也是函数指针。
比如:
void on_click(){
printf("clicked");
}
注册:
button->callback = on_click;
触发:
button->callback();
本质:
“运行时决定调用哪个函数”
依然是:多态。
十三、现在再回头看 JNI
经典 JNI:
(*env)->CallVoidMethod()
是不是突然顺眼了?
因为:JNIEnv 本质就是函数表。
也就是说:
env
↓
函数表
↓
CallVoidMethod
这和:
device->ops->open();
本质完全一致。
十四、一句话总结
函数指针让函数“可以像数据一样传递”
而:
struct + 函数指针
则让 C:
拥有了“运行时动态分发能力”。
这就是:
C语言实现多态的核心。
十五、最后
现在再回头看:
device->ops->open();
你会发现:
它已经不是:
“调用函数”
而是:
“对象通过函数表调用行为”
了。
所以很多 Linux / Android 系统代码:
虽然写的是 C,
但背后其实已经是:完整的 OOP 思维。
下一篇:
下一篇我们正式进入 JNI。
彻底讲透:
(*env)->CallVoidMethod()
为什么长这样。
以及:
- JNIEnv 为什么像对象
- 为什么 JNI 到处是二级指针
- Java 和 Native 如何互调
- JNI 为什么本质是 C 风格 OOP
真正把:
- Android
- JVM
- Native
- C对象模型
全部串起来。
AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。
更多推荐


所有评论(0)