C语言对象模型系列(二)从函数指针到虚函数表:彻底理解 C 的多态—— 为什么 device->ops->open() 看起来像 C++?
一、上一篇留下的最大问题上一篇C语言对象模型系列一为什么 Linux / Android 系统里全是 struct 函数指针—— 一篇讲透 C 语言如何实现面向对象OOP我们提到device-ops-open();这种代码在Linux 内核Android HAL驱动JNIFFmpeg里到处都是。很多人第一次看到都会懵“这不是 C 吗”“为什么像对象调用方法一样”其实这一切的核心都是函数指针。而函数指针背后本质上就是C 语言的“多态”。二、先理解什么叫“多态”很多人学 Java 时Animal animal new Dog(); animal.run();觉得很自然。但其实这里发生了一件极其重要的事“同一个调用运行时决定具体执行哪个函数。”比如animal.run();真正执行的是Dog.run();而不是Animal.run();这就叫运行时动态分发Dynamic Dispatch也就是多态Polymorphism三、C 为什么没有多态因为 C 没有virtualclassoverridevtable所以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_operationsLinux 内核经典结构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 moduleFramework 根本不关心你是高通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 思维。下一篇《JNIEnv 为什么是二级指针本质就是函数表》下一篇我们正式进入 JNI。彻底讲透(*env)-CallVoidMethod()为什么长这样。以及JNIEnv 为什么像对象为什么 JNI 到处是二级指针Java 和 Native 如何互调JNI 为什么本质是 C 风格 OOP真正把AndroidJVMNativeC对象模型全部串起来。