尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

C++之父Bjarne Stroustrup:语言设计是被工程逼出来的

C++之父Bjarne Stroustrup:语言设计是被工程逼出来的 你大概在不少技术社区都刷到过这类标题“C之父谈现代C”“Bjarne在CppCon上又说了什么”。今天这篇“C 之父交谈录”不追热点而是把 Bjarne Stroustrup 散落在《The C Programming Language》序言、历届 CppCon 主题演讲、各种技术访谈里的真实观点按主题重新拉通再附上我作为一个写了十多年 C 的从业者的批注与体会。适合三类人看刚准备入门却被语法淹没的新手、正在背“C八股”准备面试的求职者、以及已经在工程里用 C 写业务却始终觉得差点意思的老开发。语言设计从来不是因为“这样更酷”而是因为“项目真的卡住了”。这一条是贯穿整个 C 演变史的主线。理解了它你再看那些五花八门的语法、特性、争议都会清晰很多。1. C从哪里来语言不是画图纸而是被工程逼出来的1.1 1979年的仿真程序才是C真正的摇篮Stroustrup 1979 年在贝尔实验室做博士论文相关的分布式系统仿真研究当时他主要用 C 语言写代码。写的是某种模拟系统要处理大量并行的、离散的事件。C 语言能做这件事但做得非常痛苦。他需要模块化、需要类型检查、需要一种比宏拼接更体面的方式来管理复杂度。于是他自己在 C 的基础上做了一组工具最初叫“C with Classes”——带类的 C。他先用预处理器做过一版把类模拟出来写出来的代码能跑但丑陋、难维护、编译报错还不像人话。后来忍无可忍决定直接改编译器。这才有了 1983 年正式改名的 C。这里有一个很多新手完全意识不到的关键点C 不是大学教授在书房里画图纸画出来的语言它最核心的语法设计背后坐着一整排真实的工程问题。类是为了组织复杂度模板是为了摆脱复制粘贴异常是为了应对分布式系统里的错误传播——每一个特性最初都是因为某个项目的代码真的写不下去了。1.2 为什么要兼容C这笔账算得很清楚C 从诞生起就把“兼容 C”定为铁律很多人把这解释为情怀、技术洁癖但 Stroustrup 自己的说法很务实兼容 C 意味着当时现存的几十万行 C 代码、整个 UNIX 生态的基础设施可以零成本地渐进迁移到 C用户不需要推倒重来。代价同样摆在台面上。数组退化、头文件宏、隐式类型转换——这些被后来者反复吐槽的设计几乎都是 C 遗产的一部分。C 选择把 C 的“可见伤疤”全部继承下来换取了它今天在全球底层软件生态里不可替代的位置。你如果想把 C 骂得一文不值先想想它身上压着的是半个世纪的工程遗产如果想夸它强也正是这些遗产给了它触碰硬件底层的直接能力。1.3 “”这个名字本身就是个程序员的命名品味“带类的 C”后来改叫 C用的是自增运算符。这在当年计算机圈里还引发过小争议有人觉得不是所有提升都该用来表示。但一个语言的名字能是一个运算符梗本身就说明它的基因里写着“程序员造给自己用”这件事。后来的故事大家都知道了这个看起来像玩笑的名字成了几十年来系统软件领域最绕不开的存在。2. 零开销原则C最底层的设计天平2.1 什么是真正的“你不付钱”Stroustrup 反复强调的设计核心是“零开销原则”。这句话经常被误读成“C 一定比其他语言快”其实是另一回事。它的意思是你不使用的语言特性不给你带运行时成本你使用的抽象机制应该做到和手写等价代码一样的效率。举几个最具体的例子。C 的虚函数调用只有当你确实用到了运行时多态才产生间接调用开销普通成员函数默认就是静态解析不输 C 函数调用。模板在编译期展开绝不产生额外运行时抽象层。STL 容器当然有封装但你算子面就知道它不会比手写数组循环慢多少。我的体会是C 的世界里没有“免费的午餐”但也不存在“强行让你付钱的晚餐”。你的代码付不付代价基本取决于你选择用哪个层面的抽象。这也是为什么 C 至今还是游戏引擎、高频交易、数据库内核、嵌入式系统这些性能敏感领域的首选——不是因为它绝对快而是因为它的快是可以预测、可以计算、可以被你完全掌控的。2.2 零开销的代价那些折磨你的模板报错但“零开销”并不是免费的它只是把账记到了另一个地方。模板元编程和泛型代码越是强大编译期的工作量就越大编译器吐出来的错误信息就越是一篇阅读理解。几百行模板报错对 C 开发者来说早就习以为常这也是 C 学习曲线陡峭的真实原因之一。头文件机制带来的编译速度问题、多个版本二进制兼容问题、模板实例化带来的代码膨胀问题——这些成本都是“零开销原则”背后的影子价格。你用 C 省下的运行时开销有很大一部分要在编译期和开发期还回去。理解了这笔账你就不会天真地问“为什么 C 编译这么慢”也不会在一个模板报错面前直接崩溃。2.3 为什么底层世界的核心代码永远是C很多人在问Rust 都出来这么多年了是不是该取代 C 了。这个问题在 Stroustrup 的访谈里也被问过很多次。他的态度始终是欢迎竞争、也承认 Rust 在安全子集上有自己的优势但同时强调 C 的存量生态、人才储备、工具链成熟度是任何后来者短期内无法完整替代的。更关键的是C 最核心的竞争力不是“快”而是“可预测的快”。金融高频交易系统里一毫秒的垃圾回收停顿都可能造成不可接受的延迟游戏引擎渲染主循环里任何隐形的 GC 毛刺都会导致掉帧嵌入式控制器里你需要精确知道每一步消耗多少时钟周期。C 没有隐形的运行时杂音这一点不是劣势而是它统治这些领域近四十年不能动摇的根基。3. 内存、RAII与安全那位老父亲怎么看“C不安全”3.1 “C 内存不安全”这句指控到底冤枉不冤枉最近几年内存安全这个词特别热C 经常被点名。Stroustrup 在多次采访里给出的回应大意始终是现代 C 完全具备写出与托管语言同等安全级别的代码能力问题在于大量存量代码停留在“C98 时代”的裸指针驾驶风格。他反复推荐的三件套是RAII、智能指针unique_ptr 和 shared_ptr、标准容器。如果你老老实实用这套组合绝大多数内存泄漏、悬垂指针、重复释放问题在设计阶段就能被消灭。我们一位老工程师说过一句话我记到现在“C 不是不给你安全带是你自己非要把安全带卸了然后怪方向盘不安全。”3.2 RAII不是小技巧是整个资源的官方哲学RAII 全称是“Resource Acquisition Is Initialization“构造函数负责获取资源析构函数负责释放资源资源生命周期与变量的作用域牢牢绑定。这个名字起得有点绕但它解决的问题极其朴素你不需要记得在每条 return 路径上写 delete。一个 std::ofstream 打开文件之后函数无论怎么返回析构函数都会把文件关掉。一个 std::lock_guard 加锁之后出了作用域自动解锁。一个 unique_ptr 管理堆内存所有权转移明确之后你几乎不可能写出内存泄漏。RAII 真正厉害的地方在于它把资源管理从“人的纪律”变成了“语言的自动过程”。3.3 C为什么不内置垃圾回收一个不算纠结的决定Stroustrup 对垃圾回收的态度历史上其实有过调整但核心观点一直很清晰C 不把 GC 作为默认选项因为默认 GC 意味着所有人都要为某些场景的便利买单并且付出可预测性被损害的代价。游戏引擎、嵌入式实时系统、高频交易、操作系统内核——这些场景完全无法接受 GC 的任意停顿。C 的选择是提供 RAII 作为第一方案你要在某个角落里用上引用计数或者追踪式收集器语言也不拦你。这是一种“默认不给需要时自己搭”的哲学。在 C 里控制权永远比便利性优先这是一个关卡设计师式的思维不是保姆式的思维。这也解释了一个有趣的现象到 C20、C23 时代新的语言特性里依然没有 GC 的影子但现代所有主流平台、所有工具链都默认带上 AddressSanitizer、Valgrind、静态分析插件。C 社区给出的答案是“安全靠设计工具链共同保证”而不是单一由运行时来包办。4. 从C11到C20一场教科书级别的自我革命4.1 移动语义和右值引用C11改变了性能的基本盘C11 被普遍认为是“现代 C”和“传统 C”的分水岭。最重要的改动是移动语义的实现。以前返回一个大的 std::vector临时对象构造、拷贝、销毁一趟操作下来性能和简洁性没法两全。移动构造允许你“把别人手里那笔资源直接转过来”不再深拷贝元素开销从 O(n) 降到了 O(1)。右值引用配合 std::move本质上把“资源所有权转移”这件事通过类型系统表达出来了。你要把对象传给别人并且不再用了说一声 std::move底下数据直接腾挪。Stroustrup 对现代 C 的目标描述我很赞同让代码像高级语言一样表达同时像汇编一样贴近机器。移动语义就是这条路上关键的一步。4.2 Lambda、auto、结构化绑定表达力上来了代码才像现代诗C11 带来的 Lambda 表达式直接让“就地写回调”变成了可能。配合 库里的 sort、find_if、transform很多原来要写一个函数对象再加一堆手写循环的场景变成三五行代码。再到 C17 的结构化绑定、optional/variant/span代码表达能力确实在向更高级的语言靠拢。但我也要提醒一句auto 别无脑用到底。类型推导确实减少噪音但该把类型显式写出来的时候比如接口边界、公共函数的参数和返回值还是要写清楚。现代 C 不是“能隐藏就隐藏”而是“在合适的地方用合适的可见度”。4.3 C20的“大改款”概念、协程、模块、范围如果说 C11 是重启C20 就是二十年来的最大改款。Concepts 让你给模板参数加上约束过去模板报错里那些无法直视的几百行玄学文字现在能变成“T 必须满足 Number 约束”这样人话级别的提示。我接手老代码时把一套模板重构成带 concept 约束的版本编译错误从三百行缩到三行团队新人第一次知道模板报错是可以读懂的。协程为异步代码提供了语言级支持模块想要终结头文件地狱Ranges 让你能以管道方式组合算法飞船运算符 一行搞定所有比较运算。但有一条实话也想说C20 再好也不是让你一晚上全盘切换的。现实工程里往往是“根据项目需要选择标准级别”的渐进策略。团队里如果有新人先 C17 甚至 C11 起步把基础打牢再引 C20 的高级特性否则就是给项目平添理解成本。5. 热搜词背后的C学习者环境、八股和小游戏5.1 环境问题为什么能劝退一半的人很多人第一次接触 C先卡在环境上。Windows 下跑各种软件弹出“缺少 vcruntime140.dll / msvcp140.dll”这就是缺了 Microsoft Visual C Redistributable 运行时库。VSCode 配置 C/C 环境需要装 C/C 扩展、MinGW-w64 或者 Clang 编译器再配置 tasks.json 和 launch.json。我见过太多新手在这些配置上耗掉一整天然后放弃。说实话环境配置就是这么一件“一次搞定、长期受益”的事值得你静下心完成。比较省事的学习组合是VSCode MinGW-w64 C/C 扩展写好 launch.json 和 tasks.json 之后后面就是纯粹的写代码。Dev-C 虽然老但在教学场景里依然够用只是对现代标准支持较弱真到了工程环节还是得换。5.2 “C八股”里的设计取舍背答案不如推答案面试圈里的 C 八股绕不开这几个话题虚函数表和多态的内存布局、拷贝和移动的区别、深浅拷贝、内存对齐、栈空间与堆空间的区别、智能指针底层计数、模板特化、vector 扩容、覆盖与隐藏、const 的各种用法。你会发现这些考点几乎全部对应着 C 的设计取舍。虚函数表回答的是“零开销多态怎么实现”移动语义回答的是“怎么避免拷贝”栈和堆回答的是“生命周期怎么管理”覆盖和隐藏回答的是“名字查找的规则怎样影响代码行为”。所以我的建议是不要干背八股先手写十个基础算法题冒泡排序优化、二分查找、归并排序、单调栈、判断质数再把每个特性背后的设计意图搞清楚。真正理解所有权、值语义、RAII 三条主线之后八股是自然推出来的。5.3 写小游戏这事远比你想的更练人热词里频繁出现的“C小游戏”“游戏代码”“我的世界代码”一度被一些人觉得是新手自嗨项目。但以我自己带新人的经验认真写一个几百行的 SFML 小游戏贪吃蛇、坦克大战、打砖块对 C 的理解提升是刷一百道题都换不来的。一个小游戏里同时涉及类设计玩家、敌人、子弹、事件循环、状态管理、碰撞检测、对象生命周期、STL 容器管理动态实体列表。你会被迫去思考“这个对象该由谁持有”“这块资源什么时候释放”“多态在这里该怎么用”。等你能独立写完一个能玩的 2D 小游戏再回头谈 C你会发现自己已经跨过了“语法学习者”和“工程思维者”之间那道隐形的墙。我自己的经历也差不多。刚工作那几年天天在业务代码里和指针搏斗总觉得 C 怎么这么不友好。后来花了一个周末把当年那套模板代码匠心重构成 C20 风格配上一堆 Constraints 和 span突然意识到这个语言的边界其实比以前宽得多。你可以把它用成“带类的 C”也可以把它用成带有模板元编程的现代系统级语言——全看你愿不愿意站在 Stroustrup 那套设计哲学上去理解它。回到源头很多争吵和困惑其实从一开始就有答案。
返回列表