4. 开发环境搭建

各位同学,咱们今天聊的这四条规则,说白了就是一件事:你的代码能不能在目标编译器上老老实实地跑。MISRA C 在这块卡得非常死,因为它面向的是嵌入式、汽车、航空这些领域——代码出了错,可不是蓝屏重启那么简单。

我个人习惯,拿到一个新项目,第一件事就是翻编译器的文档。你想想看,如果连编译器支持什么、不支持什么都不知道,后面写再多规范检查也是白搭。

1.1 规则:所有代码必须符合C90标准

这条规则的核心就一句话:别用C99及以后的新特性。为什么?因为很多嵌入式编译器到现在还在用C90的子集,甚至有些编译器连完整的C90都支持不全。

我在项目中遇到过一件事:有个同事用了C99的//风格注释,结果移植到某款老牌编译器时,编译直接报错。那款编译器只认/* */。你说冤不冤?

规则1.1要点

  • 代码必须严格遵循C90(ISO/IEC 9899:1990)
  • 禁止使用C99引入的特性://注释、stdint.h、bool类型、可变长数组等
  • 禁止使用C11/C18的特性:泛型选择、_Static_assert、匿名结构体等

你可能会问:那stdint.h里的uint32_t也不能用?嗯,这里要注意——MISRA C:2012 其实允许用stdint.h,但前提是你的编译器支持它。如果编译器不支持,你就得自己用typedef定义。

/* 合规写法:如果编译器不支持 stdint.h */
typedef unsigned long uint32_t;
typedef unsigned char uint8_t;

/* 不合规写法:依赖C99标准头文件 */
#include <stdint.h>
uint32_t counter = 0;

1.2 规则:翻译单元必须独立可编译

这条规则说的是:每个.c文件必须能单独编译通过,不能依赖其他文件的定义。说白了,就是头文件要自己管好自己的依赖。

我曾经踩过一个坑:项目里有个config.h,里面用了#define定义了一些宏,但没包含任何头文件。结果有个.c文件先包含了config.h,再包含了uart.h,编译没问题。但另一个.c文件先包含了uart.h,再包含config.h,直接报错——因为uart.h依赖config.h里的宏定义。

避坑指南

我曾经因为头文件依赖顺序搞错,排查了整整两天。后来养成了一个习惯:每个头文件的第一行必须是#include它自己依赖的所有头文件。这样不管别人怎么包含,都不会出问题。

检查方法很简单:对每个.c文件单独执行编译命令,看能不能通过。如果依赖关系正确,每个文件都应该能独立编译。

1.3 规则:不得使用编译器扩展

这条规则是很多工程师容易忽略的。编译器扩展,说白了就是编译器厂商自己加的功能,比如GCC的__attribute__、Keil的__irq、IAR的__interrupt。这些特性虽然好用,但换一个编译器就废了。

编译器 常见扩展 MISRA合规替代
GCC __attribute__((packed)) 手动字节对齐 + 位域
Keil __irq 函数修饰 标准中断向量表 + 汇编封装
IAR __interrupt 关键字 同Keil方案
所有 内联汇编 尽量用C实现,必要时单独汇编文件

我个人建议:如果非要用编译器扩展,那就把它封装到一个独立的文件里,用#ifdef做条件编译。这样移植的时候只需要改这一个文件。

/* 不推荐:直接使用编译器扩展 */
__attribute__((interrupt)) void timer_isr(void) {
    /* ... */
}

/* 推荐:封装成宏,方便移植 */
#ifdef __GNUC__
  #define ISR_ATTR __attribute__((interrupt))
#elif __KEIL__
  #define ISR_ATTR __irq
#else
  #error "Unsupported compiler"
#endif

ISR_ATTR void timer_isr(void) {
    /* ... */
}

1.4 规则:不得依赖未定义或未指定的行为

这条规则范围很广,但核心就一点:别写那种“碰运气”的代码。比如函数参数的求值顺序、有符号整数的溢出、移位操作的位数超过类型宽度——这些都是未定义行为。

你想想看,同样的代码,今天在GCC上跑得好好的,明天换到IAR上就崩了。为什么?因为编译器对未定义行为的处理方式不同。

常见未定义行为清单

  • 有符号整数溢出(如 int a = INT_MAX + 1)
  • 移位位数超过类型宽度(如 uint8_t a = 1 << 8)
  • 函数参数求值顺序依赖(如 func(i++, i++))
  • 空指针解引用
  • 除零操作

我记得有一次,团队里有人写了这么一段代码:

int a = 1;
int b = a++ + ++a;  /* 未定义行为!a的求值顺序不确定 */

结果在调试版和发布版上跑出了不同的结果。排查了整整一个下午,最后发现是编译器优化导致的。从那以后,我定了个规矩:表达式里不允许出现两个以上的副作用操作。

知识结构总览

下面这张图,把规则1.1-1.4的核心逻辑串起来了。你可以把它当作一个检查清单:

规则1.1-1.4:编译环境与翻译单元知识结构 编译环境与翻译单元 规则1.1:C90标准 规则1.2:独立编译 规则1.3:禁用扩展 规则1.4:未定义行为 禁止C99/C11新特性 //注释、VLA、bool等 头文件自包含 无依赖顺序问题 __attribute__等 封装成宏隔离 求值顺序、溢出 移位、空指针等 核心原则:代码的可移植性 = 编译器的兼容性 每条规则都在问同一个问题:换一个编译器,你的代码还能编译通过吗?

实战检查清单

每次提交代码前,我建议你对照这个清单过一遍:

  1. 编译器版本确认:目标编译器是否支持C90完整标准?
  2. 头文件依赖检查:每个.c文件能否单独编译通过?
  3. 扩展特性扫描:代码里有没有__attribute__、__irq等非标准关键字?
  4. 未定义行为审计:有没有表达式副作用过多、移位越界、整数溢出等问题?
  5. 静态分析工具跑一遍:用PC-Lint、Coverity或MISRA Checker扫一遍,看看有没有违规项。

一句话总结

规则1.1-1.4的核心就四个字:别玩花活。老老实实写C90,头文件自包含,别依赖编译器扩展,别碰未定义行为。做到这四点,你的代码至少能在90%的嵌入式编译器上顺利通过。