很多人第一次看到 Linux 内核源码时,都会有点懵。
比如:
cpp
struct file_operations
{
int (*open)(struct inode *, struct file *);
ssize_t (*read)(struct file *, char __user *, size_t, loff_t *);
ssize_t (*write)(struct file *, const char __user *, size_t, loff_t *);
};
会忍不住想:
- 为什么结构体里面全是函数指针?
- 为什么 Linux 内核喜欢这么写?
- 这和 C++ 虚函数有什么关系?
- C 语言不是没有面向对象吗?
其实:
Linux / Android Framework / 驱动 / HAL 里面,大量都在使用:struct + 函数指针 这一套东西。
而这背后,本质上就是:**C 语言实现"面向对象"**的方式。
今天我们就从 0 开始,一步一步看看:
C 是怎么"逼近 OOP(面向对象)"的。
一、C 语言真的不能面向对象吗?
很多人觉得:
C 没有:
- class
- interface
- virtual
- inheritance
所以:
C 不支持面向对象。
但奇怪的是:
Linux 内核明明是 C 写的, 却:到处都是"对象" 。
比如:
- file
- inode
- device
- driver
- socket
甚至:
行为还能动态切换。
这其实说明:面向对象 ≠ 必须有 class 。
真正的 OOP 核心:
其实是:数据 + 行为。
而 C: 其实也能做到。
二、第一步:最朴素的"对象"
先来看最简单的结构体。
cpp
struct Animal
{
char name[32];
int age;
};
这里:Animal
已经有点:对象 的味道了。
因为:它把 数据 组织在了一起。
比如:struct Animal dog;
但问题来了。真正的对象:除了数据,还需要:行为 。
比如:
- speak
- eat
- sleep
怎么办?
三、第二步:把"行为"放进结构体
于是:
有人想到了:函数指针 。
比如:
cpp
#include <stdio.h>
struct Animal
{
char name[32];
void (*speak)(struct Animal *);
};
void dogSpeak(struct Animal *animal)
{
printf("%s: wang wang\n", animal->name);
}
int main()
{
struct Animal dog;
dog.speak = dogSpeak;
dog.speak(&dog);
return 0;
}
这里最关键的一句:
dog.speak(&dog);
你会突然发现:
它已经开始像:
dog.speak();
了。
这其实已经是:
"对象 + 方法"
的雏形了。
四、问题出现了:函数越来越多
但很快,问题就来了。
比如:
cpp
struct Animal
{
char name[32];
void (*speak)(struct Animal *);
void (*sleep)(struct Animal *);
void (*eat)(struct Animal *);
void (*run)(struct Animal *);
void (*jump)(struct Animal *);
};
一开始感觉还行。
但:
如果函数越来越多:
speak
sleep
eat
run
jump
...
那结构体会越来越大。
这时候,第一个问题出现了。
问题1:对象越来越大
比如:10000 个 Animal 对象
那每个对象 都要保存:
speak
sleep
eat
run
jump
这些函数地址。
而函数指针:本质上也是内存。
于是:对象会越来越臃肿。
问题2:大量重复存储
更关键的是:
这些函数地址:
很多其实都一样。
比如:
cpp
dog1.speak = dogSpeak;
dog2.speak = dogSpeak;
dog3.speak = dogSpeak;
所有 Dog:都指向:
dogSpeak
也就是说:
每个对象都在重复保存同一套函数地址
这其实是一种:内存浪费 。
五、于是:真正的优化出现了
既然:行为函数 是共享的。
那为什么不单独抽出来?
于是:Linux 内核里经典的:
Ops(Operation)设计
出现了。
第一步:单独抽一个"行为表"
cpp
struct AnimalOps
{
void (*speak)(struct Animal *);
void (*sleep)(struct Animal *);
void (*eat)(struct Animal *);
};
第二步:对象只保留一个 ops 指针
cpp
struct Animal
{
char name[32];
struct AnimalOps *ops;
};
第三步:统一调用
animal->ops->speak(animal);
这时候,整个设计一下就高级了。
六、为什么这种设计更高级?
这一步,其实已经非常接近:真正的面向对象设计 了。
因为它解决了很多问题。
七、好处1:节省内存
现在:
所有 Dog:共享同一套函数表。
比如:
cpp
struct AnimalOps dogOps =
{
.speak = dogSpeak,
.sleep = dogSleep,
.eat = dogEat
};
然后:
cpp
dog1.ops = &dogOps;
dog2.ops = &dogOps;
dog3.ops = &dogOps;
现在:每个对象 只需要保存:一个 ops 指针 。
而不是:一堆函数指针。
八、好处2:更像"接口"
Animal
负责:数据
AnimalOps
负责:行为
你会发现:这已经非常像 Java 里的:interface 了。
也就是说:对象与行为开始解耦 。
九、好处3:支持"多态"
比如:
cpp
animal->ops->speak(animal);
Dog:
可能执行:
dogSpeak()
Cat:
可能执行:
catSpeak()
虽然:调用方式完全一样。
但:运行结果不同。
这其实就是:
运行时多态
而 这恰恰是:面向对象最核心的能力之一。
十、好处4:更容易扩展
新增行为:
只需要:扩展 Ops。
新增对象:
只需要:替换函数实现
整个系统:扩展性会非常强。
这也是 为什么:大型系统特别喜欢这种设计。
十一、看到 Linux 内核源码时,突然悟了
再回头看 Linux:
cpp
struct file_operations
{
open
read
write
ioctl
};
是不是一下就懂了?
原来 Linux 内核:本质上也是:
对象
↓
ops
↓
行为函数表
这一套。
实际上Linux 里 大量都是这种设计。
比如:
- file_operations
- inode_operations
- net_device_ops
- driver ops
本质 全是:
数据 + 行为表
十二、原来 C++ 虚函数表底层也是这个
很多人觉得:C++ virtual 很高级
但实际上:它底层 本质也是:函数指针表
只不过 C++ 编译器:
帮你自动生成了:
- vtable(虚函数表)
- vptr(虚表指针)
本质上 和:
cpp
struct AnimalOps *ops;
这个思路,几乎一样。
十三、真正厉害的,不是函数指针技巧
很多人学到这里 会以为:
重点是:函数指针技巧 。
其实不是。
真正厉害的是 这一整套:
发现问题
↓
抽象行为
↓
解耦对象与行为
↓
函数表共享
↓
实现运行时多态
的设计思想。
你会发现 这其实已经是:现代面向对象设计 了。
而 Linux 内核、驱动框架、Android HAL、很多系统级架构:本质上都建立在这套思想之上。
十四、最终总结(一句话讲透)
很多人以为:
面向对象 = class
但实际上:
真正的面向对象核心:
数据 + 行为 + 抽象 + 多态
而:Linux 内核用 C 语言,通过:
struct + 函数指针表
硬生生实现了一整套:"系统级面向对象模型"。
这也是为什么 很多人会说:真正理解 C,才能真正理解系统。