我最早学的系统编程语言是Rust。和很多程序员相比,这不太常见——更常见的情况是先学C或C++,再来学Rust。因此网上有大量"给C程序员的Rust指南",反过来讲C的文章却少得可怜。

我最近在自学C和C++,被这门语言的古怪之处震到了。这篇不是C语言教程,而是一份清单——记录我在学C过程中遇到的、最让人意外的几个细节。(今天不涉及C++,我还没准备好碰那堆麻烦。)

打开网易新闻 查看精彩图片

布尔类型不是原生就有的

最早的C根本没有布尔类型,程序里用整数0和1代替。直到C99,才通过可选的头文件引入布尔。但即便如此,true和false也不是字面量或关键字,它们只是展开成1和0的宏定义:

  • #define true 1
  • #define false 0

到了C23,布尔才真正成为语言原语。但如果你编译的是更早的版本,还是得手动引入。

字符串靠空字节收尾,不是靠长度

Rust的&str占16字节:8字节存内存地址,8字节存字符串长度。因为str是动态大小类型,靠指针元数据来追踪长度。这样取字符串长度极快,但每个&str引用要多占内存。

C走的是另一条路:它不单独存字符串长度,而是用空字节\0给每个字符串收尾。这是一个有意的取舍,带来几个后果:

  1. C程序不需要在字符串旁边额外维护一个size变量
  2. 每个字符串末尾都要留出空字节的位置
  3. 空字符串""也要占一个字节的内存
  4. 空字节很难直接用在字符串中间,否则会搞乱里的函数
  5. 忘记加空终止符,就可能导致越界读取

实际写代码时,你得记得为空终止符多分配空间,并在字符串末尾补上它。比如一个反转字符串的C函数,要专门处理两处:分配时用malloc(len + 1),循环结束后写reversed[len] = '\0'。对应的Rust版本则完全不用操这份心。

整数宽度取决于目标平台

C的整数类型不保证用固定的位数,宽度随目标平台变化。long在Unix上是64位,在Windows上却是32位,这一点尤其让人别扭。我一位朋友几年前给过建议:如果你在意跨平台兼容性,就只用提供的固定宽度整数。

错误处理是真的糟

我很喜欢Rust的错误处理。Result逼着你面对错误,和类型(enum)加match语句让处理变得很轻松。相比之下,C的错误处理堪称悲剧。它基本靠函数返回一个"魔法整数"(比如-1)或空指针来暗示出错了。你可以读errno——一个线程局部的整数,用来检查具体错误类型——但想拿到真正的错误信息和调用栈,就难得多。

写C时最明显的一点是:语言永远不会强迫你处理错误,全靠你自己记得函数可能失败。比如malloc()可能失败并返回空指针,新手未必立刻意识到。如果机器内存耗尽,访问reversed[i]就会触发段错误。要避免这种没用的崩溃,程序应该检查空指针,发现后优雅退出:

  • char* reversed = malloc(len + 1);
  • if (reversed == NULL) {
  • perror("Error");
  • exit(1);

这样比直接段错误体验好得多。当然,记得检查每一个分配出来的指针,并不是什么愉快的开发体验。我还试过用带标签的联合体在C里复刻Rust的Result,但用起来一团糟,而且仍然拦不住你不处理错误就直接访问里面的指针。