作者最初系统语言是 Rust,他指出 C 在 C23 标准之前没有原生布尔类型;在此之前,程序员需要包含 <stdbool.h> 才能获得宏 true 和 false,它们只是展开为整数常量 1 和 0。这意味着,与 Rust 内置的 bool 不同,早期的 C 代码将布尔值当作普通整数处理,这对来自 true 和 false 为 distinct 字面量的语言的开发者来说可能会感到惊讶。文章还指出,即使 C23 之后将布尔设为真正的语言原始类型,旧版编译器仍需要该头文件以保证完全兼容。

C 字符串是以 null 结尾而非长度前缀存储。一个 C 字符串仅仅是指向其首字符的指针,其长度通过扫描标记结束的零字节(⟨0⟩)来确定。因此,每个字符串都必须预留一个额外字节用于存放终止符,忘记添加它或分配大小计算错误都可能导致在使用 strcpy 或 strlen 等函数时发生越界读取,因为这些函数会继续运行超过预期的缓冲区。文章通过一个手动实现的字符串反转例程来说明这一点:该例程显式分配 len+1 字节并在索引 len 处写入终止符,展示了 Rust 的 &str(它将长度与指针一起存储)所避免的记账开销。

显示漏洞类别的条形图,突出显示缓冲区溢出和越界写入
缓冲区溢出和越界写入是 C 语言中常见的漏洞类别bd103.dev

此图说明了缓冲区溢出和越界写入漏洞的普遍性,这些漏洞与文章中讨论的 C 语言手动内存管理和缺乏边界检查相关。

C 的整数类型不是固定宽度;其确切大小取决于目标平台。例如,long 在 Unix 上是 64 位,而在 Windows 上是 32 位;int 在两者上均为 32 位,但标准仅保证其至少为 16 位。这种可变性使得可移植代码变得脆弱,除否开发者使用 <stdint.h> 中的固定宽度别名(如 uint32_t、int64_t 等)。文章还强调了 C 的错误处理极为简约:许多函数通过返回哨兵值(如 -1 或空指针)来表示失败,而 errno 可能保存线程局部代码以提供更多细节,但语言本身没有机制强制调用者检查结果。例如,忘记检查 malloc 的返回值,在空指针被解引用时会导致段错误,这说明 C 将安全的责任放在了程序员身上。