一、上一篇留下的最大问题

上一篇:
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 思维。


下一篇:

《JNIEnv 为什么是二级指针?本质就是函数表》

下一篇我们正式进入 JNI。

彻底讲透:

(*env)->CallVoidMethod()

为什么长这样。

以及:

  • JNIEnv 为什么像对象
  • 为什么 JNI 到处是二级指针
  • Java 和 Native 如何互调
  • JNI 为什么本质是 C 风格 OOP

真正把:

  • Android
  • JVM
  • Native
  • C对象模型

全部串起来。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐