了解丹尼斯·里奇如何通过 C 塑造 Unix,并让内核、嵌入式设备和高性能软件至今受益——以及关于可移植性、性能与安全性你需要知道的要点。

C 是那种大多数人不会直接接触但几乎人人依赖的技术。如果你使用手机、笔记本、路由器、汽车、智能手表,甚至带显示屏的咖啡机,栈中的某处很可能有 C——负责启动设备、与硬件通信或保证运行足够快以呈现“即时”的体验。
对于构建者来说,C 仍然是实用的工具,因为它罕见地将控制力与可移植性结合起来。它可以非常接近机器运行(因此你可以直接管理内存和硬件),但也可以在不同的 CPU 和操作系统之间相对少量地重写就能迁移。这样的组合很难被取代。
C 的最大足迹体现在三个方面:
即便应用程序大多用更高级的语言编写,它的基础部分(或性能敏感模块)往往也能追溯到 C。
本文将把 丹尼斯·里奇、C 的初衷,以及它为何至今出现在现代产品中的原因串联起来。我们将覆盖:
本文专注于 C 本身,不是“所有低级语言”。C++ 和 Rust 可能会被拿来比较,但焦点在于 C 是什么、为什么这样设计,以及团队为何继续在真实系统中选择它。
丹尼斯·里奇(1941–2011)是美国计算机科学家,因在 AT&T 的贝尔实验室的工作而著名。贝尔实验室在早期计算和电信领域扮演了核心角色。
在 1960s 后期到 1970s,里奇与肯·汤普森等人在贝尔实验室从事操作系统研究,促成了 Unix 的诞生。汤普森创建了 Unix 的早期版本;随着系统演进为可以维护、改进并在学界与业界广泛共享的产物,里奇成为了关键的共同创造者。
里奇还创造了 C 语言,基于贝尔实验室使用的早期语言思想。C 的设计目标是为编写系统软件提供实用的工具:它赋予程序员对内存和数据表示的直接控制,同时比把所有东西都写成汇编更具可读性和可移植性。
这一组合之所以重要,是因为 Unix 最终被用 C 重写。这并非出于风格上的重写——它使 Unix 更容易迁移到新硬件并随着时间扩展。结果形成了一个强有力的正反馈回路:Unix 为 C 提供了严苛的应用场景,而 C 则让 Unix 更容易被推广到单台机器之外。
Unix 和 C 一起定义了我们所知的“系统编程”:在一门贴近机器但不绑定单一处理器的语言中构建操作系统、核心库和工具。这种影响体现在后续的操作系统、开发工具以及许多工程师今天仍在学习的约定上——并非因为神话,而是因为这种方法在大规模下行之有效。
早期操作系统大多用汇编语言编写。那能让工程师对硬件拥有完全控制,但也意味着每次改动都慢、易出错,并紧密依赖于特定处理器。即使是小功能也可能需要大量低级代码,且把系统移植到不同机器通常意味着要重写大量内容。
丹尼斯·里奇并非在真空中发明 C。它源自贝尔实验室使用的更早、更简单的系统语言:
C 的构建目标是与计算机实际运作清晰映射:内存中的字节、寄存器上的算术、代码中的跳转。因此,简单的数据类型、显式的内存访问以及与 CPU 指令相匹配的运算符是语言的核心。你可以写出足够高层以管理大型代码库的代码,同时又足够直接以控制内存布局和性能。
“可移植”意味着可以将相同的 C 源代码移到不同的计算机,并在尽量少的改动下在那儿编译并得到相同的行为。团队可以保留大部分共享代码,只替换少量与机器相关的部分。正是这种“大部分共享、少量机器相关”的混合,促成了 Unix 的传播。
C 的速度并非魔法——很大程度上来自它与计算机实际运作的直接映射,以及在代码与 CPU 之间插入的“额外工作”很少。
C 通常是编译型的。也就是说你写人类可读的源代码,编译器会把它翻译成机器码:处理器执行的原始指令。
在实践中,编译器生成可执行文件(或随后被链接成可执行文件的目标文件)。关键点是最终结果不是在运行时逐行解释的——它已经是 CPU 能理解的形式,从而减少了开销。
C 提供了简单的构建块:函数、循环、整数、数组和指针。因为语言小且显式,编译器通常能生成直接的机器码。
通常没有强制性的运行时在后台工作来跟踪每个对象、插入隐藏检查或管理复杂元数据。当你写一个循环时,通常就会得到一个循环;当你访问数组元素时,通常就是一次直接的内存访问。这种可预测性是 C 在紧密、对性能敏感的软件部分表现良好的重要原因。
C 使用手动内存管理,意味着程序显式地请求内存(例如 malloc),并显式地释放它(例如 free)。这是因为系统级软件常常需要对何时分配内存、分配多少以及持续多长时间进行精细控制——而这些都要求最小化隐藏开销。
权衡很直接:更多的控制往往带来更高的速度和效率,但也意味着更多责任。如果忘记释放内存、重复释放或在释放后继续使用内存,错误可能很严重,有时还会带来安全风险。
操作系统处在软件与硬件的边界。内核必须管理内存、调度 CPU、处理中断、与设备通信,并提供所有其他软件依赖的系统调用。这些任务不是抽象的——它们涉及读取/写入特定内存位置、使用 CPU 寄存器以及对不合时宜到来的事件做出反应。
设备驱动和内核需要一种能表达“精确执行此操作”的语言。实际上这意味着:
C 很适合这些场景,因为它的核心模型贴近机器:字节、地址和简单的控制流。内核不需要宿主强制性的运行时、垃圾回收或对象系统才能启动。
Unix 与早期系统工作的流行做法推广了里奇帮助塑造的方法:在可移植语言中实现大部分操作系统,同时把“硬件边缘”保持得尽可能薄。许多现代内核仍遵循这一模式。即便需要汇编(引导代码、上下文切换),大部分实现也通常由 C 承担。
C 也主导了核心系统库——例如标准 C 库、基础网络代码和其他高级语言经常依赖的低级运行时组件。如果你使用过 Linux、BSD、macOS、Windows 或 RTOS,无论你是否意识到,都几乎肯定用过 C 代码。
C 在操作系统工作的吸引力并非怀旧,而是工程经济学的体现:
Rust、C++ 和其他语言在操作系统的部分领域已有应用,并能带来实际好处。但 C 仍是一个共同的分母:许多内核使用的语言、绝大多数低级接口假定的语言,以及其它系统语言必须互操作的基线。
“嵌入式”通常指那些你不会把它们当作计算机的设备:温控器里的微控制器、智能音箱、路由器、汽车、医疗设备、工厂传感器以及无数家电。这些系统往往为单一目的运行多年,静默工作,且对成本、功耗和内存有严格限制。
许多嵌入式目标只有几千字节(而非几 GB)的 RAM,以及受限的闪存来存放代码。有些设备靠电池供电并必须大部分时间休眠;有些有实时性截止时间——如果电机控制回路晚了几毫秒,硬件可能会出现异常。
这些约束影响每一个决策:程序有多大、多久唤醒一次以及时间行为是否可预测。
C 往往能生成体积小的二进制且运行时开销极小。无需虚拟机,并且通常可以完全避免动态分配。这在你需要把固件塞进固定闪存或保证设备不会“意外暂停”时至关重要。
同样重要的是,C 能直观地与硬件交互。嵌入式芯片通过内存映射寄存器暴露外设——GPIO 引脚、定时器、UART/SPI/I2C 总线。C 的模型与此天然契合:你可以读取和写入特定地址、控制单个位,并且很少有抽象阻碍你操作。
许多嵌入式 C 代码通常是:
无论哪种方式,你都会看到围绕硬件寄存器(通常标记为 volatile)、定长缓冲区和精确时序构建的代码。正是这种“贴近机器”的风格,使得 C 成为需要小体积、节能并在截止时间内可靠运行的固件的默认选择。
“性能关键”指的是时间和资源成为产品一部分的任何场景:毫秒影响用户体验,CPU 周期影响服务器成本,内存使用决定程序是否能运行。在这些地方,C 仍然是默认选项,因为它让团队控制数据在内存中的布局、工作如何调度以及编译器允许哪些优化。
你常常会在这些领域的核心看到 C 的身影:
这些领域并非到处都要“快”。通常只有特定的内循环占据大部分运行时间。
团队很少会为追求速度而把整个产品都用 C 重写。相反他们会做性能分析,找到热路径(占用大部分时间的小部分代码),并对其进行优化。
C 有帮助,因为热路径往往受低级细节限制:内存访问模式、缓存行为、分支预测和分配开销。通过调整数据结构、避免不必要的拷贝并控制分配,可以在不改动其余应用的情况下实现显著加速。
现代产品常是“混合语言”的:大部分代码用 Python、Java、JavaScript 或 Rust 写,而性能关键核心用 C 实现。
常见的集成方式包括:
这种模式让开发保持现实:在高级语言中快速迭代,在关键处获得可预测的性能。代价是在边界处需要小心——数据转换、所有权规则和错误处理,因为跨越 FFI 应该是高效且尽量安全的。
C 之所以迅速传播,是因为它“能走”:同一核心语言可以在从微控制器到超级计算机的不同机器上实现。可移植性并非魔力——它来自共享标准和面向这些标准的编码文化。
早期的 C 实现因厂商而异,导致代码难以共享。重大转折来自 ANSI C(常称 C89/C90)以及之后的 ISO C(后续版本如 C99、C11、C17 和 C23)。你无需记住编号;重要的是:标准是对语言和标准库行为的公开约定。
标准提供了:
因此,以标准为导向编写的代码通常能在不同编译器和平台间以令人惊讶的少量改动移动。
可移植性问题通常来自依赖标准未保证的事物,包括:
int 并不保证是 32 位,指针大小也各不相同。如果程序对具体大小有隐含假设,换平台时可能失败。一个好的默认做法是优先使用标准库,并把不可移植的代码放在小而明确命名的封装后面。
同时,用能促使你写出可移植、定义良好的 C 的编译选项来编译代码。常见选择包括:
-std=c11)-Wall -Wextra)并严肃对待它们这种组合——以标准为先的代码加上严格的构建——比任何“巧妙”的技巧都更能提升可移植性。
C 的能力也是它的锋利一面:它让你贴近内存。这正是 C 快速且灵活的原因,同时也容易让初学者(或疲惫的专家)犯下其他语言会阻止的错误。
把程序的内存想象成一条长街的编号信箱。变量是装东西的信箱(比如一个整数)。指针不是信箱里的东西——它是写在纸条上的地址,告诉你要打开哪个信箱。
这很有用:你可以传递地址而不是拷贝内容,也可以指向数组、缓冲区、结构体甚至函数。但如果地址错误,你就打开了错的信箱。
这些问题表现为崩溃、无声的数据损坏和安全漏洞。在系统级代码中——C 常被使用的地方——这些失败会影响其上层的一切。
C 不是“默认不安全”的语言。它是宽容的:编译器假定你写的就是你的意思。这对性能和低级控制很有利,但也意味着如果不配合谨慎的习惯、复审和良好工具,C 很容易被误用。
C 给你直接控制,但很少宽恕错误。好消息是“安全的 C”并非靠魔法,而是靠有纪律的习惯、清晰的接口和让工具来做枯燥的检查。
从设计 API 开始,让错误用法变得困难。偏好接收缓冲区大小的函数,而不是只接指针;返回明确的状态码,并记录谁拥有已分配的内存。
边界检查应成为常规而非例外。如果函数要写入缓冲区,就应事先验证长度并快速失败。关于内存所有权,保持简单:一个分配器对应一条释放路径,并对调用方或被调用方释放资源做出明确规则。
现代编译器可以对高风险模式发出警告——在 CI 中把警告当作错误。开发时使用运行时检查(sanitizers,例如 AddressSanitizer、UndefinedBehaviorSanitizer、LeakSanitizer)以发现越界写、释放后使用、整数溢出等问题。
静态分析器和 linter 有助于发现测试中可能暴露不出的错误。模糊测试对解析器和协议处理尤为有效:它会生成意外输入,经常能揭示缓冲区和状态机缺陷。
代码审查应明确查找常见 C 失败模式:越界一位错误、缺失的 NUL 终止符、有符号/无符号混用、未检查的返回值以及可能在错误路径泄露内存的情况。
当语言不会保护你时,测试更重要。单元测试是基础;集成测试更好;对已发现缺陷的回归测试是最佳实践。
如果项目有严格的可靠性或安全需求,考虑采用受限的“C 子集”和成文规则(例如限制指针算术、禁止某些库调用或要求使用封装函数)。关键在于一致性:选择团队能用工具与复审强制执行的规则,而不是只停留在幻灯片上的理想。
C 处在一个不寻常的交叉点:它足够小以便端到端理解,同时又足够接近硬件和操作系统边界,成为其他一切依赖的“胶水”。这种组合使得团队即便在新语言看起来更好时仍会选择它。
C++ 的目标是加入更强的抽象机制(类、模板、RAII),同时在很大程度上与 C 保持源代码兼容。但“兼容”并不等于“相同”。C++ 在隐式转换、重载解析以及某些边缘声明是否合法等方面有不同规则。
在实际产品中经常混用:
桥接通常通过 C API 边界完成。C++ 代码用 extern "C" 导出函数以避免名称改编,双方就 Plain Old Data 结构达成一致。这让团队能渐进式现代化,而无需大规模重写。
Rust 的重要承诺是在不使用垃圾回收的情况下实现内存安全,并配合强大的工具链和生态。在很多新建系统项目中,它能减少整类错误(释放后使用、数据竞争)。
但采用并非没有成本。团队可能受限于:
Rust 可以与 C 互操作,但边界会增加复杂性,并非每个嵌入式目标或构建环境都有同等的支持。
世界上大量基础代码是用 C 写的,重写代价高且有风险。C 也适合那些需要可预测二进制、最小运行时假设和广泛编译器可用性的环境——从微控制器到主流 CPU。
如果你需要最大覆盖范围、稳定接口和经验证的工具链,C 仍然是理性的选择。如果你的约束允许且安全是首要目标,新语言可能值得考虑。最佳决策通常从目标硬件、工具链和长期维护计划开始,而不是今年流行什么。
C 并不会“消失”,但其重心正变得更清晰。它将在需要对内存、时序和二进制进行直接控制的领域持续繁荣——同时在那些安全性与迭代速度比抢最后一微秒性能更重要的地方逐步收缩。
C 可能继续作为默认选择出现在:
这些领域演进缓慢、遗留代码基庞大,并奖励那些能以字节、调用约定和失败模式思考的工程师。
对于新应用开发,许多团队更偏好具有更强安全保证且生态更丰富的语言。内存安全错误(释放后使用、缓冲区溢出)代价高昂,而现代产品往往优先快速交付、并发能力和安全默认。在系统编程中,一些新组件正迁移到更安全的语言——同时它们仍与 C(作为基石)接口。
即便低级核心是 C,团队通常仍需要周边软件:Web 仪表盘、API 服务、设备管理门户、内部工具或用于诊断的小型移动应用。那一层通常是迭代速度最重要的地方。
如果你想在这些层面快速推进而不重建整条流水线,Koder.ai 可以帮忙:它是一个 vibe-coding 平台,能通过对话创建 Web 应用(React)、后端(Go + PostgreSQL)和移动应用(Flutter)——适合快速搭建管理界面、日志查看器或与基于 C 的系统集成的车队管理服务。它的规划模式与源码导出让你可以方便地做原型,然后把代码库迁移到所需的方向。
从基础开始,但以专业使用 C 的方式去学:
如果你想要更多面向系统的文章和学习路径,请浏览 /blog。
C 仍然重要,因为它将底层控制(内存、数据布局、硬件访问)与广泛的可移植性结合在一起。这种组合使其成为必须引导机器、在受限条件下运行或提供可预测性能的代码的实用选择。
C 仍然主导以下领域:
即便大部分应用用高级语言实现,关键的基础设施也常常依赖于 C。
丹尼斯·里奇在贝尔实验室设计 C,目标是让编写系统软件变得可行:既贴近机器,又比汇编更可移植和可维护。一个主要的证明是 用 C 重写 Unix,这让 Unix 更容易移植到新硬件并随着时间扩展。
通俗地说,可移植性意味着可以在不同 CPU/操作系统上编译同一份 C 源代码,并通过最少的修改获得一致的行为。通常的做法是保持大部分代码可共享,把与硬件/操作系统相关的部分封装在小的模块或包装里。
C 往往更快,因为它与机器指令的映射很接近,通常没有强制性的运行时开销。编译器常能为循环、算术和内存访问生成直接的机器码,这在关键内循环中尤其重要。
许多 C 程序使用手动内存管理:
malloc)free)这允许精确控制内存何时被使用和占用多少,在内核、嵌入式系统和性能关键路径中很有价值。代价是错误可能导致崩溃或安全问题。
内核和驱动需要:
C 很适合这些需求,因为它提供低级访问、稳定的工具链和可预测的二进制结果。
嵌入式目标通常内存/闪存极小、功耗受限并且可能有实时要求。C 能生成小体积的二进制、避免沉重的运行时开销,并且能通过内存映射寄存器和中断直接与外设交互,这些特性非常契合嵌入式需求。
常见做法是将大部分产品保留在高级语言中,只把真正的 热路径 用 C 实现。典型的集成方式包括:
关键是保持边界高效,并明确定义数据所有权和错误处理规则。
把 C 写得更安全通常依赖于纪律和工具:
-Wall -Wextra)并修复它们这些做法不会消除所有风险,但能显著减少常见错误类型。