一、C语言与设计模式概述

大家好,我是你们的老朋友。今天咱们聊聊C语言和设计模式。

很多人觉得,设计模式是Java、C++这些面向对象语言的专利。C语言?过程式语言,跟设计模式八竿子打不着。我以前也这么想,直到我在一个嵌入式项目中吃了大亏。

那是一个通信协议栈的项目,代码量大概五万行。一开始写得挺爽,后来需求一变,改一处要动全身。我花了整整两周重构,才把代码理顺。从那以后,我开始认真思考:C语言到底需不需要设计模式?

1.1 C语言特性回顾

咱们先快速过一下C语言的核心特性。这些是后面所有模式的基础。

  • 指针:C语言的灵魂。函数指针更是实现回调、多态的关键。
  • 结构体:把数据打包。配合函数指针,就能模拟出“类”的效果。
  • 宏定义:预处理阶段的“代码生成器”。用好了能减少大量重复代码。
  • 模块化:.h文件声明接口,.c文件隐藏实现。这就是信息隐藏。
  • 内存管理:malloc/free。没有垃圾回收,一切靠自己。

你想想看,这些特性其实已经为设计模式埋下了伏笔。指针可以指向不同的函数,结构体可以包含不同的数据——这不就是多态和封装吗?

核心观点:C语言虽然没有class关键字,但通过“结构体+函数指针”的组合,完全可以实现面向对象的核心机制。

1.2 设计模式概念

设计模式是什么?说白了,就是前人踩过的坑、总结出来的套路。

GoF那本书里定义了23种经典模式。但别被数字吓到。实际项目中常用的,也就那么七八种。我个人的习惯是:先理解思想,再套用模式,而不是反过来。

设计模式有三个核心要素:

  1. 上下文:这个模式在什么场景下用?
  2. 问题:它要解决什么痛点?
  3. 解决方案:具体的代码结构是怎样的?

举个例子。你写了一个串口驱动,上层应用要接收数据。最简单的做法是轮询——但这样CPU就被占死了。怎么办?回调函数。这就是观察者模式的雏形。

我的经验:不要为了用模式而用模式。我见过有人在一个只有几百行代码的驱动里硬套了三个设计模式,结果代码比原来还难维护。模式是工具,不是目的。

1.3 为什么C语言需要设计模式

这个问题我问过很多同行。答案五花八门。我总结一下:

痛点 没有模式时 用了模式后
代码耦合度高 改一个模块,牵连一片 接口隔离,各管各的
扩展性差 加功能要改原有代码 开闭原则,加新模块就行
可读性低 一堆if-else,逻辑混乱 结构清晰,一看就懂
复用困难 代码和业务强绑定 模块解耦,到处能用

我曾经接手过一个老项目,里面有一个状态机,用switch-case写了三百多行。每次加一个状态,就要在好几个case里加代码。后来我把它重构为状态模式,每个状态一个文件。嗯,从那以后,加状态再也不用提心吊胆了。

避坑指南:C语言没有异常处理机制。设计模式中的错误处理,一定要通过返回值或错误码来传递。我曾经在回调函数里直接exit(),结果整个系统崩了。血的教训。

1.4 软件架构基础

设计模式是微观层面的。架构是宏观层面的。两者相辅相成。

嵌入式软件架构,常见的有这么几种:

  • 分层架构:驱动层、中间件层、应用层。层与层之间通过接口通信。
  • 事件驱动架构:通过事件队列解耦各个模块。适合GUI、通信协议等场景。
  • 微内核架构:核心系统只提供最基本的功能,其他都做成插件。比如一些RTOS就是这么设计的。

我个人比较喜欢分层架构。原因很简单:每一层只关心自己的事。驱动层只管读写寄存器,中间件层只管协议解析,应用层只管业务逻辑。出了问题,定位也快。

下面这张图,是我对本章知识体系的一个梳理:

C语言与设计模式知识体系 C语言特性 指针 · 结构体 · 宏 · 模块化 设计模式概念 上下文 · 问题 · 解决方案 为什么需要 解耦 · 扩展 · 可读 · 复用 软件架构基础 分层架构 · 事件驱动 · 微内核 代码示例 结构体+函数指针 避坑指南 错误处理 · 内存管理 个人经验 项目实战 · 重构心得 核心:用C语言的特性,实现设计模式的思想

从这张图可以看出,C语言特性是地基,设计模式是框架,架构是蓝图。三者缺一不可。

1.5 一个简单的例子

光说不练假把式。咱们看一段代码。假设你要实现一个LED驱动,支持开关和闪烁。

// led.h
typedef struct {
    void (*turn_on)(void);
    void (*turn_off)(void);
    void (*blink)(int interval_ms);
} LedOps;

// led_gpio.c
static void gpio_on(void) {
    // 写GPIO寄存器
    GPIO_PORT->BSRR = LED_PIN;
}

static void gpio_off(void) {
    GPIO_PORT->BRR = LED_PIN;
}

static void gpio_blink(int interval_ms) {
    // 定时器控制
}

LedOps led_gpio = {
    .turn_on = gpio_on,
    .turn_off = gpio_off,
    .blink = gpio_blink
};

你看,通过结构体+函数指针,我们把操作封装起来了。上层应用只需要调用led_gpio.turn_on(),根本不用关心底层是GPIO还是I2C。这就是策略模式的雏形。

小技巧:我习惯在结构体里再加一个void *priv字段,用来存放私有数据。这样每个实例可以有自己的状态,更接近面向对象的感觉。

好了,这一章就到这里。记住一句话:设计模式不是银弹,但它能帮你写出更干净、更可维护的C代码。后面我们会一个一个模式地拆解,结合真实项目案例,让你真正掌握它们。


公众号:蓝海资料掘金营,微信deep3321