「嵌入式结构体 + 反向父指针」设计模式
今天来总结一下C语言中常用的「嵌入式结构体 + 反向父指针」设计模式
以我自己写的代码为例子:
1 | struct AbCanvasGTK { |
上述代码在内存中的实际分布和关系如下:
1 | GtkGfx 对象整体地址:0x55a1b2c30000 ~ 0x55a1b2c30027 (40 字节) |
大块内存包含小块内存,小块内存里存着大块内存的入口地址
这种操作:**「父结构体包含子结构体实体 + 子结构体存指针反向指向父」**的操作,不仅非常多,更是 C 语言大型项目(尤其是 Linux 内核、底层库、GUI、服务端程序)的经典标准设计范式,有专门的名字,叫 「嵌入式结构体 + 反向父指针」(Linux 内核还延伸出了更极致的 container_of 模式)
结构范式
先来将这这操作抽象成为一个统一的范式:
结构范式
1 | // 子结构体:轻量级,只做功能入口,反向引用父 |
目的:拿到轻量级的子对象,就能快速找到管理所有资源的父对象,同时通过「实体嵌入」实现内存一体化、生命周期统一
注意:以下示例为AI生成,没有做真正的源码追踪!
Linux 内核示例
Linux 内核:把这种模式用到极致,Linux 内核几乎处处都是这种设计,甚至为了优化,延伸出了 **container_of 宏 **(不额外存父指针,靠地址偏移直接找父,思想和上述代码代码完全一致),是内核模块化、面向对象设计的基础。
内核大概有两种范式:
- 一种是子直接存父指针
- 另一种内核更常用:子不存指针,用 container_of 宏算父地址(省内存,作用相同)
「反向找父」神器:container_of 宏
内核很少像上述代码一样直接存 parent 指针(为了省 8 字节),而是用 container_of,作用和上述代码 parent 完全一样,只是通过「子结构体的地址 + 子在父中的偏移」,直接算出父结构体的地址。
内核container_of定义(简化)
1 |
ptr:子结构体的指针type:父结构体类型member:子结构体在父中的成员名- 作用:拿到子,就能找到父,和上述代码用
c->parent完全等价。
1. 内核双向链表 struct list_head
这是内核最基础的数据结构,所有内核链表都用它,完美匹配上述代码模式:
1 | // 子结构体:链表节点,极简,嵌入到任意父结构体 |
和上述代码代码的共性:
- 父包含子实体:
task_struct实体嵌入list_head,同一块内存,同生同死。 - 子反向找父:内核通过
container_of宏,从list_head指针,直接计算出外层task_struct的地址(替代上述代码代码里直接存parent,更省内存)。 - 用途:内核通过链表节点,管理成千上万个进程,拿到节点就能找到完整的进程描述符。
实际用法
1 | // 拿到一个链表节点(子),相当于上述代码拿到 AbCanvasGTK* |
- 用途
和上述代码用 AbCanvasGTK 作为绘图入口一样,内核用 list_head 作为链表操作的入口,只传递轻量的子节点,就能操作父的全部资源,全内核(进程、文件、设备、网络)都在用。
2. 内核设备模型 struct device
Linux 设备驱动的核心,和上述代码 GTK 绘图场景(资源管理 + 子对象入口)几乎一模一样:
1 | // 子结构体:设备基础对象,抽象入口 |
和上述代码代码的共性:
- 上层驱动只操作轻量的
struct device,和上述代码绘图时只操作AbCanvasGTK一致。 - 通过
dev.parent反向找到父设备,和上述代码通过AbCanvasGTK.parent 找到GtkGfx` 一致。 - 资源全在父结构体,子结构体只做入口,解耦上层和底层。
3. PCI 设备模型 struct pci_dev + struct device(直接存父指针)
这个是和上述代码代码写法 100% 相同的例子:子结构体直接存父指针,用于「抽象接口 + 具体资源」,和上述代码「抽象画布 + 绘图资源上下文」是同一个设计目的。
- 内核源码(简化)
1 | // 子结构体:通用设备抽象层 |
- 和上述代码代码一一映射(完全一致)
| 上述代码代码 | 内核 PCI 设备代码 | 完全一致的角色 |
|---|---|---|
AbCanvasGTK |
struct device |
抽象层子结构体,对外提供统一接口 |
GtkGfx |
struct pci_dev |
具体实现父结构体,管理所有底层资源 |
struct AbCanvasGTK { struct GtkGfx *owner; } |
struct device { struct device *parent; } |
子直接存父指针,反向引用 |
g->s_canvas |
pdev->dev |
父实体嵌入子,内存一体 |
- 实际用法(和上述代码点击按钮、绘图找上下文完全一样)
1 | // 拿到抽象设备(子),相当于上述代码拿到 AbCanvasGTK* |
- 用途
和上述代码设计目的完全相同:
- 上层只操作抽象的
struct device(上述代码操作抽象画布),不用关心是 PCI、USB、串口设备。 - 底层资源都在具体的
struct pci_dev里(上述代码在 GtkGfx 里)。 - 子的父指针,让上层能随时找到底层资源。
4. 网络子系统 struct socket + struct sock
上述代码代码是「应用层绘图接口 → GTK 底层实现」,内核网络是「用户态 Socket 接口 → 内核网络实现」,结构完全一致。
- 内核源码(简化)
1 | // 子结构体:给用户态的抽象Socket接口 |
- 核心逻辑
和上述代码一模一样:
- 用户 / 上层操作轻量的
struct socket(上述代码操作AbCanvasGTK)。 - 父
struct sock管理所有网络资源(上述代码GtkGfx管理绘图资源)。 - 子通过
sk指针找到父(上述代码通过`parent)。
5. 文件系统 struct inode
文件系统是内核的核心模块,同样用这种模式。
- 内核源码(简化)
1 | // 子结构体:文件地址空间抽象 |
- 逻辑
- 子
address_space是文件缓存的抽象入口。 - 父
inode管理文件所有资源。 - 子通过
host指针找到父,和上述代码parent完全一样。
文件系统 struct file
1 | // 子结构体:文件抽象,给进程用的轻量入口 |
同样是资源放在父,子做入口,反向引用的逻辑。
内核示例总结
上述代码代码和内核代码的关系
| GTK 绘图模块 | Linux 内核对应模块 | 设计思想完全一致 |
|---|---|---|
抽象画布 AbCanvasGTK |
链表节点list_head、抽象设备device、抽象 Socketsocket |
轻量子结构体,对外提供统一接口 |
绘图资源上下文 GtkGfx |
进程task_struct、PCI 设备pci_dev、网络sock |
父结构体,管理所有底层资源 |
子结构体存owner指针 |
子存parent/sk/host指针,或container_of |
子反向定位父,获取全部资源 |
父实体嵌入子s_canvas |
父实体嵌入tasks/dev/socket |
内存一体化,生命周期统一,避 |
其他C 项目示例
除了 Linux 内核,所有工业级 C 项目,只要涉及复杂资源管理、模块化、抽象接口,都会用这种模式,和上述代码 GTK 代码同源。
GLib 库
GTK 本身就是基于 GLib 开发的,GLib 的GObject 系统(GTK 控件的基础),核心就是这种模式:
1 | // 子结构体:GObject,所有GTK控件的基础,轻量抽象 |
上述代码 GTK 代码,本质是 GObject 模式的简化版。
Nginx
Nginx 的核心连接、事件模型,大量使用嵌入式结构体 + 反向指针:
1 | // 子结构体:事件对象,轻量级,事件入口 |
- 拿到事件
ngx_event_t,通过conn找到完整的连接资源,和上述代码拿到画布找到绘图上下文完全一致。
SQLite
SQLite 的存储、分页模块,核心结构也是如此:
1 | // 子结构体:页面缓存入口,轻量 |
SDL
SDL 的绘图、窗口系统,和上述代码 GTK 绘图代码设计几乎一样:
1 | // 子结构体:渲染纹理,轻量绘图入口 |
总结
「嵌入式结构体 + 反向父指针」设计模式解决了 C 语言没有面向对象、没有自动内存管理、没有继承的痛点
- C 语言实现「组合 / 复用」,模拟面向对象
C 没有类和继承,通过 「父结构体嵌入子结构体」,实现**「组合」**—— 把通用功能(画布、链表、事件、设备)封装成子结构体,复用到所有父结构体中,和面向对象的「继承 / 组合」效果一致。
- 解耦上层接口和底层资源,接口更简洁
- 上层只需要传递轻量的子对象(上述代码
AbCanvasGTK、内核的list_head、Nginx的ngx_event_t),不用传递臃肿的父对象。 - 底层通过反向指针,随时拿到父对象的所有资源,上层简洁,底层强大。
- 内存极致高效,生命周期绝对安全
- 子结构体是实体嵌入,和父结构体共用同一块堆内存,不需要额外
malloc,减少内存碎片和分配开销。 - 销毁父结构体时,子结构体自动同步销毁,不需要单独释放,彻底避免内存泄漏。
- 反向引用解决「由子寻父」的刚需
程序中,往往只能拿到功能入口的子对象(画布、事件、链表节点),但所有资源都在父对象里。