4. 基本语法规则(二):必须使用大括号包裹所有控制语句体、switch语句必须包含default分支

好,咱们接着聊基本语法。上一节讲了命名和注释,这一节我要跟你聊两个看似简单、但坑特别多的规则。一个是「大括号」的使用,另一个是「switch 的 default 分支」。

说实话,这两个规则在教科书里可能就一句话带过。但在我十几年的航天软件生涯里,因为这两条栽跟头的案例,我至少见过两位数。你想想看,火箭飞在天上,一个控制语句的歧义,可能就导致姿态控制逻辑跑偏了。嗯,咱们得把话说清楚。

4.1 大括号:别偷懒,每一对都是安全锁

先问个问题:if (x > 0) y = 1; 这句代码有问题吗?

从语法上讲,没问题。但从安全编码的角度讲,这就是一颗定时炸弹。

规则很简单:所有控制语句体,包括 if、else、for、while、do-while,哪怕只有一行,也必须用大括号包裹起来。

为什么?我跟你讲个真实案例。有一次我在评审一个同事的代码,他写了一段类似这样的逻辑:

if (status == OK)
    do_something();
    do_another();  // 这行不在if里面!

你看出问题了吗?他本意是想两个函数都在 if 条件成立时执行。但因为没加大括号,do_another() 实际上永远都会执行。这种错误在代码审查时很难一眼看出来,尤其是代码量大的时候。

我个人习惯是:写 if 的同时,就把大括号打出来。先写 if () { },再往里面填内容。这样永远不会漏。

正确写法:

if (status == OK) {
    do_something();
    do_another();
}

for (int i = 0; i < MAX; i++) {
    process_data(i);
}

while (running) {
    check_sensors();
}

⚠️ 特别注意: 在 MISRA C 和 DO-178C 的编码标准中,不加大括号属于「违反基本编码规则」。在航天软件评审中,这会被直接打回。我曾经见过一个项目,因为一个没加大括号的 if 语句,导致代码评审返工了整整两天。

还有一种情况,就是嵌套的 if-else。如果不加大括号,else 的匹配关系会非常混乱。比如:

// 危险写法
if (a > 0)
    if (b > 0)
        x = 1;
else
    x = 2;  // 这个else到底匹配哪个if?

你可能会觉得 else 匹配第一个 if,但 C 语言的规则是:else 匹配最近的未匹配的 if。所以这个 else 实际上匹配的是 if (b > 0)。这种歧义在代码审查时特别容易引发争论。

用大括号就清晰了:

if (a > 0) {
    if (b > 0) {
        x = 1;
    }
} else {
    x = 2;
}

你看,一目了然。大括号在这里不仅仅是语法要求,更是代码的「语义标点符号」。

4.2 switch 语句:default 分支不是摆设

接下来聊 switch。很多程序员写 switch 时,觉得所有 case 都覆盖了,就不写 default 了。比如:

// 不推荐的写法
switch (cmd) {
    case CMD_START:
        start_engine();
        break;
    case CMD_STOP:
        stop_engine();
        break;
    case CMD_ABORT:
        abort_mission();
        break;
}

看起来好像没问题?cmd 只有三种取值嘛。但我要告诉你:在航天软件中,你必须假设所有「不可能」的情况都可能发生

为什么?因为内存翻转、电磁干扰、硬件故障,都可能导致变量出现你意想不到的值。我曾经在测试中遇到过,一个枚举类型的变量,因为内存位翻转,变成了枚举定义之外的值。如果没有 default 分支,程序就会直接跳过整个 switch,导致没有任何处理动作。

这在航天任务中意味着什么?意味着某个关键指令被静默忽略了。嗯,后果不堪设想。

正确写法:

switch (cmd) {
    case CMD_START:
        start_engine();
        break;
    case CMD_STOP:
        stop_engine();
        break;
    case CMD_ABORT:
        abort_mission();
        break;
    default:
        // 记录错误,或者执行安全默认行为
        log_error("Unknown command: %d", cmd);
        execute_safe_default();
        break;
}

💡 我的建议: default 分支里至少要做两件事:一是记录错误日志(如果有日志系统),二是执行一个安全默认行为。比如复位到安全状态,或者忽略并继续执行。千万不要让 default 分支空着,哪怕你写个注释说明「理论上不会执行到这里」。

还有一种情况,就是有些程序员会在 default 里写 break; 就完事了。这也不行。你想想看,如果 default 分支什么都不做,那跟没写有什么区别?至少应该有一个断言或者错误处理。

我个人在项目中常用的模式是:

switch (mode) {
    case MODE_IDLE:
        idle_handler();
        break;
    case MODE_ACTIVE:
        active_handler();
        break;
    case MODE_ERROR:
        error_handler();
        break;
    default:
        // 不应该到达这里,但以防万一
        reset_to_safe_mode();
        break;
}

你看,default 分支里调用了 reset_to_safe_mode(),这是一个安全兜底函数。即使出现了意料之外的 mode 值,系统也能进入一个已知的安全状态。

4.3 避坑指南

我曾经在项目中遇到过这样一个问题:一个同事在 switch 的 case 里声明了局部变量,但没有加大括号,导致编译报错。比如:

switch (val) {
    case 1:
        int temp = 10;  // 编译错误!
        break;
    default:
        break;
}

在 C99 标准中,case 标签后面不能直接声明变量。正确的做法是加一对大括号:

switch (val) {
    case 1: {
        int temp = 10;
        process(temp);
        break;
    }
    default:
        break;
}

这个细节很多人不知道。如果你在 case 里需要局部变量,记得加上大括号。这其实也呼应了我们第一条规则——大括号是安全的保证。

4.4 总结一下

这两条规则,说白了就是一句话:别偷懒,别假设

  • 大括号:不管几行代码,一律加上。这是防止语义歧义的最简单方法。
  • default 分支:不管你觉得多「不可能」,都要写。这是应对异常情况的最后防线。

在航天软件中,我们常说「防御性编程」。这两条规则就是防御性编程最基础、最有效的实践。你想想看,一个火箭上有几百万行代码,如果每个 if 都不加大括号,每个 switch 都不写 default,那得埋下多少隐患?

好了,这一节就到这里。下一节我们聊「指针使用规范」,那又是一个容易出大坑的地方。咱们到时候细说。