第十五章:程序失败
级别 2 — 认知
未定义行为、错误码、信号——C 中如何出错以及 Auto 如何防止它们。
程序会失败。问题不在于失败是否会发生,而在于语言如何帮助你预防、检测和恢复。C 的失败处理方法是最小化的:语言假设程序员总是正确的,并且很少提供安全网。Auto 采取相反的方法——语言通过编译时检查和运行时安全帮助程序员避免失败。
15.1 不当行为
C 将程序错误行为分为几个类别。最危险的是未定义行为(UB):
未定义行为:标准不对其施加任何要求的行为。
UB 意味着编译器被允许做任何事情。常见示例:
c
// C 深入:未定义行为
int arr[5];
arr[10] = 42; // UB:越界访问
int *p = NULL;
*p = 42; // UB:空指针解引用
int x;
printf("%d\n", x); // UB:读取未初始化变量
int a = INT_MAX;
int b = a + 1; // UB:有符号整数溢出
free(p);
*p = 42; // UB:释放后使用UB 在实践中实际发生的情况:
| 场景 | 可能的后果 |
|---|---|
| 缓冲区溢出 | 静默数据损坏 |
| 空指针解引用 | 段错误(崩溃) |
| 释放后使用 | 任意代码执行 |
| 整数溢出 | 不正确的结果 |
| 类型混淆 | 数据损坏,安全漏洞 |
编译器利用 UB 进行优化。如果标准说行为未定义,编译器可以假设它永远不会发生:
c
// C 深入:编译器利用 UB
int *check(int *p) {
int x = *p; // 解引用 p
if (p == NULL) { // 编译器移除此检查:如果 p 是 NULL,
return NULL; // 上面的解引用就是 UB,
} // 而 UB "不会发生"
return p;
}Auto 如何防止 UB。 Auto 通过以下方式消除整类 UB:
- 边界检查:数组访问在可能时在编译时验证,在必要时在运行时验证。
- 空值安全:Auto 中没有空指针。可选值使用显式
Option类型。 - 初始化强制:每个变量必须初始化。
- 无指针算术:数组索引替代所有指针算术。
- 溢出检测:整数溢出在运行时被捕获。
auto
// Auto:UB 预防
fn safe_divide(a int, b int) int {
if b == 0 {
print("Error: division by zero")
return 0
}
a / b
}
fn safe_access(arr [5]int, index int) int {
if index < 0 || index >= 5 {
print("Error: index out of bounds")
return 0
}
arr[index]
}