函数指针 + 结构体 = C 语言的“对象模型”

补充知识点:

从 C 对象模型看 JNI:一行 (*env)->CallVoidMethod 背后的系统级真相

从函数表到 JNIEnv:彻底看懂 JNI 中的二级指针、结构体函数表与 -> 语法糖

一、为什么 C 语言需要"对象模型"?

在 C 语言里,只有两种基本东西:

  • ✅ 数据(变量 / struct)

  • ✅ 函数(全局函数)

没有

  • class
  • method
  • interface
  • virtual
  • 多态

但系统软件(操作系统、虚拟机、驱动、中间件)必须要有

  • 抽象接口
  • 模块解耦
  • 运行时替换实现
  • 多态调用

于是,C 语言世界里诞生了一种"约定俗成"的结构:

👉 struct(保存状态) + 函数指针(保存行为)

这套组合,就是 C 语言的"对象模型"。

二、最小对象模型:状态 + 行为

先从一个极小例子开始,看 C 如何"造对象"。

🎯 目标:做一个"计数器对象"

我们希望它有:

  • 状态:value

  • 方法:inc() / get()

✅ C 语言实现:

cpp 复制代码
#include <stdio.h>

typedef struct Counter Counter;

struct Counter {
    int value;                              // 状态
    void (*inc)(Counter* self);             // 行为
    int  (*get)(Counter* self);
};

void counter_inc(Counter* self) {
    self->value++;
}

int counter_get(Counter* self) {
    return self->value;
}

int main() {
    Counter c;
    c.value = 0;
    c.inc = counter_inc;
    c.get = counter_get;

    c.inc(&c);
    c.inc(&c);
    printf("%d\n", c.get(&c)); // 2
}

🔍 这里发生了什么?

  • struct Counter 保存状态
  • 函数指针保存"方法"
  • self 参数 = C 里的 this
  • c.inc(&c) = 对象调用方法

👉 这已经是一个完整意义上的"对象"

三、为什么函数指针一定要带 self?

因为 C 的函数不属于任何对象。

void counter_inc(Counter* self)

是普通函数,不知道"我是谁"。

所以:

👉 必须手动把对象传进去。

这在系统层极其常见:

cpp 复制代码
int (*read)(struct Device* dev, void* buf, int len);
int (*start)(struct Engine* engine);

第一个参数,几乎永远是:

  • self
  • ctx
  • handle
  • object

👉 它就是"对象上下文"。

四、对象能力从哪来?三大核心特性

✅ 1. 封装(Encapsulation)

调用方不直接操作内部数据,只通过函数指针访问:

c.inc(&c);

✅ 2. 多态(Polymorphism)

同一个结构体接口,可以绑定不同实现:

cpp 复制代码
void inc1(Counter* c) { c->value += 1; }
void inc2(Counter* c) { c->value += 2; }

c.inc = inc1; // 普通计数器
c.inc = inc2; // 加速计数器

👉 调用代码不变,行为变化。

✅ 3. 解耦(Decoupling)

调用方不关心:

  • 函数叫什么
  • 函数在哪
  • 如何实现

它只认:

👉 这个 struct 里有哪些"能力"。

五、进阶:vtable(虚函数表)模型

系统层通常不会把函数指针直接塞进对象,

而是拆成两层:

  • 对象:状态

  • vtable:方法表(共享)

✅ 结构升级版:

cpp 复制代码
typedef struct Counter Counter;
typedef struct CounterVTable CounterVTable;

struct CounterVTable {
    void (*inc)(Counter* self);
    int  (*get)(Counter* self);
};

struct Counter {
    int value;
    const CounterVTable* vptr;
};

✅ 实现:

cpp 复制代码
void counter_inc(Counter* self) { self->value++; }
int  counter_get(Counter* self) { return self->value; }

const CounterVTable COUNTER_VT = {
    .inc = counter_inc,
    .get = counter_get
};

✅ 使用:

cpp 复制代码
Counter c = { .value = 0, .vptr = &COUNTER_VT };
c.vptr->inc(&c);
printf("%d\n", c.vptr->get(&c));

👉 这已经和 C++ 虚函数机制几乎完全一致

  • 对象里有 vptr
  • vptr 指向函数表
  • 调用 = 查表 + 跳转

六、这套模型正是系统世界的通用设计

✅ Linux 内核

cpp 复制代码
struct file_operations {
    int (*open)(struct inode*, struct file*);
    ssize_t (*read)(struct file*, char*, size_t, loff_t*);
    ssize_t (*write)(...);
};

不同文件系统 → 填不同 ops 表。

✅ HAL / 驱动 / 中间件

cpp 复制代码
struct audio_hw_device {
    int (*init)(...);
    int (*start)(...);
    int (*stop)(...);
};

✅ JNI(你非常熟)

cpp 复制代码
struct JNINativeInterface_ {
    jclass (*FindClass)(JNIEnv*, const char*);
    jobject (*CallObjectMethod)(JNIEnv*, jobject, jmethodID, ...);
    ...
};

(*env)->FindClass(env, "java/lang/String");

👉 典型:vtable + self + 查表调用。

七、统一抽象视角(非常重要)

以后你看到任何系统接口,只要问三件事:

  1. 这个 struct 是不是"对象"?
  2. 这些函数指针是不是"方法表"?
  3. 第一个参数是不是 self / ctx?

如果是 →

👉 这就是 C 语言对象模型。

八、和 C++ / Java 的一一对应

C 系统层 C++ / Java
struct class
函数指针 method
self 参数 this
vtable 虚函数表
ops 结构体 interface
运行时赋值 多态

👉 所谓"面向对象",在底层几乎都落回这一套。

九、一句话系统级总结

C 没有类,

但用 struct 保存状态,

用函数指针保存行为,

用 vtable 共享方法,

就造出了整个系统世界。

Linux、Android、JVM、驱动、NDK,没有例外。

下一篇:

从 C 对象模型 → JNI → HAL → Linux 内核接口------一条贯穿系统软件的完整认知链

相关推荐
季明洵2 天前
C语言实现顺序表
数据结构·算法·c·顺序表
日更嵌入式的打工仔7 天前
C 语言 restrict 关键字
c
REDcker10 天前
OpenSSL 完整文档
c++·安全·github·c·openssl·后端开发
橘颂TA11 天前
【剑斩OFFER】算法的暴力美学——力扣 675 题:为高尔夫比赛砍树
数据结构·算法·c·结构与算法
程芯带你刷C语言简单算法题14 天前
Day48~对于高度为 n 的台阶,从下往上走,每一步的阶数为 1,2,3 中的一个。问要走到顶部一共有多少种走法
c语言·开发语言·学习·算法·c
余衫马17 天前
为什么在 Windows 上用 Clang/LLVM?
c++·windows·c
REDcker19 天前
AIGCJson 库介绍与使用指南
c++·json·aigc·c
REDcker20 天前
RTCP 刀尖点跟随技术详解
c++·机器人·操作系统·嵌入式·c·数控·机床
消失的旧时光-194322 天前
函数指针 + 结构体 = C 语言的“对象模型”?——从 C 到 C++ / Java 的本质统一
linux·c语言·开发语言·c++·c