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,那得埋下多少隐患?
好了,这一节就到这里。下一节我们聊「指针使用规范」,那又是一个容易出大坑的地方。咱们到时候细说。