C语言还在写操作系统,程序员早就不爱它了,可谁也绕不开它。
最近翻Linux内核源码,看到`mm/memory.c`里全是带`volatile`的指针,一行行读下来,没一个花里胡哨的语法,就是地址加偏移、强转、解引用——好像50年前写的。问了几个做嵌入式的学长,他们说智能手表固件、电机驱动、航天器飞控,只要芯片小、没虚拟内存、不能崩,写的全是C。不是不想换,是换了就跑不动。
很多人以为C能活这么久是因为“它快”。其实不对。FORTRAN也快,汇编更快。问题是FORTRAN写不了中断处理,汇编换颗芯片就得重写一半。C在1973年把Unix重写一遍,不是因为它多牛,而是它刚好卡在一个缝里:人能看懂,机器能直接执行,中间不加一层骗自己的东西。B语言试过,但没类型,编译器没法算内存布局,读个文件慢40%;Pascal有类型,可不让算指针,硬件寄存器就摸不到。
C的类型不是教你怎么编程,是告诉编译器“这块内存必须这么排”。比如`struct{int a; char b;}`,不是语法规定,是跟CPU签的合同:a占4字节,b紧挨着占1字节,对齐方式定了,缓存行怎么填也定了。Pascal不让你动地址,等于把合同撕了,再好的类型系统,也进不了内核。
指针也不是bug,是代理。你写`*p = 1`,不是在冒险,是在说“请把数字1,原封不动塞进p指向的那个确切地址”。ARM手册里写的寄存器地址0xFF00,C里直接强转成结构体指针去读,没有中间商,没有解释器,没有GC停顿。Python做不到,Java做不到,连Rust都得开`unsafe`块才能干这事——而`unsafe`块里面写的,还是C那一套。
函数调用看着简单,其实是个大事情。每次调函数,栈怎么压、参数怎么传、返回地址放哪,C规定死了。这个规定不是为了好看,是让操作系统能在毫秒级切任务。COBOL的`PERFORM`没栈帧,FORTRAN子程序不能嵌套,它们在单任务时代挺好,一到多进程就卡壳。C的函数调用就是人脑分治和CPU流水线之间定的握手协议。
有人说Rust能替代C,可翻Rust标准库源码,`std::sync::atomic`底层还是调GCC内置函数,`core::ptr`一堆`unsafe`块,里面解引用、取地址、强转类型,全是C式写法。它不是推翻C,是给C穿了件防弹衣,关键地方还是得裸奔。Linux内核不用C++,不是讨厌类,是虚函数表查表要跳转、异常要铺展开栈、new/delete可能卡在内存分配上——实时系统等不了那几十纳秒的不确定性。
C标准几十年没加新语法。C89、C99、C11、C17,改的全是边角漏洞:比如C11加了`_Atomic`,不是为了炫技,是告诉编译器“这个变量可能被多核同时改,请按CPU缓存一致性协议生成指令”。它不增加能力,只堵洞。所以1995年写的glibc,今天gcc13照样能链,ABI没变过。C++标准十年一变范式,C标准五十年没动接口底线。
嵌入式那边更直白。一个STM32F103,64KB Flash,20KB RAM,跑不了Python解释器,更别提JVM。你写一行`print("hello")`,背后要带几百KB运行时。C编译出来就是一条`mov r0, 1`,连库都不用,裸机上电就能跑。这不是选择,是物理定律:内存不够,就只能这么干。
学C的时候,老师总说“注意指针别越界”。后来才懂,越界不是怕程序崩,是怕你忘了自己正在跟真实硬件对话。地址不是数字,是插座;内存不是容器,是电路板上的铜线;函数不是模块,是CPU流水线里的一个确定位置。C没教会我怎么写漂亮代码,但它让我第一次知道,键盘敲下去,电流真正在哪条线上跑。
C语言不是活成了经典,是从来没死过。