C 语言如何实现“面向对象”?—— 从 struct + 函数指针,到 Linux 内核设计思想

很多人第一次看到 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,才能真正理解系统。

相关推荐
handler011 小时前
滑动窗口(同向双指针)算法:模板与例题解析
c语言·c++·笔记·算法·蓝桥杯·双指针·滑动窗口
不做无法实现的梦~1 小时前
Linux 新手到日常运维操作指南
linux·运维·服务器
xingfujie1 小时前
第3章 安装 kubeadm/kubelet/kubectl
linux·云原生·容器·kubernetes·kubelet
不能隔夜的咖喱1 小时前
黑马ai大模型笔记(自用,比较粗糙)
linux·windows·python
暴力求解1 小时前
Linux--网络-->UDP_socket
linux·网络·网络协议·udp·操作系统
小短腿的代码世界1 小时前
Qt时间日期处理与QTimer高级应用:从毫秒级精度到跨平台定时器的完整架构解析
开发语言·qt·架构
无限进步_1 小时前
Linux指令实战:40+核心命令的用法与思维模型
linux·服务器·前端
ZZZKKKRTSAE1 小时前
一篇猛攻zabbix
linux·运维·zabbix·redhat·rhel9
TAN-90°-1 小时前
Java 6——成员变量初始值 object equals和== toString instanceof 参数传递问题
java·开发语言