从前端到后端:C++ 是如何做到“像素级”兼容 C 语言的?

发布时间:2026/7/13 21:23:52
从前端到后端:C++ 是如何做到“像素级”兼容 C 语言的? 写于2026.6.29日–与7.1再次修订补充细节让初学者清晰流程前言如果您是刚接触编程、对C和C关系感到好奇的小白请放心往下读这篇文章同样能让您看懂。毕竟对这些底层关系产生好奇的往往是初学者而老前辈们早就门儿清了。纵观网上的诸多文章大多只是简单罗列两者的表面区别很少有人能深入到语言本身的“第一性原理”去剖析。而今天我真正想搞清楚的是C到底是怎么做到如此完美地兼容C语言语法的【初学时的错误认知】我最初接触时一度以为C就是C语言的加强版它包含了C的一切直接无脑学C就行了。这么说是不准确的。初步概念当时因为好奇不断搜索我渐渐明白C和C实际上是两种不同的编程语言只是C兼容C的部分语法。以及一些C的发展历史。所以我当初只能这样回答自己C兼容C的语法是C之父故意这么设计的。C和C的关系手绘图C和C是两门不同的语言。它们的关系如图所示的确两者有重叠的部分这部分就是C兼容C的部分早期的C但同时两者也有自己独特的部分发展到后期的C和C。C“这么大”不难理解毕竟C包含“类”、“模板”等一堆概念。但是后期C语言也不断发展自己的特性。这部分是C所不包含的比如C99引入的伸缩数组、restrict 关键字、还有复合字面量等。因此说C是C的超集是不严谨且错误的说法C不是完全包含C的所有语法。结论C和C是两门不同的编程语言。所以您读到这里就搞清楚C和C的关系了。进一步思考C为什么能兼容C语言这么多核心特性上面我们将两者的关系搞清楚了。0、语言间独特的“亲密”C不仅兼容C的很多语法甚至还能直接#include C库引入使用它们。C“如此高的兼容性”是如何做到的编程语言大多只是基础语法类似但是少有能像C和C如此特别如此“亲密”。这给我带来“困惑与好奇”。显然Java和C同样也是两种不同的编程语言但是Java却和C有着非常大的差距。一个是JVM虚拟机运行一个是编译成机器码。而这两者语言特性差异极大不像C/C关系亲密。难道你们不觉得编程语言中就C和C很独特吗它们经常写成C/C。但是我很少看到其他语言这么罗列比如Java/Python或者Java/JavaScript或者C/C#?GNU启动1、gcc与g是驱动程序不是编译器那时我是初学者思考问题没有深度对问题的了解不够全面。所以很多东西概念上就是错误的。我在初学时很喜欢说“gcc编译器”“g编译器”但它们不是编译器而是编译器的驱动程序。对于小白来说你可以将它们俩理解为“包工头”每次调用gcc/g时它们都会按顺序”呼叫“四个工具来干活。分别是【牢大】预处理器展开 #include替换 #define删除注释。输出.i 文件纯净的高级语言源码【牢二】编译器接收.i文件。C语言cc1或者Ccc1plus。负责把高级语言翻译成汇编代码.s。它内部逻辑上分为前端解析语法和后端生成特定CPU的汇编。【牢三】汇编器输入.s 文件。将人类可读的汇编指令如 mov rax, 1一对一地翻译成 CPU 能看懂的二进制机器码010101…并记录好重定位信息。输出.o 文件目标文件里面是二进制的机器码【牢四】链接器将目标文件.o与系统库拼接成可执行文件输出.exe/ELF最终的可执行文件。C 和 C 虽然在语法上天差地别前端不同但它们共用编译器后端生成汇编的逻辑一样并且共用底层的二进制工具链也就是【牢三】汇编器和【牢四】链接器我们才知道原来编译器还分前端和后端。C和C如此紧密的原因之一编译器前端不同但后端、汇编器、链接器都是共用的。这样就能解释为什么链接时C能用C的库。但这只是条件之一随后还有两个条件“内存模型”“ABI应用二进制接口”也兼容2、内存模型是什么定义内存模型是程序编译后放入内存的存放方式。咱写完的代码运行后数据都是先加载到内存中的。举个抽象的例子你在C语言创建三个变量假设是ABC放入内存按ABC顺序放入。你在C里也做同样的事情创建三个变量ABC放入内存也是按ABC顺序放入这就是内存模型一致。而其他的语言可能放入顺序是ACB或者CBA总之内存模型不一致。也就是说不同编程语言它们把数据打包到内存的存放结构不同。3、ABI应用二进制接口是什么程序运行后数据会加载到内存中然后在从内存中取出放到寄存器里交给CPU执行。定义ABI 是编译后的机器码之间互相“交接工作”的交通规则。ABI是一个很大的概念为了简单我就介绍它涵盖的内容之一【调用约定】。ABI规定函数调用时参数是放在寄存器还是压入内存栈从哪个寄存器传返回值怎么拿谁来清理栈C 的 ABI 远比 C 复杂。相同点在基础的数据类型大小和底层调用约定上C 通常兼容所在平台的 C ABI。不同点C 引入了大量高级特性其 ABI 额外规定了 C 语言没有的东西**Qusetion内存模型相同但是ABI不一致是否兼容 答案不行举个简单例子就算是两个编程语言的内存模型一致都是ABC的顺序放入但是ABI不一致有的把ABC都放入寄存器123中有的可能放入寄存器456中或者栈中你说这能兼容吗彼此的底层规则都不一致。 技术细节名称修饰与extern “C”C支持函数重载因此编译器会对函数名进行“名称修饰”添加参数类型信息以区分不同函数。例如函数void func(int)可能被修饰为_Z4funci。而C语言没有函数重载函数名在编译后保持不变。这种差异是跨语言链接失败的核心原因。C提供了extern C声明它告诉编译器不要对指定函数进行名称修饰确保函数名与C语言一致。这样C代码就能直接调用C函数。// 在C中调用C函数的标准做法 extern C { #include c_header.h // 包含C语言头文件 } int main() { c_function(); // 能正确链接因为c_function的符号未被修饰 return 0; }4、C兼容C的大量语法这一点不是起决定性作用的这是C之父本贾尼·斯特劳斯特卢普让C程序员快速换到C上从而保持的设计。对于编程语言本身的设计来说语法不同只是编译器前端解析的规则不同。只要你共用编译器后端、ABI一致、内存模型一致。那就能高度兼容。很好的证明 Kotlin与Java你们或许听说过Kotlin和Java。搞安卓开发的都比较熟悉。Kotlin和Java的语法就大不相同但是两者都是编译成字节码然后交给JVM虚拟机来执行。从技术角度来看Java的功能都是JVM虚拟机给予的而Kotlin也一样。理论上你用Java能做的事情Kotlin同样也能做到。但是大家千万别说Kotlin可以取代Java这是不对的。有很多古老庞大项目都是用Java写的代码能跑就行比如银行系统那种谁敢动 拿Kotlin重构还得花钱请人干搞不好还容易出新Bug。所以说“取代”不现实。我是想说如果你开发一个新项目你用Java能实现的功能Kotlin100%也能做到因为两者底层都是JVM。懂了吧回到C/C中因此假设C和C的语法都不太一样但是只要前面说的那3点成立那么你仍然明白C能做的事情C大多也能做到。语法轻微不一致不起决定性作用C/C都有结构体和指针等是因为底层机制高度兼容。以前听别人提过一个简单C程序拿到cpp里稍微改改甚至不改也能照样跑起来。观点总结C之所以如此程度的兼容C语言。必须满足三点1.编译器后端、汇编器、链接器都共用。2.内存模型一致3. ABI兼容至少包含C那部分是相同的正是因为这样所以C和C才如此“亲密”因为底层很多东西C都是为了兼容C而保持和C是一样的结构。甚至共用编译器的后端。写在最后在AI大模型几秒钟就能生成一段代码的时代仍然有人愿意翻着老旧的博客死磕底层逻辑坚持这种“古法编程”。我觉得这是一件挺酷的事。因为无论上层应用如何狂飙突进计算机终究是人类用逻辑一砖一瓦搭建起来的造物。我曾在B站看过浙大凯翁老师的视频他说过一句话让我至今难忘学计算机一定要有一个非常强大的心理状态。计算机的所有东西都是人做出来的别人能想的出来我也一定能想的出来。在计算机里没有任何黑魔法所有的东西只不过是我现在不知道而已总有一天我会把所有的细节所有的内部的东西全都搞明白”这句话送给每一个正在底层原理前感到迷茫的初学者。无论是看似高深的ABI应用二进制接口还是编译器前端与后端的分离剥去那些唬人的专业术语外衣里面藏着的不过是前辈工程师们为了解决特定问题而留下的思维痕迹。没有什么是神迹一切皆是凡人智慧的沉淀。鄙人并非天赋之人只是单纯地热爱着计算机。计算机贯穿了我的整个青春也是它陪我走过了每一个低谷与高峰。何为热爱坚持就是我的答案。