标签 C语言 下的文章

微软 Azure 被曝“泄露”使用 Ubuntu Linux 的用户信息

报道,一个用户在微软 Azure 云上使用企业沙箱启动了一个 Ubuntu Linux 的实例进行测试。三个小时后,在他的 LinkedIn 账户上,收到了一条来自 Canonical 销售代表的消息,他说:“我看到你在 Azure 中运行了一个 Ubuntu 镜像”,并告诉他,他将成为他的“企业中任何与 Ubuntu 相关的联系人”。

微软称,“只有在客户部署Azure 市场发布商的产品时,才会按照我们的条款和条件中的规定,与发布商分享客户信息。”而 Canonical 回应称:“根据 Azure 条款和条件,微软与 Ubuntu 的发布商 Canonical 共享在 Azure 上运行 Ubuntu 实例的开发者的联系方式。根据隐私规则,这些联系信息被保存在 Canonical 的 CRM 中。”

所以,这看起来是一则“正常”使用用户隐私的行为?对此我不多加评论,大家自行体会。

C 语言超越 Java 夺得 TIOBE 指数第一

在 2021 年 2 月的榜单中,C 语言取得了第一名。与 2020 年相比,这两种语言在榜单上的位置互换了,但榜单的其他部分几乎与一年前完全相同。Python 位于第三位,其次是 C++、C#、Visual Basic、JavaScript、PHP 和 SQL。

让人吃惊的是,一些新锐语言被老牌语言反超。在过去的一年里,Go 在榜单上跌到了第 13 位,被汇编语言、R 和 Groovy 超越。以及,Swift 从第 10 位下降到第 15 位,在去年被 Ruby 超越。

看来,编程的世界,一些老牌语言老而弥坚啊,或许 C 语言真能长青下去。

顶级银行加入 Linux 和开源专利保护组织

全球企业和投资银行巴克莱银行和拥有 2600 万全球客户的道明银行集团已经加入了开放发明网络(OIN)的 Linux 和开源保护联盟。多年来,有史以来最大的专利非侵犯联盟 OIN 保护 Linux 免受专利攻击和专利巨魔的侵害。而现在连银行都会受到专利巨魔的攻击,所以寻求加入联盟寻求保护也在情理之中。

现在所有企业现在都依赖于 Linux 和开源软件,任何攻击它们的东西都对大家的业务不利。专利巨魔的猖獗可见一斑。

代码英雄讲述了开发人员、程序员、黑客、极客和开源反叛者如何彻底改变技术前景的真实史诗。

什么是《代码英雄》

代码英雄 Command Line Heroes 是世界领先的企业开源软件解决方案供应商红帽(Red Hat)精心制作的原创音频播客,讲述开发人员、程序员、黑客、极客和开源反叛者如何彻底改变技术前景的真实史诗。该音频博客邀请到了谷歌、NASA 等重量级企业的众多技术大牛共同讲述开源、操作系统、容器、DevOps、混合云等发展过程中的动人故事。

本文是《代码英雄》系列播客《代码英雄》第三季(8):C 语言之巨变音频脚本。

导语:C 语言和 UNIX 是现代计算的根基。我们这一季介绍的许多语言都与 C 语言有关,或者至少受到 C 语言的影响。但是 UNIX 和 C 都只是 贝尔实验室 Bell Labs 的几个开发人员作为秘密计划项目创造出来两个成果而已。

贝尔实验室是二十世纪中期的一个创新中心。Jon Gertner 将其描述为一个“创意工厂”。他们在二十世纪 60 年代最大的项目之一是帮助建立一个名为 Multics 的 分时 time-sharing 操作系统。Joy Lisi Rankin 博士解释了当时关于分时系统的一些宣传,它被描述为有可能使计算成为一种公共服务。大型团队投入了数年的精力来构建 Multics —— 但这并不是他们所希望的成果。贝尔实验室在 1969 年正式远离了分时系统。但正如 Andrew Tanenbaum 所描述的那样,一个由英雄组成的小团队还是坚持了下来。C 语言和 UNIX 就是这样的结果。他们并不知道他们的工作会对技术的发展产生多大的影响。

00:00:00 - 发言人 1

我们掀起了新一波的研究浪潮。我们的创造力正在延伸。

00:00:10 - 发言人 2

噪音、噪音。

00:00:13 - 发言人 1

这些人是贝尔电话实验室的设计工程师。

00:00:16 - Saron Yitbarek

在上世纪 60 年代,坐落于新泽西州默里山的贝尔实验室,是科技革新的中心。在那里,我们的未来科技迈出了第一步。在那里,贝尔实验室发明了激光与晶体管,它还是信息论的摇篮。在 1968 年,贝尔实验室的四名程序员创造了一种极具开拓性的工具,它根本地改变了我们世界运行的方式,也标志着贝尔实验室的种种创新达到了新的高峰。

00:00:53

我是 Saron Yitbarek,这里是《代码英雄》—— 一款来自红帽公司的原创播客。在一整季的节目中,我们追寻着编程语言世界中最具影响力的一些故事,现在,我们终于迎来了这一季的结尾。我认为,我们把最好的故事留到了最后。这个故事中的编程语言使本季中提到的其他编程语言成为了可能。在正好 50 年以前,C 语言在贝尔实验室被设计出来,这是一种非常基础的通用程序设计语言。它是如此的基础,以至于我们有时候都会忘记,C 语言的发明是多么意义深远的成就。

00:01:35

为了得到事件的全貌,我们需要回到上世纪 60 年代,正好在 C 语言的诞生之前。那是一个一切似乎都有可能的时代。

00:01:46 - Jon Gertner

在上世纪 60 年代,贝尔实验室简直是研究人员的世外桃源。在今天,已经很难找到与贝尔实验室相似的企业研发实验室了。

00:01:56 - Saron Yitbarek

这是 Jon Gertner,他是《 创意工厂:贝尔实验室与美国革新大时代 The Idea Factory: Bell Labs and the Great Age of American Innovation 》的作者。我们采访了 Jon,让他大家解释当时贝尔实验室的情况。为什么在上世纪 60 年代,贝尔实验室能够成为他所说的“创意工厂”呢?

00:02:15 - Jon Gertner

我想今天我们都相信——“竞争带来伟大的科技革新”,但是我不能确定这种观点的正确性,并且,其实,贝尔实验室的成就在一定程度上是与这种观点相悖的。贝尔实验室的工程师和科学家们并没有特别大的压力,但是与此同时,由于贝尔实验室在诸多的研究实验室中的地位,它又确实可以雇佣到最优秀、最聪明的研究者,并给他们足够的时间去研究感兴趣的问题,同时提供大量的资助。如果你能证明你的研究项目符合这家电话公司(LCTT 译注:指 AT&T)的目标和理念,你就能够得到经费。

00:03:00 - Saron Yitbarek

而 Jon 强调,虽然贝尔实验室是一个商业公司的产物,但它的价值观还是比较接近学术界的。通过让员工自行决定工作方式及内容,贝尔实验室实践了类似于开源社区的开放式领导原则。

00:03:19 - Jon Gertner

这是诸如苹果、谷歌与微软这样的大公司出现前的时代。计算机的历史更多的聚焦在西海岸,聚焦于 自制计算机俱乐部 Homebrew Computer Clum 这样的组织,以及从其中发展而出的企业;但是我认为贝尔实验室和那些企业一样重要。贝尔实验室坐落于一个现在看来几乎不可思议的地方:新泽西的郊区。但是,这里曾聚集了对科技突破做出了巨大贡献的科学家、研究者和计算机工程师,他们的研究成果对全世界都有惊天动地般的显著影响。

00:03:54 - Saron Yitbarek

分时 time-sharing ”就是这些惊天动地的项目之一。它的核心概念很简单,实现难度却极大。他们能构建一个能够同时由成百上千的用户使用的操作系统吗?这样的发明将会使当时的计算机领域为之震动。从 1964 年起,贝尔实验室的天才们,与 通用电气 General Electric 和麻省理工学院(MIT)合作,试图集体推进这项工作的进展。实际上,麻省理工学院在一年前已经有了相关的研究项目,即 MAC 计划 Project MAC ;但是现在,所有这些顶级团队已经团结起来,开始着手钻研大型主机分时操作系统的构建方式。

00:04:40

实际上早在 1959 年, 约翰·麦卡锡 John McCarthy 就提出了分时操作系统的概念。请收听我们 第七集 的节目获知更多细节。他设想了一种可以在多个用户之间切换其“注意力”的大型计算机。麦卡锡认定,这样的一种机器有潜力极大地拓展现有的计算机文化。让我们来设想一下吧,如果一千名用户能够同时在一台机器上工作,你就完成了对整个编程与计算机世界的民主化。现在,这支群星荟萃的团队准备着手将麦卡锡的梦想变成现实,并为他们想象中的操作系统起了一个名字 —— Multics(LCTT 译注:Multi- 前缀代表“多人的”)。

00:05:23

Multics 团队为分时操作系统进行了多年的工作,但是该项目遇到了严重的资金困难,并且在十年之后,项目仍然看不到尽头。雪上加霜的是,项目的领导者 Bill Baker 是一个化学家,对贝尔实验室的计算机科学部门并不感兴趣。除此之外,我们仍然能找到一个新的问题 —— 自尊心问题。

00:05:46 - Jon Gertner

在贝尔实验室,人们每天习以为常的一件事情就是独自工作。我的意思是,贝尔实验室的人们有一种感觉:他们认为自己拥有一切他们所需要的的人才和构思,并且拥有最先进的科技,当他们遇到值得解决的问题时,他们有能力去解决这样的问题。这种看法可能有一定合理性;但是 Multics 项目在贝尔实验室没有进展,在某种程度上也可能是因为像这样更加复杂的、合作性的工作在贝尔实验室的体系中运转不良,也不能让那里的高管们满意。

00:06:20 - Saron Yitbarek

Jon Gertner 是 《创意工厂》 The Idea Factory 一书的作者,他刚刚发表的新书是 《世界尽头的冰》 The Ice at the End of the World

00:06:32

贝尔实验室于 1969 年四月正式宣布放弃 Multics 项目,但这是否就是故事的结尾呢?就贝尔实验室而言,分时操作系统 Multics 之梦已经破灭。但是这个梦真的结束了吗?结果却并不是这样,并非所有贝尔实验室的研究人员都放弃了分时操作系统。四个顽固的拒不退让者在所有人都已放弃之后,继续坚持这一梦想,而那就是下一个故事了。

00:07:08

说实话,有些梦想太美好了,这样的梦想是很难被人抛弃的。

00:07:12 - Joy Lisi Rankin

这是一件大事业。

00:07:14 - Saron Yitbarek

这位是 Joy Lisi Rankin,她是 《美国计算机人物史》 A People's History of Computing in the United States 一书的作者。Joy 将会和我聊聊分时操作系统的理想,以及为什么分时操作系统如此不可或缺。

00:07:27 - Joy Lisi Rankin

开发分时操作系统是一件重要且极富雄心壮志的事,直到该项目开始之前,大部分上世纪 60 年代早期的分时系统在一台主机上都约有 40 至 50 个终端。因此,提升终端的数量是重要性很高的一件事,贝尔实验室的雄心很可能超出了大部分人的认知,这也是这个项目在实现最初目的的过程中碰到了不少困难的原因。但是,尽管如此,分时操作系统继续以不同的形态发展,并真正地走向繁荣;分时操作系统不仅仅在麻省理工学院得到发展,也走向了其他的地方。

00:08:09 - Saron Yitbarek

是啊。那么,当我们谈起上世纪 60 年代,是谁在推动分时操作系统的需求?你提到了麻省理工学院、通用电气公司和贝尔实验室。那么我们的关注点是商业还是学术团体?谁才是真正的推动者?

00:08:23 - Joy Lisi Rankin

我认为学术团体和商业团体共同推动了发展的进程,除此以外,一些 科学 scientific 团体也参与了这项事业,因为,正如我之前所说,分时操作系统是一种更加一对一、富有互动性的计算体验。但是从另一个角度来看,我也会说教育工作者也同样在推动这件事的发展。并且,从国家的层面上讲,当时也在进行关于创建全国性计算设施的对话。那么,基本上来说,所谓的全国性计算设施指的就是全国性的分时操作系统网络。真的,美国的思想领袖们也有这样的言论,他们认为这样的系统会是与供电、电话、供水一样的基础性服务。

00:09:08 - Saron Yitbarek

哇哦。

00:09:08 - Joy Lisi Rankin

对啊,我知道的!这确实很……

00:09:09 - Saron Yitbarek

那可真是一项大事业。

00:09:11 - Joy Lisi Rankin

那是一项非常大的事业。

00:09:13 - Saron Yitbarek

Joy 让我想起了一件事。尽管这一期节目主要聚焦于创造了 C 语言和 UNIX 操作系统的团队,但是在贝尔实验室之外,对分时操作系统的推动是一项运动,比任何一个团队都大。将计算机视为公共设施是一个非常有意义的想法,在这项事业中,有许多优秀的人物,可惜我们不能请他们来到这里,比如 Bob Albrecht 和 Martin Greenberger ,以及其他的一些杰出人物。

00:09:37

好的,在进行了一些预先说明之后,让我继续和 Joy 的对话吧。

00:09:41 - Joy Lisi Rankin

那么,当约翰·麦卡锡在麻省理工大学的演讲上首次公开的谈论分时操作系统时,他明确的将其与电力进行了比较,并说:“这是一个让所有人都能使用计算机的方式,不仅仅是在学校里和商业活动中,还在每个人的家里。”回首过去,再阅读当时的文章与档案,许多人都确信,未来会出现一种能够被规范化管理的计算公共设施。因此,人们对这种全国性的分时基础设施充满了信心和支持。

00:10:22 - Saron Yitbarek

非常有趣的一点是,在 1970 年,IBM 实际上已经退出了分时操作系统这一产业。即使是通用电气也出售了他们的大型主机部门,不过他们还仍然保留了一部分分时操作系统相关的业务。让我们简单地谈一谈这些吧,1970 年发生了什么?

00:10:39 - Joy Lisi Rankin

我认为 1970 年已经一定程度上已经成为某种标志,这也许是人为假想的标志,这一年标志着公共计算设施与分时操作系统产业的失败。从某些角度上来说,这种观点是错误的。我认为在上世纪 60 年代末期,麻省理工和 Multics 项目明显在创建一个支持上千个终端的分时操作系统上遇到了困难,而这是一个知名度极高、影响力很大的项目。在同一时期,数十个基于分时计算模型的商业项目在美国兴起并繁荣发展。这是一个科技泡沫。随后,对于分时操作系统的热情走向衰落。这不完全是因为通用电气出售了他们的计算主机业务,他们在上世纪 70 年代至 80 年代间一直保留着他们的分时计算业务,并且这一业务盈利状况良好。除此以外,当时的大学,例如麻省理工学院,也继续运行着他们的分时操作系统,直到上世纪 80 年代。

00:11:52

因此,依我之见,“分时系统只是一个在上世纪 70 年代破碎的科技泡沫”的公共记忆之所以产生,一定程度上是因为人们过多地关注了 Multics 的困境。然而,事实上来说,如果我们回到过去,看一看当时的人们如何使用分时操作系统,以及分时操作系统赢得了多少利润,了解一下分时操作系统的成功,我们就会发现,其实上世纪 70 年代正是分时系统繁荣的年代。

00:12:17 - Saron Yitbarek

现在让我们把眼光放回到贝尔实验室,由四位技术专家组成的小组想要创造他们自己的分时操作系统。他们是 肯·汤普逊 Ken Thompson 丹尼斯·里奇 Dennis Ritchie 道格拉斯·麦克劳伊 Doug McIlroy 约瑟夫·欧桑纳 J.F. Ossanna 。不过他们并不想完成 Multics,他们想要越级跳过 Multics,制作一个不受过往拖累、功能更为强大的操作系统,他们称之为 UNIX(LCTT 译注:Uni- 这个前缀代表“单一的”)。

00:12:39 - Joy Lisi Rankin

我认为 Multics 是 UNIX 的灵感来源,其原因在于,许多在 Multics 上工作的程序员是如此享受分时操作系统在编程上的优点,以至于在 Multics 陷入困境时,他们便想要创造一个属于他们自己的分时环境。这些来自贝尔实验室的程序员,他们决定构建他们自己的编程框架与分时操作系统,这就是 UNIX 的起源。

00:13:20 - Saron Yitbarek

Joy Lisi Rankin 是 《美国计算机人物史》 A People's History of Computing in the United States 一书的作者。

00:13:29

丹尼斯·里奇 Dennis Ritchie 将自己和其他三名同事称为一个 团队 fellowship 。他们几个开发者想要作为一个紧密的四人小团体而工作,并且他们需要一种能够协调他们程序设计的硬件。但是贝尔实验室已经放弃了分时操作系统的梦想,即便它是一个学术研究的世外桃源,给已经放弃的项目拨款这件事也超出了他们的底线。因此他们拒绝了使用新硬件的提议。为此事购买新的硬件太过昂贵了,为什么要冒险呢?但研究员们还是坚持了下来。

00:14:05

汤普逊和里奇要求得到一种类似 GE645 的机器,这是他们一直用来进行 Multics 相关工作的型号。当他们得知无法得到经费时,他们刚刚在纸上潦草地写下一些关于文件系统的想法。最后,他们在一个他们称之为“太空旅行”的游戏中成功地实现了他们的一些想法,这个游戏运行在 PDP7 机型上,这种机型基本上与 Commodore 64 是同一个级别的。没有贝尔实验室的支持,他们的开发是缓慢的,至少开始是这样的,是一个字节、一个字节地前进的。这四人组复活了分时操作系统之梦,以他们称之为 UNIX 的形式。

00:14:47

不过这里就是问题所在了:UNIX 操作系统是用汇编语言写成的。也就是说,他们用纸带向 PDP7 传输文件;你可以想象到,他们在缺乏理想的工具与上级的支持的情况下,努力构建这个开创性的操作系统时所遇到的困难。UNIX 已经获得生命,但还没有一种合适的编程语言能够让它歌唱。

00:15:23

开发者们初次尝试为 UNIX 设计的语言称为 B 语言,由 肯·汤普逊 Ken Thompson 编写。

00:15:30 - Andy Tanenbaum

这是 BCPL( 基础综合编程语言 Basic Combined Programming Language )的一种衍生语言。

00:15:33 - Saron Yitbarek

这位是 安德鲁·塔能鲍姆 Andy Tanenbaum 。他是阿姆斯特丹的一位计算机科学教授,也是许多书籍的作者,包括经典教材 《计算机网络》 Computer Networks 。让我们听听他讲解汤普逊的 B 语言背后的故事。

00:15:48 - Saron Yitbarek

所以说, B 语言是 BCPL 的一种衍生物?

00:15:51 - Andy Tanenbaum

BCPL 源于一种构建 CPL 编译器的企图,这种语言编写的编译器确实能够起到作用,而 CPL 基于 ALGOL 60,ALGOL 60 语言又源于 ALGOL 58。ALGOL 58 则源于对 Fortran 进行改进的尝试。

00:16:01 - Saron Yitbarek

搞明白了吗?现在的问题就是,B 语言有许多历史包袱。B 语言和它的这些前身相比,并没有太多的突破性改变,因此,B 语言不能完成让 UNIX 歌唱的挑战。B 语言中没有变量类型,对于初学者来说这是一个问题。除此以外,B 语言对应的汇编代码仍然比 B 语言编译器的 线程代码 threaded-code 技术 [1] 要快。

00:16:31 - Andy Tanenbaum

BCPL 和 B 语言只有一种数据类型,就是 双字节类型 word 。双字节类型在基于双字节类型开发的 IBM 的 704 和 709、7090、7094 机型上效果不错,但是从 360 和其它所有的小型电脑开始的机型都是基于 单字节类型 byte 的。在这种情况下,双字节类型就不是一个好主意了,它和现代计算机的匹配程度极其糟糕。因此,显然 B 语言无法解决现有的问题。

00:16:57 - Saron Yitbarek

那么,该团队之前工作使用过的所有机器都是基于双字节类型的,但是在基于单字节对象的操作上,这种类型的机器就不够好用了。幸运的是,在这个时间点上,贝尔实验室的领导们又回来加入了 UNIX 项目,他们意识到了这个团队中正在产生令人激动的进展。他们资助了一台价值 65000 美元的 PDP-11,并且这台机器不是基于双字节类型的,而是面向单字节的。现在,装备上了 PDP-11,丹尼斯·里奇能够在处理编程语言的难题时更进一步。

00:17:36 - Andy Tanenbaum

丹尼斯,以及在肯的少量帮助下,决定编写一种更加结构化新编程语言,包含其它数据类型,比如说 字符类型 char 整数类型 int 长整数类型 long 等等。

00:17:47 - Saron Yitbarek

因此,在 1971 年至 1973 年之间, 丹尼斯·里奇 Dennis Ritchie 一直在调整 B 语言。他增加了一种字符类型,并且构建了一个新的编译器,这样就不需要再使用线程代码技术了。两年结束时,B 语言已经变成了一种崭新的语言,这就是 C 语言。

00:18:08

C 语言是一种功能强大的语言,结合了高级功能和底层特性,能够让使用者直接进行操作系统编程。它的一切都是如此的恰到好处。C 语言从机器层次中进行了足够的抽象,以至于它也可以移植到其他的机型。它并非一种只能用来写写应用的语言。它几乎是一种通用的编程工具,无论是在个人电脑还是超级计算机上都十分有效,而这一点极其重要,因为个人电脑革命当时已经近在眼前。

00:18:49

团队的成员们在确定了 C 语言就是正确的道路之后,就立刻用它重写了 UNIX 内核和许多 UNIX 组件。因此,只要你想使用 UNIX,你就必须使用 C 语言。C 语言的成功与 UNIX 的成功紧密的结合在了一起。

00:19:06 - Andy Tanenbaum

C 语言的流行,其实主要不是因为它是一门比 B 语言更优秀的语言 —— 当然它确实比 B 语言优秀 —— 而是因为,它是编写 UNIX 的语言,并且当 UNIX 广泛发行的时候,它自带了一个 C 语言编译器;甚至最后它还配备了两个 C 语言编译器。那么,UNIX 受到了广泛欢迎,每个使用它的人都有了 C 编译器,而且 UNIX 的一切都是由 C 语言写成的。而 C 语言是一种相当不错的语言,它又是与 UNIX 共同出现的,那为什么还要找其他的编程语言呢?

00:19:33 - Saron Yitbarek

从这里开始, C 语言的价值开始显现。

00:19:35 - Andy Tanenbaum

由于 UNIX 是用 C 语言写成的,并且带有一个 C 语言编译器,C 语言与 UNIX 从一开始就在一定程度上互相依赖,因此,它们也共同成长。在一个关键的时间点,C 语言在 UNIX 系统中已经足够流行时,像 Steve Johnson 这样的人开发了可移植的 C 语言编译器,这种编译器可以为其他型号的计算机产生机器码。最终,出现了面向其他操作系统的 C 语言编译器,人们开始用 C 语言编写各种各样的软件 —— 从数据库系统到……天知道什么奇奇怪怪的玩意儿,因为 C 语言在各种环境下都可用,并且十分有效,效率很高。

00:20:07 - Saron Yitbarek

因此,不久以后,人们也开始用 C 语言编写与 UNIX 无关的程序,因为这门语言的优点是显而易见的。Andy 将为我们讲述,C 语言如何完全接管了整个编程世界。

00:20:20 - Andy Tanenbaum

我想说的是,C 语言在正确的时间出现在了正确的地点。在上世纪 70 年代,计算机的普及范围远比现在要小。普通人不会拥有计算机,并且对计算机一无所知,但是在大学和大企业所拥有的计算机中,有许多都使用了 UNIX 操作系统以及随之而来的 C 语言,也就是说,这些大学和大企业都在使用 C 语言。这些大学与大企业发布了大量的软件,也产生了大量的程序员。如果一个企业想招聘一名 C 程序员,发布招聘广告后一定会有人来应聘。如果想招聘一名 B 语言程序员,没人会来面试。

00:20:49 - Saron Yitbarek

在 C 语言的世界中,有许多基础设施 —— 软件、函数库、头文件等,这一切编程工具都构成了一个完美的闭环。

00:20:59 - Andy Tanenbaum

因此,C 语言变得越来越流行。

00:21:02 - Saron Yitbarek

现在,互联网的兴起导致了人们对 C 语言安全性的关注,这些问题在变种中得到了部分解决,比如 C#。有些时候我们会觉得,好像所有的兴奋点都在 Python 或 Go 等新语言上。但是我们希望能在播客中试图做的一件事就是让大家回忆起当下的我们与历史的紧密关联,而 C 语言的影响至今仍然是不可思议的。

00:21:29

C 语言在现代最出名的产物就是 UNIX 的教子 —— Linux,而 Linux 的绝大部分都是用 C 编写的。就连 Linux 项目使用的标准编译器 GCC( GNU 编译器集合 GNU Compiler Collection ),也是用 C 语言写成的。虽然这一点可能不太引人注意,但是今天所有聚集在 Linux 上的开源编程者,都与一种在半个世纪以前的语言相联系,而 C 语言的统治也在年复一年的增强。

00:22:02 - Andy Tanenbaum

以上这些事情的结果就是世界上占支配地位的两种操作系统的诞生。一个是运行在 Linux 操作系统上的安卓,而 Linux 是重写 UNIX 操作系统的产物。而 iOS,本质上来讲是一种 4.4 版的 Berkeley UNIX。因此,安卓和 iOS 从本质上说都是 UNIX。我怀疑几乎所有的服务器都是运行在 UNIX 或 Linux 的某个版本上的。这些服务器在幕后发挥着巨大的作用,并且任何运行 UNIX 的系统都源于 C 语言,为 UNIX 所编写的一切程序都使用了 C 语言。C 语言确实是无处不在的。

00:22:41 - Saron Yitbarek

安德鲁·塔能鲍姆 Andy Tanenbaum 是一名计算机科学教授,他是《计算机网络》一书的作者。说点有趣的题外话吧,他同时也是 MINIX,一个免费、开源版本的 UNIX 的作者,而 MINIX 事实上也是 林纳斯•托瓦兹 Linus Torvalds 开发 Linux 的灵感来源。当然,Andy 使用 C 语言编写 MINIX。

00:23:03 - Saron Yitbarek

今天,C 语言存在于我们生活中的任何一个角落,从火星上的漫游车到台式电脑上的浏览器。它影响了许多我们在本季节目中提到的语言,例如 Go、Javascript 和 Perl。由于 C 语言与 UNIX 密不可分的联系,C 语言很可能是分布最广泛的编程语言。

00:23:28 - 发言人 7

1998 年美国国家科学奖的获得者是——来自朗讯科技公司贝尔实验室的 肯·汤普逊 Kenneth L. Thompson 丹尼斯·里奇 Dennis M. Ritchie 的团队。

00:23:40 - Saron Yitbarek

回望上世纪 60 年代,这四位贝尔实验室的员工—— 肯·汤普逊 Ken Thompson 丹尼斯·里奇 Dennis Ritchie 道格拉斯·麦克劳伊 Doug McIlroy 约瑟夫·欧桑纳 J.F. Ossanna ——他们那时还不得不向上级乞求关注和资助。但是在 1998 年,汤普逊和里奇就收到了美国国家科学奖,这是为了表彰他们在 C 语言和 UNIX 上的工作。他们也共享了一百万美元的图灵奖奖金。历史的眼光是公正的。

00:24:10

在一整季的节目中,我们一直在追寻那些我们最喜爱的编程语言的发展沿革与魅力。无论它们像 C 语言一样搭上了操作系统发展的便车,又或者是像 Go 语言一样在一种新的基础架构上发展,有一件事是永恒不变的:编程语言有它们自己的生命。它们是活着的。它们出生,成长,走向成熟。有时,编程语言也会变老,走向消亡。我们越多的了解这些语言,我们越会发现编程语言是一股重要的力量,它们总是在不断地变化,以切合时代的需要。我们的职责就是意识到这些变化,并且加以回应。我们的语言一直都是构建我们想要的世界的最佳工具。

00:25:00

以上就是我们所有第三季的《代码英雄》节目。我希望大家喜欢收听我们的节目。节目的第四季已经在制作中,即将推出,敬请期待。

00:25:13

《代码英雄》是来自红帽公司的原创播客。

00:25:18

如果你想深入了解 C 语言或者本季节目中我们提到的任何其他编程语言的故事,欢迎访问 redhat.com/commandlineheroes。我是 Saron Yitbarek ,下期之前,编程不止。


  1. 线程代码 threaded-code 技术:一种通过把一系列调用指令转换成一完整的地址表,然后使用恰当的方式调用的技术。线程代码最初被用来减少代码的占用空间,提高代码密度。通俗地讲,这种技术有点类似于在 C 语言中把一系列的 switch-case 语句转化为用函数指针数组实现的形式。 ↩︎

什么是 LCTT SIG 和 LCTT LCRH SIG

LCTT SIG 是 LCTT 特别兴趣小组 Special Interest Group ,LCTT SIG 是针对特定领域、特定内容的翻译小组,翻译组成员将遵循 LCTT 流程和规范,参与翻译,并获得相应的奖励。LCRH SIG 是 LCTT 联合红帽(Red Hat)发起的 SIG,当前专注任务是《代码英雄》系列播客的脚本汉化,已有数十位贡献者加入。敬请每周三、周五期待经过我们精心翻译、校对和发布的译文。

欢迎加入 LCRH SIG 一同参与贡献,并领取红帽(Red Hat)和我们联合颁发的专属贡献者证书。


via: https://www.redhat.com/en/command-line-heroes/season-3/the-c-change

作者:Red Hat 选题:bestony 译者:QwQ2000 校对:Northurland, wxy

本文由 LCRH 原创编译,Linux中国 荣誉推出

我们将所有的 C 语言要素放置到一份易读的备忘录上。

1972 年, 丹尼斯·里奇 Dennis Ritchie 任职于 贝尔实验室 Bell Labs ,在几年前,他和他的团队成员发明了 Unix 。在创建了一个经久不衰的操作系统(至今仍在使用)之后,他需要一种好的方法来对这些 Unix 计算机编程,以便它们可用执行新的任务。在现在看来,这很奇怪,但在当时,编程语言相对较少,Fortran、Lisp、Algol 以及 B 语言都很流行,但是,对于贝尔实验室的研究员们想要做的事情来说,它们还是远远不够的。丹尼斯·里奇表现出一种后来被称为程序员的主要特征的特质:创造了他自己的解决方案。他称之为 C 语言,并且在近 50 年后,它仍在广泛的使用。

为什么你应该学习 C 语言

今天,有很多语言为程序员提供了比 C 语言更多的特性。最明显的是 C++ 语言,这是一种以相当露骨的方式命名的语言,它构建在 C 语言之上,创建了一种很好的面向对象语言。不过,许多其它语言的存在是有充分理由的。计算机擅长一致的重复,因此任何可预见的东西都可以构建在编程语言中,对程序员来说这意味着更少的工作量。为什么在 C++ 语言中用一行语句就可以将一个 int 转换为一个 long 时(long x = long(n);),还要在 C 语言用两行语句呢?

然而,C 语言在今天仍然有用。

首先,C 语言是一种相当简约和直接的语言。除了编程的基础知识之外,并没有很高级的概念,这很大程度上是因为 C 语言实际上就是现代编程语言的基础之一。例如,C 语言的特性之一是数组,但是它不提供字典(除非你自己写一个)。当你学习 C 语言时,你会学习编程的基础组成部分,它可以帮助你认识到如今的编程语言的改进及其的精心设计。

因为 C 语言是一种最小化的编程语言,你的应用程序很可能会获得性能上的提升,这在其它许多编程语言中是看不到的。当你考虑你的代码可以执行多快的时候,很容易陷入锱铢必较的境地,因此,重要的是要问清楚你是否需要为某一特定任务提供更多的速度。与 Python 或 Java 相比,使用 C 语言,你在每行代码中需要纠结的地方更少。C 语言程序运行很快。这是 Linux 内核使用 C 语言编写的一个很好的理由。

最后,C 语言很容易入门,特别是,如果你正在运行 Linux,就已经能运行 C 语言代码了,因为 Linux 系统包含 GNU C 库(glibc)。为了编写和构建 C 语言程序,你需要做的全部工作就是安装一个编译器,打开一个文本编辑器,开始编码。

开始学习 C 语言

如果你正在运行 Linux ,你可以使用你的软件包管理器安装一个 C 编译器。在 Fedora 或 RHEL 上:

$ sudo dnf install gcc

在 Debian 及其衍生系统上:

$ sudo apt install build-essential

在 macOS 上,你可以 安装 Homebrew ,并使用它来安装 GCC

$ brew install gcc

在 Windows 上, 你可以使用 MinGW 安装一套最小的包含 GCC 的 GNU 实用程序集。

在 Linux 或 macOS 上验证你已经安装的 GCC:

$ gcc --version
gcc (GCC) x.y.z
Copyright (C) 20XX Free Software Foundation, Inc.

在 Windows 上,提供 EXE 文件的完整路径:

PS> C:\MinGW\bin\gcc.exe --version
gcc.exe (MinGW.org GCC Build-2) x.y.z
Copyright (C) 20XX Free Software Foundation, Inc.

C 语法

C 语言不是一种脚本语言。它是一种编译型语言,这意味着它由 C 编译器处理来产生一个二进制可执行文件。这不同于脚本语言(如 Bash)或混合型语言(如 Python)。

在 C 语言中,你可以创建函数来执行你希望做到的任务。默认情况下,执行的是一个名为 main 的函数。

这里是一个使用 C 语言写的简单的 “hello world” 程序:

#include <stdio.h>

int main() {
  printf("Hello world");
  return 0;
}

第一行包含一个被称为 stdio.h(标准输入和输出)的 头文件,它基本上是自由使用的、非常初级的 C 语言代码,你可以在你自己的程序中重复使用它。然后创建了一个由一条基本的输出语句构成的名为 main 的函数。保存这些文本到一个被称为 hello.c 的文件中,然后使用 GCC 编译它:

$ gcc hello.c --output hello

尝试运行你的 C 语言程序:

$ ./hello
Hello world$

返回值

这是 Unix 哲学的一部分,一个函数在执行后“返回”一些东西:在成功时不返回任何东西,在失败时返回其它的一些东西(例如,一个错误信息)。这些返回的内容通常使用数字(确切地说是整数)表示:0 表示没有错误,任何大于 0 的数字都表示一些不成功的状态。

Unix 和 Linux 被设计成在运行成功时保持沉默是很明智的。这是为了让你在执行一系列命令时,假设没有任何错误或警告会妨碍你的工作,从而可以始终为成功执行做准备。类似地,在 C 语言中的函数在设计上也预期不出现错误。

你可以通过一个小的修改,让你的程序看起来是失败的,就可以看到这一点:

include <stdio.h>

int main() {
  printf("Hello world");
  return 1;
}

编译它:

$ gcc hello.c --output failer

现在使用一个内置的 Linux 测试方式来运行它。仅在成功时,&& 操作符才会执行一个命令的第二部分。例如:

$ echo "success" && echo "it worked"
success
it worked

失败时,|| 测试会执行一个命令的第二部分。

$ ls blah || echo "it did not work"
ls: cannot access 'blah': No such file or directory
it did not work

现在,尝试你的程序,在成功时,它返回 0;而是返回 1

$ ./failer && echo "it worked"
String is: hello

这个程序成功地执行了,但是没有触发第二个命令。

变量和类型

在一些语言中,你可以创建变量而不具体指定变量所包含的数据的类型。这些语言如此设计使得解释器需要对一个变量运行一些测试来视图发现变量是什么样的数据类型。例如,var=1 定义了一个整型数,当你创建一个表达式将 var 与某些东西相加时,Python 知道显然它是一个整型数。它同样知道当你连接 helloworld 时,单词 world 是一个字符串。

C 语言不会为你做任何这些识别和调查;你必须自己定义你的变量类型。这里有几种变量类型,包括整型(int),字符型(char),浮点型(float),布尔型(boolean)。

你可能也注意到这里没有字符串类型。与 Python 和 Java 和 Lua 以及其它的编程语言不同,C 语言没有字符串类型,而是将字符串看作一个字符数组。

这里是一些简单的代码,它建立了一个 char 数组变量,然后使用 printf 将数组变量和一段简单的信息打印到你的屏幕上:

#include <stdio.h>

int main() {
   char var[6] = "hello";
   printf("Your string is: %s
",var);
}

你可能会注意到,这个代码示例向一个由五个字母组成的单词提供了六个字符的空间。这是因为在字符串的结尾有处一个隐藏的终止符,它占用了数组中的一个字节。你可以通过编译和执行代码来运行它:

$ gcc hello.c --output hello
$ ./hello
hello

函数

和其它的编程语言一样,C 函数也接受可选的参数。你可以通过定义你希望函数接受的数据类型,来将参数从一个函数传递到另一个函数:

#include <stdio.h>

int printmsg(char a[]) {
   printf("String is: %s
",a);
}

int main() {
   char a[6] = "hello";
   printmsg(a);
   return 0;
}

简单地将一个函数分解为两个函数的这种方法并不是非常有用,但是它演示了默认运行 main 函数以及如何在函数之间传递数据。

条件语句

在真实的编程中,你通常希望你的代码根据数据做出判断。这是使用条件语句完成的,if 语句是其中最基础的一个语句。

为了使这个示例程序更具动态性,你可以包含 string.h 头文件,顾名思义,它包含用于检查字符串的代码。尝试使用来自 string.h 文件中的 strlen 函数测试传递给 printmsg 函数的字符串是否大于 0

#include <stdio.h>
#include <string.h>

int printmsg(char a[]) {
  size_t len = strlen(a);
  if ( len > 0) {
    printf("String is: %s
",a);
  }
}

int main() {
   char a[6] = "hello";
   printmsg(a);
   return 1;
}

正如在这个示例中所实现的,该条件永远都不会是非真的,因为所提供的字符串总是 hello,它的长度总是大于 0。这个不够认真的重新实现的 echo 命令的最后一点要做是接受来自用户的输入。

命令参数

stdio.h 文件包含的代码在每次程序启动时提供了两个参数: 一个是命令中包含多少项的计数(argc),一个是包含每个项的数组(argv)。例如, 假设你发出这个虚构的命令:

$ foo -i bar

argc3argv 的内容是:

  • argv[0] = foo
  • argv[1] = -i
  • argv[2] = bar

你可以修改示例 C 语言程序来以字符串方式接受 argv[2],而不是默认的 hello 吗?

命令式编程语言

C 语言是一种命令式编程语言。它不是面向对象的,也没有类结构。使用 C 语言的经验可以教你很多关于如何处理数据,以及如何更好地管理你的代码运行时生成的数据。多使用 C 语言,你最后能够编写出其它语言(例如 Python 和 Lua)可以使用的库。

想要了解更多关于 C 的知识,你需要使用它。在 /usr/include/ 中查找有用的 C 语言头文件,并且看看你可以做什么小任务来使 C 语言对你有用。在学习的过程中,使用来自 FreeDOS 的 Jim Hall 编写的 C 语言忘备录。它在一张双面纸忘备录上放置了所有的基本要素,所以在你练习时,可以立即访问 C 语言语法的所有要素。


via: https://opensource.com/article/20/8/c-programming-cheat-sheet

作者:Seth Kenlon 选题:lujun9972 译者:robsean 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

我将在本系列的第二篇中深入研究由多个文件组成的 C 程序的结构。

第一篇中,我设计了一个名为喵呜喵呜的多文件 C 程序,该程序实现了一个玩具编解码器。我也提到了程序设计中的 Unix 哲学,即在一开始创建多个空文件,并建立一个好的结构。最后,我创建了一个 Makefile 文件夹并阐述了它的作用。在本文中将另一个方向展开:现在我将介绍简单但具有指导性的喵呜喵呜编解码器的实现。

当读过我的《如何写一个好的 C 语言 main 函数》后,你会觉得喵呜喵呜编解码器的 main.c 文件的结构很熟悉,其主体结构如下:

/* main.c - 喵呜喵呜流式编解码器 */

/* 00 系统包含文件 */
/* 01 项目包含文件 */
/* 02 外部声明 */
/* 03 定义 */
/* 04 类型定义 */
/* 05 全局变量声明(不要用)*/
/* 06 附加的函数原型 */
   
int main(int argc, char *argv[])
{
  /* 07 变量声明 */
  /* 08 检查 argv[0] 以查看该程序是被如何调用的 */
  /* 09 处理来自用户的命令行选项 */
  /* 10 做点有用的事情 */
}
   
/* 11 其它辅助函数 */

包含项目头文件

位于第二部分中的 /* 01 项目包含文件 */ 的源代码如下:

/* main.c - 喵呜喵呜流式编解码器 */
...
/* 01 项目包含文件 */
#include "main.h"
#include "mmecode.h"
#include "mmdecode.h"

#include 是 C 语言的预处理命令,它会将该文件名的文件内容拷贝到当前文件中。如果程序员在头文件名称周围使用双引号(""),编译器将会在当前目录寻找该文件。如果文件被尖括号包围(<>),编译器将在一组预定义的目录中查找该文件。

main.h 文件中包含了 main.c 文件中用到的定义和类型定义。我喜欢尽可能多将声明放在头文件里,以便我在我的程序的其他位置使用这些定义。

头文件 mmencode.hmmdecode.h 几乎相同,因此我以 mmencode.h 为例来分析。

/* mmencode.h - 喵呜喵呜流编解码器 */
  
#ifndef _MMENCODE_H
#define _MMENCODE_H
  
#include <stdio.h>
  
int mm_encode(FILE *src, FILE *dst);
  
#endif /* _MMENCODE_H */

#ifdef#define#endif 指令统称为 “防护” 指令。其可以防止 C 编译器在一个文件中多次包含同一文件。如果编译器在一个文件中发现多个定义/原型/声明,它将会产生警告。因此这些防护措施是必要的。

在这些防护内部,只有两个东西:#include 指令和函数原型声明。我在这里包含了 stdio.h 头文件,以便于能在函数原型中使用 FILE 定义。函数原型也可以被包含在其他 C 文件中,以便于在文件的命名空间中创建它。你可以将每个文件视为一个独立的命名空间,其中的变量和函数不能被另一个文件中的函数或者变量使用。

编写头文件很复杂,并且在大型项目中很难管理它。不要忘记使用防护。

喵呜喵呜编码的最终实现

该程序的功能是按照字节进行 MeowMeow 字符串的编解码,事实上这是该项目中最简单的部分。截止目前我所做的工作便是支持允许在适当的位置调用此函数:解析命令行,确定要使用的操作,并打开将要操作的文件。下面的循环是编码的过程:

/* mmencode.c - 喵呜喵呜流式编解码器 */
...
   while (!feof(src)) {

     if (!fgets(buf, sizeof(buf), src))
       break;

     for(i=0; i<strlen(buf); i++) {
       lo = (buf[i] & 0x000f);
       hi = (buf[i] & 0x00f0) >> 4;
       fputs(tbl[hi], dst);
       fputs(tbl[lo], dst);
     }
   }

简单的说,当文件中还有数据块时( feof(3) ),该循环读取(feof(3) )文件中的一个数据块。然后将读入的内容的每个字节分成两个 hilo 半字节 nibble 。半字节是半个字节,即 4 个位。这里的奥妙之处在于可以用 4 个位来编码 16 个值。我将 hilo 用作 16 个字符串查找表 tbl 的索引,表中包含了用半字节编码的 MeowMeow 字符串。这些字符串使用 fputs(3) 函数写入目标 FILE 流,然后我们继续处理缓存区的下一个字节。

该表使用 table.h 中的宏定义进行初始化,在没有特殊原因(比如:要展示包含了另一个项目的本地头文件)时,我喜欢使用宏来进行初始化。我将在未来的文章中进一步探讨原因。

喵呜喵呜解码的实现

我承认在开始工作前花了一些时间。解码的循环与编码类似:读取 MeowMeow 字符串到缓冲区,将编码从字符串转换为字节

 /* mmdecode.c - 喵呜喵呜流式编解码器 */
 ...
 int mm_decode(FILE *src, FILE *dst)
 {
   if (!src || !dst) {
     errno = EINVAL;
     return -1;
   }
   return stupid_decode(src, dst);
 }

这不符合你的期望吗?

在这里,我通过外部公开的 mm_decode() 函数公开了 stupid_decode() 函数细节。我上面所说的“外部”是指在这个文件之外。因为 stupid_decode() 函数不在该头文件中,因此无法在其他文件中调用它。

当我们想发布一个可靠的公共接口时,有时候会这样做,但是我们还没有完全使用函数解决问题。在本例中,我编写了一个 I/O 密集型函数,该函数每次从源中读取 8 个字节,然后解码获得 1 个字节写入目标流中。较好的实现是一次处理多于 8 个字节的缓冲区。更好的实现还可以通过缓冲区输出字节,进而减少目标流中单字节的写入次数。

/* mmdecode.c - 喵呜喵呜流式编解码器 */
...
int stupid_decode(FILE *src, FILE *dst)
{
  char           buf[9];
  decoded_byte_t byte;
  int            i;
    
  while (!feof(src)) {
    if (!fgets(buf, sizeof(buf), src))
      break;
    byte.field.f0 = isupper(buf[0]);
    byte.field.f1 = isupper(buf[1]);
    byte.field.f2 = isupper(buf[2]);
    byte.field.f3 = isupper(buf[3]);
    byte.field.f4 = isupper(buf[4]);
    byte.field.f5 = isupper(buf[5]);
    byte.field.f6 = isupper(buf[6]);
    byte.field.f7 = isupper(buf[7]);
      
    fputc(byte.value, dst);
  }
  return 0;
}

我并没有使用编码器中使用的位移方法,而是创建了一个名为 decoded_byte_t 的自定义数据结构。

/* mmdecode.c - 喵呜喵呜流式编解码器 */
...

typedef struct {
  unsigned char f7:1;
  unsigned char f6:1;
  unsigned char f5:1;
  unsigned char f4:1;
  unsigned char f3:1;
  unsigned char f2:1;
  unsigned char f1:1;
  unsigned char f0:1;
} fields_t;
  
typedef union {
  fields_t      field;
  unsigned char value;
} decoded_byte_t;

初次看到代码时可能会感到有点儿复杂,但不要放弃。decoded_byte_t 被定义为 fields_tunsigned char联合。可以将联合中的命名成员看作同一内存区域的别名。在这种情况下,valuefield 指向相同的 8 位内存区域。将 field.f0 设置为 1 也将会设置 value 中的最低有效位。

虽然 unsigned char 并不神秘,但是对 fields_t 的类型定义(typedef)也许看起来有些陌生。现代 C 编译器允许程序员在结构体中指定单个位字段的值。字段所在的类型是一个无符号整数类型,并在成员标识符后紧跟一个冒号和一个整数,该整数指定了位字段的长度。

这种数据结构使得按字段名称访问字节中的每个位变得简单,并可以通过联合中的 value 字段访问组合后的值。我们依赖编译器生成正确的移位指令来访问字段,这可以在调试时为你节省不少时间。

最后,因为 stupid_decode() 函数一次仅从源 FILE 流中读取 8 个字节,所以它效率并不高。通常我们尝试最小化读写次数,以提高性能和降低调用系统调用的开销。请记住:少量的读取/写入大的块比大量的读取/写入小的块好得多。

总结

用 C 语言编写一个多文件程序需要程序员要比只是是一个 main.c 做更多的规划。但是当你添加功能或者重构时,只需要多花费一点儿努力便可以节省大量时间以及避免让你头痛的问题。

回顾一下,我更喜欢这样做:多个文件,每个文件仅有简单功能;通过头文件公开那些文件中的小部分功能;把数字常量和字符串常量保存在头文件中;使用 Makefile 而不是 Bash 脚本来自动化处理事务;使用 main() 函数来处理命令行参数解析并作为程序主要功能的框架。

我知道我只是蜻蜓点水般介绍了这个简单的程序,并且我很高兴知道哪些事情对你有所帮助,以及哪些主题需要详细的解释。请在评论中分享你的想法,让我知道。


via: https://opensource.com/article/19/7/structure-multi-file-c-part-2

作者:Erik O'Shaughnessy 选题:lujun9972 译者:萌新阿岩 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

学习如何构造一个 C 文件并编写一个 C main 函数来成功地处理命令行参数。

我知道,现在孩子们用 Python 和 JavaScript 编写他们的疯狂“应用程序”。但是不要这么快就否定 C 语言 —— 它能够提供很多东西,并且简洁。如果你需要速度,用 C 语言编写可能就是你的答案。如果你正在寻找稳定的职业或者想学习如何捕获空指针解引用,C 语言也可能是你的答案!在本文中,我将解释如何构造一个 C 文件并编写一个 C main 函数来成功地处理命令行参数。

我:一个顽固的 Unix 系统程序员。

你:一个有编辑器、C 编译器,并有时间打发的人。

让我们开工吧。

一个无聊但正确的 C 程序

 title=

C 程序以 main() 函数开头,通常保存在名为 main.c 的文件中。

/* main.c */
int main(int argc, char *argv[]) {

}

这个程序可以编译但不任何事。

$ gcc main.c
$ ./a.out -o foo -vv
$

正确但无聊。

main 函数是唯一的。

main() 函数是开始执行时所执行的程序的第一个函数,但不是第一个执行的函数。第一个函数是 _start(),它通常由 C 运行库提供,在编译程序时自动链入。此细节高度依赖于操作系统和编译器工具链,所以我假装没有提到它。

main() 函数有两个参数,通常称为 argcargv,并返回一个有符号整数。大多数 Unix 环境都希望程序在成功时返回 0(零),失败时返回 -1(负一)。

参数名称描述
argc参数个数参数向量的个数
argv参数向量字符指针数组

参数向量 argv 是调用你的程序的命令行的标记化表示形式。在上面的例子中,argv 将是以下字符串的列表:

argv = [ "/path/to/a.out", "-o", "foo", "-vv" ];

参数向量在其第一个索引 argv[0] 中确保至少会有一个字符串,这是执行程序的完整路径。

main.c 文件的剖析

当我从头开始编写 main.c 时,它的结构通常如下:

/* main.c */
/* 0 版权/许可证 */
/* 1 包含 */
/* 2 定义 */
/* 3 外部声明 */
/* 4 类型定义 */
/* 5 全局变量声明 */
/* 6 函数原型 */

int main(int argc, char *argv[]) {
/* 7 命令行解析 */
}

/* 8 函数声明 */

下面我将讨论这些编号的各个部分,除了编号为 0 的那部分。如果你必须把版权或许可文本放在源代码中,那就放在那里。

另一件我不想讨论的事情是注释。

“评论谎言。”
- 一个愤世嫉俗但聪明又好看的程序员。

与其使用注释,不如使用有意义的函数名和变量名。

鉴于程序员固有的惰性,一旦添加了注释,维护负担就会增加一倍。如果更改或重构代码,则需要更新或扩充注释。随着时间的推移,代码会变得面目全非,与注释所描述的内容完全不同。

如果你必须写注释,不要写关于代码正在做什么,相反,写下代码为什么要这样写。写一些你将要在五年后读到的注释,那时你已经将这段代码忘得一干二净。世界的命运取决于你。不要有压力。

1、包含

我添加到 main.c 文件的第一个东西是包含文件,它们为程序提供大量标准 C 标准库函数和变量。C 标准库做了很多事情。浏览 /usr/include 中的头文件,你可以了解到它们可以做些什么。

#include 字符串是 C 预处理程序(cpp)指令,它会将引用的文件完整地包含在当前文件中。C 中的头文件通常以 .h 扩展名命名,且不应包含任何可执行代码。它只有宏、定义、类型定义、外部变量和函数原型。字符串 <header.h> 告诉 cpp 在系统定义的头文件路径中查找名为 header.h 的文件,它通常在 /usr/include 目录中。

/* main.c */
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <libgen.h>
#include <errno.h>
#include <string.h>
#include <getopt.h>
#include <sys/types.h>

这是我默认会全局包含的最小包含集合,它将引入:

#include 文件提供的东西
stdio提供 FILEstdinstdoutstderrfprint() 函数系列
stdlib提供 malloc()calloc()realloc()
unistd提供 EXIT_FAILUREEXIT_SUCCESS
libgen提供 basename() 函数
errno定义外部 errno 变量及其可以接受的所有值
string提供 memcpy()memset()strlen() 函数系列
getopt提供外部 optargopterroptindgetopt() 函数
sys/types类型定义快捷方式,如 uint32_tuint64_t

2、定义

/* main.c */
<...>

#define OPTSTR "vi:o:f:h"
#define USAGE_FMT  "%s [-v] [-f hexflag] [-i inputfile] [-o outputfile] [-h]"
#define ERR_FOPEN_INPUT  "fopen(input, r)"
#define ERR_FOPEN_OUTPUT "fopen(output, w)"
#define ERR_DO_THE_NEEDFUL "do_the_needful blew up"
#define DEFAULT_PROGNAME "george"

这在现在没有多大意义,但 OPTSTR 定义我这里会说明一下,它是程序推荐的命令行开关。参考 getopt(3) man 页面,了解 OPTSTR 将如何影响 getopt() 的行为。

USAGE_FMT 定义了一个 printf() 风格的格式字符串,它用在 usage() 函数中。

我还喜欢将字符串常量放在文件的 #define 这一部分。如果需要,把它们收集在一起可以更容易地修正拼写、重用消息和国际化消息。

最后,在命名 #define 时全部使用大写字母,以区别变量和函数名。如果需要,可以将单词放连在一起或使用下划线分隔,只要确保它们都是大写的就行。

3、外部声明

/* main.c */
<...>

extern int errno;
extern char *optarg;
extern int opterr, optind;

extern 声明将该名称带入当前编译单元的命名空间(即 “文件”),并允许程序访问该变量。这里我们引入了三个整数变量和一个字符指针的定义。opt 前缀的几个变量是由 getopt() 函数使用的,C 标准库使用 errno 作为带外通信通道来传达函数可能的失败原因。

4、类型定义

/* main.c */
<...>

typedef struct {
  int           verbose;
  uint32_t      flags;
  FILE         *input;
  FILE         *output;
} options_t;

在外部声明之后,我喜欢为结构、联合和枚举声明 typedef。命名一个 typedef 是一种传统习惯。我非常喜欢使用 _t 后缀来表示该名称是一种类型。在这个例子中,我将 options_t 声明为一个包含 4 个成员的 struct。C 是一种空格无关的编程语言,因此我使用空格将字段名排列在同一列中。我只是喜欢它看起来的样子。对于指针声明,我在名称前面加上星号,以明确它是一个指针。

5、全局变量声明

/* main.c */
<...>

int dumb_global_variable = -11;

全局变量是一个坏主意,你永远不应该使用它们。但如果你必须使用全局变量,请在这里声明,并确保给它们一个默认值。说真的,不要使用全局变量

6、函数原型

/* main.c */
<...>

void usage(char *progname, int opt);
int do_the_needful(options_t *options);

在编写函数时,将它们添加到 main() 函数之后而不是之前,在这里放函数原型。早期的 C 编译器使用单遍策略,这意味着你在程序中使用的每个符号(变量或函数名称)必须在使用之前声明。现代编译器几乎都是多遍编译器,它们在生成代码之前构建一个完整的符号表,因此并不严格要求使用函数原型。但是,有时你无法选择代码要使用的编译器,所以请编写函数原型并继续这样做下去。

当然,我总是包含一个 usage() 函数,当 main() 函数不理解你从命令行传入的内容时,它会调用这个函数。

7、命令行解析

/* main.c */
<...>

int main(int argc, char *argv[]) {
    int opt;
    options_t options = { 0, 0x0, stdin, stdout };

    opterr = 0;

    while ((opt = getopt(argc, argv, OPTSTR)) != EOF) 
       switch(opt) {
           case 'i':
              if (!(options.input = fopen(optarg, "r")) ){
                 perror(ERR_FOPEN_INPUT);
                 exit(EXIT_FAILURE);
                 /* NOTREACHED */
              }
              break;

           case 'o':
              if (!(options.output = fopen(optarg, "w")) ){
                 perror(ERR_FOPEN_OUTPUT);
                 exit(EXIT_FAILURE);
                 /* NOTREACHED */
              }    
              break;
              
           case 'f':
              options.flags = (uint32_t )strtoul(optarg, NULL, 16);
              break;

           case 'v':
              options.verbose += 1;
              break;

           case 'h':
           default:
              usage(basename(argv[0]), opt);
              /* NOTREACHED */
              break;
       }

    if (do_the_needful(&options) != EXIT_SUCCESS) {
       perror(ERR_DO_THE_NEEDFUL);
       exit(EXIT_FAILURE);
       /* NOTREACHED */
    }

    return EXIT_SUCCESS;
}

好吧,代码有点多。这个 main() 函数的目的是收集用户提供的参数,执行最基本的输入验证,然后将收集到的参数传递给使用它们的函数。这个示例声明一个使用默认值初始化的 options 变量,并解析命令行,根据需要更新 options

main() 函数的核心是一个 while 循环,它使用 getopt() 来遍历 argv,寻找命令行选项及其参数(如果有的话)。文件前面定义的 OPTSTR 是驱动 getopt() 行为的模板。opt 变量接受 getopt() 找到的任何命令行选项的字符值,程序对检测命令行选项的响应发生在 switch 语句中。

如果你注意到了可能会问,为什么 opt 被声明为 32 位 int,但是预期是 8 位 char?事实上 getopt() 返回一个 int,当它到达 argv 末尾时取负值,我会使用 EOF文件末尾标记)匹配。char 是有符号的,但我喜欢将变量匹配到它们的函数返回值。

当检测到一个已知的命令行选项时,会发生特定的行为。在 OPTSTR 中指定一个以冒号结尾的参数,这些选项可以有一个参数。当一个选项有一个参数时,argv 中的下一个字符串可以通过外部定义的变量 optarg 提供给程序。我使用 optarg 来打开文件进行读写,或者将命令行参数从字符串转换为整数值。

这里有几个关于代码风格的要点:

  • opterr 初始化为 0,禁止 getopt 触发 ?
  • main() 的中间使用 exit(EXIT_FAILURE);exit(EXIT_SUCCESS);
  • /* NOTREACHED */ 是我喜欢的一个 lint 指令。
  • 在返回 int 类型的函数末尾使用 return EXIT_SUCCESS;
  • 显示强制转换隐式类型。

这个程序的命令行格式,经过编译如下所示:

$ ./a.out -h
a.out [-v] [-f hexflag] [-i inputfile] [-o outputfile] [-h]

事实上,在编译后 usage() 就会向 stderr 发出这样的内容。

8、函数声明

/* main.c */
<...>

void usage(char *progname, int opt) {
   fprintf(stderr, USAGE_FMT, progname?progname:DEFAULT_PROGNAME);
   exit(EXIT_FAILURE);
   /* NOTREACHED */
}

int do_the_needful(options_t *options) {

   if (!options) {
     errno = EINVAL;
     return EXIT_FAILURE;
   }

   if (!options->input || !options->output) {
     errno = ENOENT;
     return EXIT_FAILURE;
   }

   /* XXX do needful stuff */

   return EXIT_SUCCESS;
}

我最后编写的函数不是个样板函数。在本例中,函数 do_the_needful() 接受一个指向 options_t 结构的指针。我验证 options 指针不为 NULL,然后继续验证 inputoutput 结构成员。如果其中一个测试失败,返回 EXIT_FAILURE,并且通过将外部全局变量 errno 设置为常规错误代码,我可以告知调用者常规的错误原因。调用者可以使用便捷函数 perror() 来根据 errno 的值发出便于阅读的错误消息。

函数几乎总是以某种方式验证它们的输入。如果完全验证代价很大,那么尝试执行一次并将验证后的数据视为不可变。usage() 函数使用 fprintf() 调用中的条件赋值验证 progname 参数。接下来 usage() 函数就退出了,所以我不会费心设置 errno,也不用操心是否使用正确的程序名。

在这里,我要避免的最大错误是解引用 NULL 指针。这将导致操作系统向我的进程发送一个名为 SYSSEGV 的特殊信号,导致不可避免的死亡。用户最不希望看到的是由 SYSSEGV 而导致的崩溃。最好是捕获 NULL 指针以发出更合适的错误消息并优雅地关闭程序。

有些人抱怨在函数体中有多个 return 语句,他们喋喋不休地说些“控制流的连续性”之类的东西。老实说,如果函数中间出现错误,那就应该返回这个错误条件。写一大堆嵌套的 if 语句只有一个 return 绝不是一个“好主意”™。

最后,如果你编写的函数接受四个以上的参数,请考虑将它们绑定到一个结构中,并传递一个指向该结构的指针。这使得函数签名更简单,更容易记住,并且在以后调用时不会出错。它还可以使调用函数速度稍微快一些,因为需要复制到函数堆栈中的东西更少。在实践中,只有在函数被调用数百万或数十亿次时,才会考虑这个问题。如果认为这没有意义,那也无所谓。

等等,你不是说没有注释吗!?!!

do_the_needful() 函数中,我写了一种特殊类型的注释,它被是作为占位符设计的,而不是为了说明代码:

/* XXX do needful stuff */

当你写到这里时,有时你不想停下来编写一些特别复杂的代码,你会之后再写,而不是现在。那就是我留给自己再次回来的地方。我插入一个带有 XXX 前缀的注释和一个描述需要做什么的简短注释。之后,当我有更多时间的时候,我会在源代码中寻找 XXX。使用什么前缀并不重要,只要确保它不太可能在另一个上下文环境(如函数名或变量)中出现在你代码库里。

把它们组合在一起

好吧,当你编译这个程序后,它仍然几乎没有任何作用。但是现在你有了一个坚实的骨架来构建你自己的命令行解析 C 程序。

/* main.c - the complete listing */

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <libgen.h>
#include <errno.h>
#include <string.h>
#include <getopt.h>

#define OPTSTR "vi:o:f:h"
#define USAGE_FMT  "%s [-v] [-f hexflag] [-i inputfile] [-o outputfile] [-h]"
#define ERR_FOPEN_INPUT  "fopen(input, r)"
#define ERR_FOPEN_OUTPUT "fopen(output, w)"
#define ERR_DO_THE_NEEDFUL "do_the_needful blew up"
#define DEFAULT_PROGNAME "george"

extern int errno;
extern char *optarg;
extern int opterr, optind;

typedef struct {
  int           verbose;
  uint32_t      flags;
  FILE         *input;
  FILE         *output;
} options_t;

int dumb_global_variable = -11;

void usage(char *progname, int opt);
int  do_the_needful(options_t *options);

int main(int argc, char *argv[]) {
    int opt;
    options_t options = { 0, 0x0, stdin, stdout };

    opterr = 0;

    while ((opt = getopt(argc, argv, OPTSTR)) != EOF) 
       switch(opt) {
           case 'i':
              if (!(options.input = fopen(optarg, "r")) ){
                 perror(ERR_FOPEN_INPUT);
                 exit(EXIT_FAILURE);
                 /* NOTREACHED */
              }
              break;

           case 'o':
              if (!(options.output = fopen(optarg, "w")) ){
                 perror(ERR_FOPEN_OUTPUT);
                 exit(EXIT_FAILURE);
                 /* NOTREACHED */
              }    
              break;
              
           case 'f':
              options.flags = (uint32_t )strtoul(optarg, NULL, 16);
              break;

           case 'v':
              options.verbose += 1;
              break;

           case 'h':
           default:
              usage(basename(argv[0]), opt);
              /* NOTREACHED */
              break;
       }

    if (do_the_needful(&options) != EXIT_SUCCESS) {
       perror(ERR_DO_THE_NEEDFUL);
       exit(EXIT_FAILURE);
       /* NOTREACHED */
    }

    return EXIT_SUCCESS;
}

void usage(char *progname, int opt) {
   fprintf(stderr, USAGE_FMT, progname?progname:DEFAULT_PROGNAME);
   exit(EXIT_FAILURE);
   /* NOTREACHED */
}

int do_the_needful(options_t *options) {

   if (!options) {
     errno = EINVAL;
     return EXIT_FAILURE;
   }

   if (!options->input || !options->output) {
     errno = ENOENT;
     return EXIT_FAILURE;
   }

   /* XXX do needful stuff */

   return EXIT_SUCCESS;
}

现在,你已经准备好编写更易于维护的 C 语言。如果你有任何问题或反馈,请在评论中分享。


via: https://opensource.com/article/19/5/how-write-good-c-main-function

作者:Erik O'Shaughnessy 选题:lujun9972 译者:MjSeven 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出

我的上一篇博文《与 C 语言长别离》引来了我的老朋友,一位 C++ 专家的评论。在评论里,他推荐把 C++ 作为 C 的替代品。这是不可能发生的,如果 C++ 代替 C 是趋势的话,那么 Go 和 Rust 也就不会出现了。

但是我不能只给我的读者一个光秃秃的看法(LCTT 译注:此处是双关语)。所以,在这篇文章中,我来讲述一下为什么我不再碰 C++ 的故事。这是关于计算机语言设计经济学专题文章的起始点。这篇文章会讨论为什么一些真心不好的决策会被做出来,然后进入语言的基础设计之中,以及我们该如何修正这些问题。

在这篇文章中,我会一点一点的指出人们(当然也包括我)自从 20 世纪 80 年代以来就存在的关于未来的编程语言的预见失误。直到最近,我们才找到了证明我们错了的证据。

我记得我第一次学习 C++ 是因为我需要使用 GNU eqn 输出 MathXML,而 eqn 是使用 C++ 写的。那个项目不错。在那之后,21 世纪初,我在 韦诺之战 Battle For Wesnoth 那边当了多年的资深开发人生,并且与 C++ 相处甚欢。

在那之后啊,有一天我们发现一个不小心被我们授予提交权限的人已经把游戏的 AI 核心搞崩掉了。显然,在团队中只有我是不那么害怕查看代码的。最终,我把一切都恢复正常了 —— 我折腾了整整两周。再那之后,我就发誓我再也不靠近 C++ 了。

在那次经历过后,我发现这个语言的问题就是它在尝试使得本来就复杂的东西更加复杂,来粗陋补上因为基础概念的缺失造成的漏洞。对于裸指针这样东西,它说“别这样做”,这没有问题。对于小规模的个人项目(比如我的魔改版 eqn),遵守这些规定没有问题。

但是对于大型项目,或者开发者水平参差不齐的多人项目(这是我经常要处理的情况)就不能这样。随着时间的推移以及代码行数的增加,有的人就会捅篓子。当别人指出有 BUG 时,因为诸如 STL 之类的东西给你增加了一层复杂度,你处理这种问题所需要的精力就比处理同等规模的 C 语言的问题就要难上很多。我在韦诺之战时,我就知道了,处理这种问题真的相当棘手。

我给 Stell Heller(我的老朋友,C++ 的支持者)写代码时不会发生的问题在我与非 Heller 们合作时就被放大了,我和他们合作的结局可能就是我得给他们擦屁股。所以我就不用 C++ ,我觉得不值得为了其花时间。 C 是有缺陷的,但是 C 有 C++ 没有的优点 —— 如果你能在脑内模拟出硬件,那么你就能很简单的看出程序是怎么运行的。如果 C++ 真的能解决 C 的问题(也就是说,C++ 是类型安全以及内存安全的),那么失去其透明性也是值得的。但是,C++ 并没有这样。

我们判断 C++ 做的还不够的方法之一是想象一个 C++ 已经搞得不错的世界。在那个世界里,老旧的 C 语言项目会被迁移到 C++ 上来。主流的操作系统内核会是 C++ 写就,而现存的内核实现,比如 Linux 会渐渐升级成那样。在现实世界,这些都没有发生。C++ 不仅没有打消语言设计者设想像 D、Go 以及 Rust 那样的新语言的想法,它甚至都没有取代它的前辈。不改变 C++ 的核心思想,它就没有未来,也因此,C++ 的 抽象泄露 leaky abstraction 也不会消失。

既然我刚刚提到了 D 语言,那我就说说为什么我不把 D 视为一个够格的 C 语言竞争者的原因吧。尽管它比 Rust 早出现了八年(和 Rust 相比是九年)Walter Bright 早在那时就有了构建那样一个语言的想法。但是在 2001 年,以 Python 和 Perl 为首的语言的出现已经确定了,专有语言能和开源语言抗衡的时代已经过去。官方 D 语言库/运行时和 Tangle 的无谓纷争也打击了其发展。它从未修正这些错误。

然后就是 Go 语言(我本来想说“以及 Rust”。但是如前文所述,我认为 Rust 还需要几年时间才能有竞争力)。它的确是类型安全以及内存安全的(好吧,是在大多数时候是这样,但是如果你要使用接口的话就不是如此了,但是自找麻烦可不是正常人的做法)。我的一位好友,Mark Atwood,曾指出过 Go 语言是脾气暴躁的老头子因为愤怒而创造出的语言,主要是 C 语言的作者之一(Ken Thompson) 因为 C++ 的混乱臃肿造成的愤怒,我深以为然。

我能理解 Ken 恼火的原因。这几十年来我就一直认为 C++ 搞错了需要解决的问题。C 语言的后继者有两条路可走。其一就是 C++ 那样,接受 C 的抽象泄漏、裸指针等等,以保证兼容性。然后以此为基础,构建一个最先进的语言。还有一条道路,就是从根源上解决问题 —— 修正 C语言的抽象泄露。这一来就会破环其兼容性,但是也会杜绝 C/C++ 现有的问题。

对于第二条道路,第一次严谨的尝试就是 1995 年出现的 Java。Java 搞得不错,但是在语言解释器上构建这门语言使其不适合系统编程。这就在系统编程那留下一个巨大的洞,在 Go 以及 Rust 出现之前的 15 年里,都没有语言来填补这个空白。这也就是我的 GPSD 和 NTPsec 等软件在 2017 年仍然主要用 C 写成的原因,尽管 C 的问题也很多。

在许多方面这都是很糟糕的情况。尽管由于缺少足够多样化的选择,我们很难认识到 C/C++ 做的不够好的地方。我们都认为在软件里面出现缺陷以及基于安全方面考虑的妥协是理所当然的,而不是想想这其中多少是真的由于语言的设计问题导致的,就像缓存区溢出漏洞一样。

所以,为什么我们花了这么长时间才开始解决这个问题?从 C 1972 年面世到 Go 2009 年出现,这其中隔了 37 年;Rust 也是在其仅仅一年之前出现。我想根本原因还是经济。

从最早的计算机语言开始,人们就已经知道,每种语言的设计都体现了程序员时间与机器资源的相对价值的权衡。在机器这端,就是汇编语言,以及之后的 C 语言,这些语言以牺牲开发人员的时间为代价来提高性能。 另一方面,像 Lisp 和(之后的)Python 这样的语言则试图自动处理尽可能多的细节,但这是以牺牲机器性能为代价的。

广义地说,这两端的语言的最重要的区别就是有没有自动内存管理。这与经验一致,内存管理缺陷是以机器为中心的语言中最常见的一类缺陷,程序员需要手动管理资源。

当相对价值断言与软件开发在某个特定领域的实际成本动因相匹配时,这个语言就是在经济上可行的。语言设计者通过设计一个适合处理现在或者不远的将来出现的情况的语言,而不是使用现有的语言来解决他们遇到的问题。

随着时间的推移,时兴的编程语言已经渐渐从需要手动管理内存的语言变为带有自动内存管理以及垃圾回收(GC)机制的语言。这种变化对应了摩尔定律导致的计算机硬件成本的降低,使得程序员的时间与之前相比更加的宝贵。但是,除了程序员的时间以及机器效率的变化之外,至少还有两个维度与这种变化相关。

其一就是距离底层硬件的距离。底层软件(内核与服务代码)的低效率会被成倍地扩大。因此我们可以发现,以机器为中心的语言向底层推进,而以程序员为中心的语言向着高级发展。因为大多数情况下面向用户的语言仅仅需要以人类的反应速度(0.1 秒)做出回应即可。

另一个维度就是项目的规模。由于程序员抽象发生的问题的漏洞以及自身的疏忽,任何语言都会有可预期的每千行代码的出错率。这个比率在以机器为中心的语言上很高,而在程序员为中心的带有 GC 的语言里就大大降低。随着项目规模的增大,带有 GC 的语言作为一个防止出错率不堪入目的策略就显得愈发重要起来。

当我们使用这三种维度来看当今的编程语言的形势 —— C 语言在底层,蓬勃发展的带有 GC 的语言在上层,我们会发现这基本上很合理。但是还有一些看似不合理的是 —— C 语言的应用不合理地广泛。

我为什么这么说?想想那些经典的 Unix 命令行工具吧。那些小程序通常都可以使用带有完整的 POSIX 支持的脚本语言快速实现出来。重新编码那些程序将使得它们调试、维护和拓展起来都会更加简单。

但是为什么还是使用 C (或者某些像 eqn 的项目,使用 C++)?因为有转换成本。就算是把相当小、相当简单的程序使用新的语言重写并且确认你已经忠实地保留了所有非错误行为都是相当困难的。笼统地说,在任何一个领域的应用编程或者系统编程在一种语言的权衡过时之后,仍然坚持使用它。

这就是我和其他预测者犯的大错。 我们认为,降低机器资源成本(增加程序员时间的相对成本)本身就足以取代 C 语言(以及没有 GC 的语言)。 在这个过程中,我们有一部分或者甚至一大部分都是错误的 —— 自 20 世纪 90 年代初以来,脚本语言、Java 以及像 Node.js 这样的东西的兴起显然都是这样兴起的。

但是,竞争系统编程语言的新浪潮并非如此。 Rust 和 Go 都明确地回应了增加项目规模 这一需求。 脚本语言是先是作为编写小程序的有效途径,并逐渐扩大规模,而 Rust 和 Go 从一开始就定位为减少大型项目中的缺陷率。 比如 Google 的搜索服务和 Facebook 的实时聊天复用。

我认为这就是对 “为什么不再早点儿” 这个问题的回答。Rust 和 Go 实际上并不算晚,它们相对迅速地回应了一个直到最近才被发现低估的成本动因问题。

好,说了这么多理论上的问题。按照这些理论我们能预言什么?它告诉我们在 C 之后会出现什么?

推动 GC 语言发展的趋势还没有扭转,也不要期待其扭转。这是大势所趋。因此:最终我们拥有具有足够低延迟的 GC 技术,可用于内核和底层固件,这些技术将以语言实现方式被提供。 这些才是真正结束 C 长期统治的语言应有的特性。

我们能从 Go 语言开发团队的工作文件中发现端倪,他们正朝着这个方向前进 —— 可参见关于并发 GC 的学术研究 —— 从未停止研究。 如果 Go 语言自己没有选择这么做,其他的语言设计师也会这样。 但我认为他们会这么做 —— 谷歌推动他们的项目的能力是显而易见的(我们从 “Android 的发展”就能看出来)。

在我们拥有那么理想的 GC 之前,我把能替换 C 语言的赌注押在 Go 语言上。因为其 GC 的开销是可以接受的 —— 也就是说不只是应用,甚至是大部分内核外的服务都可以使用。原因很简单: C 的出错率无药可医,转化成本还很高。

上周我尝试将 C 语言项目转化到 Go 语言上,我发现了两件事。其一就是这活很简单, C 的语言和 Go 对应的很好。还有就是写出的代码相当简单。由于 GC 的存在以及把集合视为首要的数据结构,人们会预期代码减少,但是我意识到我写的代码比我最初期望的减少的更多,比例约为 2:1 —— 和 C 转 Python 类似。

抱歉呐,Rust 粉们。你们在内核以及底层固件上有着美好的未来,但是你们在别的 C 领域被 Go 压的很惨。没有 GC ,再加上难以从 C 语言转化过来,还有就是 API 的标准部分还是不够完善。(我的 select(2) 又哪去了啊?)。

对你们来说,唯一的安慰就是,C++ 粉比你们更糟糕 —— 如果这算是安慰的话。至少 Rust 还可以在 Go 顾及不到的 C 领域内大展宏图。C++ 可不能。

本站按:本文由著名开源领袖 ESR 撰写,了解 ESR 事迹的同学知道他拒绝去大公司荣养,而仍然主要负责一些互联网基础性项目的开发维护(如 NTPsec),所以,他在创造者赞助网站 Patreon 上有一份生活赞助计划,大家可以考虑献出一些微薄之力支持他,每个月 $20 也不过你一餐饭而已。

via: http://esr.ibiblio.org/?p=7724

作者:Eric Raymond 译者:name1e5s 校对:wxy

本文由 LCTT 原创编译,Linux中国 荣誉推出