C程序设计新思维(第2版)

978-7-115-46095-0
作者: [美]Ben Klemens 克莱蒙
译者: 赵岩
编辑: 胡俊英
分类: C语言

图书目录:

详情

本书汇集编写高效、实用的C程序所需的工具和技巧,倡导读者重新认识并重视C语言,以更好地使用C语言。书中继承了上一版中关于如何实用shell、makefiles、漂亮的文本编辑器、调试器、内存管理等工具和技术,并附加了关于并发编程、虚拟表等特性的介绍,更好地帮助C语言使用者进行拾遗补漏。另外,书中提供了链表结构和XML解析器的现成的库,便于读者使用,同时对如何编写易读代码和函数接口进行了讲解。

图书摘要

版权信息

书名:C程序设计新思维(第2版)

ISBN:978-7-115-46095-0

本书由人民邮电出版社发行数字版。版权所有,侵权必究。

您购买的人民邮电出版社电子书仅供您个人使用,未经授权,不得以任何方式复制和传播本书内容。

我们愿意相信读者具有这样的良知和觉悟,与我们共同保护知识产权。

如果购买者有侵权行为,我们可能对该用户实施包括但不限于关闭该帐号等维权措施,并可能追究法律责任。


• 著    [美] 本·克莱蒙(Ben Klemens)

  译    赵 岩

  责任编辑 胡俊英

• 人民邮电出版社出版发行  北京市丰台区成寿寺路11号

  邮编 100164  电子邮件 315@ptpress.com.cn

  网址 http://www.ptpress.com.cn

• 读者服务热线:(010)81055410

  反盗版热线:(010)81055315


Copyright© 2015 by O’Reilly Media, Inc.

Simplified Chinese Edition, jointly published by O’Reilly Media, Inc. and Posts & Telecom Press, 2017. Authorized translation of the English edition, 2015 O’Reilly Media, Inc., the owner of all rights to publish and sell the same.

All rights reserved including the rights of reproduction in whole or in part in any form.

本书中文简体版由O’Reilly Media, Inc.授权人民邮电出版社出版。未经出版者书面许可,对本书的任何部分不得以任何方式复制或抄袭。

版权所有,侵权必究。


C语言已经有几十年的历史了。经过长时间的发展和普及,C语言的应用场景有了很大的变化,一些旧观念应该被淡化或者不再被推荐。

本书展现了传统C语言教科书所不具有的最新的相关技术。全书分为开发环境和语言两个部分,从编译、调试、测试、打包、版本控制等角度,以及指针、语法、文本、结构、面向对象编程、函数库等方面,对C程序设计的核心知识进行查缺补漏和反思。本书鼓励读者放弃那些对大型机才有意义的旧习惯,拿起新的工具来使用这门与时俱进的简洁语言。

本书适合有一定基础的C程序员和C语言学习者阅读,也适合想要深入理解C语言特性的读者参考。


最近非常有幸地接受了人民邮电出版社的邀请来翻译本书,这是一本非常经典的C语言著作,目前已经是第2版了。计算机图书出了很多年,大家对其自有判断,最简单的办法就是根据书名,20世纪90年代末期出版过几本比较经典的计算机图书,书名为:《**入门到精通》《21天学会**》等。不过很快大家就开始借用这种书名,最后搞得有些良莠不齐。更有甚者,最近出现了好多类似《**从入门到放弃》《**从入门到入院》的图书,彻底颠覆了以前程序员心目中这么神圣的书名。好在还有O’Reilly出版社的动物丛书,目前还都是品质和经典的象征。这套书经典到有时圈内的人们都忘了书名,只记得动物的名字,例如Perl语言的 “骆驼书”以及Git的“蝙蝠书”等。

当完成最后一个字的录入,作为本书的译者,我认为应该系统地给这本书做个总结了。首先,这是一本经典的C语言图书,亚马逊上有50多条评论,评分达到4分。这个傲人的成绩主要来自于本书的两个优点:第一个优点就是系统性和大局观。C语言最开始作为开发UNIX操作系统的工具,它和UNIX操作系统有着不可分割的关系。无论UNIX派生的POSIX标准以及GNU运动,C语言都是其核心的开发语言和工具。所以如果想真正发挥C语言的威力,那必须要把这个语言放到一个更大的生态环境中去。本书通过对POSIX标准库、GNU编译器、Shell 脚本、Make、Git、文档和测试,以及各种常用的函数库等一系列内容的介绍,建立了一个高效整合的C语言开发环境。C语言作为这个环境的核心开发语言,通过各种开发工具和函数库的配合,将开发环境的优点淋漓尽致地发挥出来,从而能显著地提高你的开发效率。

第二个优点就是新思维和反规则。作为物理学的爱好者,我用物理来做一个类比 。牛顿创立了经典力学和万有引力。正当我们认为物理学已经完胜的时候,爱因斯坦在一边幽幽地说的那句“光会拐弯”。在爱因斯坦的结论在观测日全食方面得到验证以后,这位天才自信心爆棚并宣称“一切都是可以通过计算来确定的”。这个时候研究量子力学的波尔却传给了他一个纸条说:“上帝掷骰子!”也许,我是说也许,我们一直都相信的规则或者答案过于片面。 就像有一天我6岁的女儿小米粒问我:“我们人类是从哪里来的啊?”我说:“有人说是猴子变的,有人说是神创造的,你信哪种说法都可以。你告诉爸爸,你信哪种啊?”我的女儿想都没想就回答到:“是神把猴子变成人的!”

我们人类总是有一种倾向,一旦形成了自己的某些规则,那么就会自然地排斥和否定另外的反规则。而本书的难能可贵之处就在于,它不仅提出了C语言的一些反规则,而且通过一些例子证明这些反规则是合理的。例如,我们可以建造高效和准确的宏,可以不需要对内存的使用斤斤计较,哪怕有点内存泄漏,我们可以用goto,但是对switch却完全可以放弃,等等。现代物理有一个反物质学说,当物质和反物质相遇时,二者会立即湮没,并爆发出巨大的能量。这里我借用一下:当你熟悉了规则,同时也理解了反规则,这个时候你的心中就没有了规则。剩下的就是巨大的能力。此时小李飞刀已经不带刀,此时无招已经胜有招。

俗话说:“优点不说没不了,缺点不说不得了。”下面说说本书的缺点,那就是:对每一部分的内容并没有详细地介绍!所以你不要指望着阅读完本书就能熟练地使用shell脚本,写出复杂的makefile并通过Git高效地与人协作。坦白地说,这也并不算是缺点。本书的目的就是告诉你,当你想做什么事情的时候,有哪些工具你可以用,而这些工具的最常见的用法又是什么。当你发现这些工具并不够用的时候,你可以去找专门的介绍相关内容的书。这个时候你会发现完整介绍某些工具的书,它的厚度足以挡住狙击步枪射出的子弹。同时,POSIX体系内的东西,有臭名昭著的学习曲线。你要是不服就下载一个VIM编辑器试试看!

从安装VIM到现在,我一直在使用它,但我一直搞不懂如何退出VIM。大家都说C语言难,客观地来讲,这个锅不能让C语言和本书来背。但是,你想要“会当凌绝顶,一览众山小”,那你的学习曲线必须要很陡才行!

另外,本书的第3.3节“用Autotools打包代码”,我个人认为这个技术有些过时。目前流行的build system 是CMake系统,它在移植性和易用性上都要比Autotoolson工具要好。所以CMake是新开发系统的首选build system,也是未来的潮流。

最后说说本书面向的读者对象。首先,本书并不是教材,虽然书的后面有一个简短的介绍C语言的附录,我没有骗你,它确实很简短。请注意我的用词,我只是说“简短”,并没有说“简单”。所以,如果你是一个C语言的初学者或者是零基础读者,那么本书并不适合你。

本书面向的读者对象是有一定C语言基础的高年级学生,或者是一些使用C语言作为主要开发语言的工程师。对于大学高年级的学生,它们缺乏的是一种对大的编程环境的认识。而对于使用C语言的从业人员,本书会让你对C语言有不一样的认识。它对你多年使用C语言形成的习惯和风格提出了挑战,让你有一种“原来C语言也可以这么用”的赞叹!然后让这些反规则去湮没你心中存在多年的规则,从而爆发出巨大的能量!

最后再提醒一句,本书真的不适合初学者!否则,你看不懂会说我翻译得不好。我已经把E=MC2翻译成了“能量等于质量乘以光速的平方”,如果你还看不懂,就别怪译者了。

举个简单的例子,书中8.2节最后一段是:

这意味着如果两个文件中的两个变量有共同的名字,但是你想它们应该彼此独立的。这个时候如果你忘记static关键字,编译器可以把有外部链接变量的链接成一个变量。这种细微的bug非常容易发生,所以对于那些有内部链接的变量不要忘了使用static。

如果你不了解tentative definition(临时定义)这一C语言中特有的现象,就不能充分理解“这种细微的bug”究竟是什么。所以为了让读者能更容易地阅读本书,我为本书做了一个网站: http://zhaoyan.website/xinzhi/c21/book.php,里面有我对每一章的观点,同时推荐了一些帮助读者理解的补充内容等。例如,对于上面的tentative definition的问题,我给出了非常好的介绍,同时还有我自己编写的一段程序对这一概念进行了说明。对于本书的每部分内容过于简短和艰深这一缺点,我也做了一些有益的补充和修正。

最后我想说的是:这本书真的很好!这本书真的很难!你有勇气挑战一下吗?

——赵岩

2017年5月


 

虽然C仅有为数不多的关键词,并且没有那么多细节修饰,但是它很棒[1]!你可以用C来做任何事情。它就像一把吉他上的C、G和D弦,你很快就可以掌握其基本原理,然后就得用你的余生来提高。不理解它的人害怕它的威力,并认为它粗糙得不够安全。虽然没有企业和组织花钱去推广它[2],但是实际上它在所有的编程语言排名中一直被认为是最流行的语言。

这门语言已经有几十年的历史了,可以说已经进入了中年。创造它的是少数对抗管理阶层并遵从完美的punk rock精神的人;但那是20世纪70年代的事情了,现在这门语言已经历尽沧桑,并且成为主流的语言。

当punk rock变成主流的时候人们会怎样?在其从20世纪70年代出现后的几十年里,punk rock已经从边缘走向中心:The Clash、The Offspring、Green Day和The Strokes等乐队已经在全世界卖出了几百万张唱片(这还只是一小部分),我也在家附近的超市里听过被称为grunge的一些精简乐器版本的punk rock分支。Sleater-Kinney乐队的前主唱还经常在自己那个很受欢迎的喜剧节目中讽刺punk rocker音乐人[3]。对这种持续的进化,一种反应是划一条界限,将原来的风格称为punk rock,而将其余的东西称为面向大众的粗浅的punk。传统主义者还是可以播放20世纪70年代的唱片,但如果唱片的音轨磨损了,他们可以购买数码版本,就像他们为自己的小孩购买Ramones牌的连帽衫一样。

外行是不明白的。有些人听到punk这个词时脑海里就勾画出20世纪70年代特定的景象,经常的历史错觉就是那个时代的孩子们真的在做什么不同的事情。喜欢欣赏1973年Iggy Pop 的黑胶唱片的传统主义者一直是那么兴趣盎然,但是他们有意无意地加强了那种punk rock已经停滞不前的刻板印象。

回到C的世界里,这里既有挥舞着ANSI’89标准大旗的传统主义者,也有那些拥抱变化,甚至都没有意识到如果回到20世纪90年代,他们写的代码都不可能被成功编译与运行的人。外行人是不会知道个中缘由的。他们看到从20世纪80年代起至今还在印刷的书籍和20世纪90年代起至今还存于网上的教程,他们听到的都是坚持当年的软件编写方式的、死硬的传统主义者的言论,他们甚至都不知道语言本身和别的用户都在一直进化。非常可惜,他们错过了一些好东西。

这是一本打破传统并保持C语言punk精神的书。我对将本书的代码与1978年Kernighan和Ritchie出版的书[4]中的C标准进行对比毫无兴趣。既然连我的电话都有512MB内存,为什么还在我的书里花费章节讲述如何为可执行文件减少几千字节呢?我正在一个廉价的红色上网本上写这本书,而它却可以每秒运行3 200 000 000条指令,那为什么我还要操心8位和16位所带来的操作的差异呢?我们更应该关注如何做到快速编写代码并且让我们的合作者们更容易看懂。毕竟我们是在使用C语言,所以我们那些易读但是并没有被完美优化的代码运行起来还是会比很多烦琐的语言明显更快。

问题:这本书与其他书有什么不同?

答案:有些书写得好,有些书写得有趣,但是大部分C语言的教科书都非常相像(我曾经读过很多这样的教科书,包括[Griffiths,2012]、[Kernighan,1978]、[Kernighan,1988]、[Kochan,2004]、[Oualline,1997]、[Perry,1994]、[Prata,2004]和[Ullman,2004]。多数教材都是在C99标准发布并简化了很多用法之后写成的,你可以看到现在出版的这些教材的第版仅仅在一些标注上做了一点说明,而不是认真反思了如何使用这门语言。它们都提到你可以拥有一些库来编写你自己的代码,但是书籍完成时,缺少了保障库的可靠性和可移植性的安装与开发环境。那些教科书现在仍然有效并且具有自己的价值,但是现代的C代码已经看起来和那些教科书里面的不太一样了。

这本书与那些教科书的不同之处,在于对这门语言及其开发环境进行了拾遗补漏。书中讲解的方式是:直接使用提供了链表结构和XML解析器的现成的库,而不是把这些从头再写一次。这本书也体现了如何编写易读代码和用户友好的函数接口。

问题:这本书的目标读者是谁?我需要是一个编程大师吗?

答案:你必须有某种语言的编程经验,或许是Java,或者是类似于Perl的某种脚本语言。这样我就没有必要再向你讲为什么你不应该写一个很长的没有任何子函数的函数了。

本书的内容假设你已经有了通过写C代码而获得的C语言的基本知识。附录A提供了一个简短的有关C语言基础的教程,那些以前写Python和Ruby等脚本语言的读者可以阅读它。

请允许我介绍我写的另一本关于统计和科学计算的教科书Modeling with Data [Klemens,2008]。那本书不仅提供了很多关于如何处理数值和统计模型的内容,它还可以用作一本独立的C语言的教材,并且我认为那本书还避免了很多早期教材的缺点。

问题:我是个编写应用软件的程序员,不是一个研究操作系统内核的人。为什么我应该用C而不是像Python这类可以快速编程的脚本语言呢?

答案:如果你是一个应用软件程序员的话,这本书就是为你准备的。我知道人们经常认定C是一种系统语言,这让我觉得真是缺少了点punk的反叛精神——他们是谁啊?要他们告诉我们要用什么语言?

像“我们的语言几乎和C一样快,但更容易编写”这样的言论很多,简直成了陈词滥调。好吧,C显然是和C一样快,并且这本书的目的是告诉你C也像以前的教科书所暗示的那样容易使用。你没必要使用malloc,也没必要像20世纪90年代的系统程序员那样深深卷入内存管理,我们已经有处理字符串的手段,甚至核心语法也进化到了支持更易读的代码的境界。

我当初正式学习C语言是为了加速一个用脚本语言R编写的仿真程序。和众多的脚本语言一样,R具有C接口并且鼓励用户在宿主语言[6]太慢的时候使用。最终我的程序里有太多的从R语言到C语言的调用,最后我索性放弃了宿主语言。随后发生的事情你已经知道,就是我在写这本关于现代C语言技术的书。

问题:如果原本使用脚本语言的应用软件程序员能喜欢这本书当然好,但我是一名内核黑客。我在五年级的时候就自学了C语言,有时做梦都在正确编译。那这本书还有什么新鲜的吗?

答案:C语言在过去的几年里真的进化了很多。就像我下面要讨论的那样,各个编译器对新功能的支持的时间也不一样,感谢自从ANSI标准发布后,又发布了两个新的C语言标准。也许你应该读一下第10章,找找有什么能叫你感到惊讶的。本书的一部分,如讲解指针的经常被人错误理解的一些概念(第6章),也覆盖了自从1980年以后变化的内容。

并且,开发环境也升级了。很多我提到的工具,如make和debugger,你已经很熟悉了,但是我发现别人可能还不知道。Autotools已经改变了代码发布的方式,Git也改变了我们合作编程的方式 。

问题:我实在忍不住要问,为什么这本书中有差不多三分之一的篇幅都没有C代码?

答案:这本书本意就是讲述一些其他C语言教科书没有讲到的内容,排在首位的就是工具和环境。如果你没有使用调试器(独立的或者集成在你的IDE中),你就是在自讨苦吃。教科书经常把debugger放到最后面,有的根本就不提。与他人共享代码也需要另外的工具集,如Autotools和Git。代码并不存在于真空里,其他的教科书都在假设读者只需要了解C语言语法就会有生产力了,那就让我写一点不同于这些教科书的内容吧。

问题:有太多的用于C开发的工具,你在本书中如何取舍呢?

相比大部分语言,C语言社区有更高的内部互通性。GNU提供了太多的C语言扩展,还有那些只工作在Windows平台的IDE,只存在于LLVM中的编译器扩展等。这就是为什么过去的教科书不去讲解工具的原因。但是现在,有些系统应用得非常普遍。很多工具来自于GNU;LLVM和相关工具虽然不是主流,但是也打下了坚实的基础。不管你用什么,Windows、Linux或者你从你的云计算提供商那里取得的任何东西,这里我介绍的工具全部是容易并且可以快速地安装的。我提到了一些平台相关的工具,但是仅限那么几例。

我并没有介绍集成开发环境(IDEs),因为很少的集成开发环境能跨平台工作(尝试建立一个Amazon Elastic Computer Cloud实例,然后在上面安装Eclipse和它的C插件),而且IDEs的选择大部分被个人的喜好所左右。IDE有一个项目建造系统,它通常与别的IDE的项目建造系统不兼容。IDE的项目文件在你分发到外面的时候就不能用了。除非你硬性规定所有的人(在教室、特定办公室或者某些计算平台上)都必须使用相同的IDE。

问题:我能上网,一两秒的功夫就能找到命令和语法的细节。那么说真的,为什么我还要读这本书?

答案:的确。在Linux或Mac机器上你只要用一个带有 man operator 的命令行就能查到运算符优先级表,那么我为什么还要把它放在这本书里?

我可以和你上同样的Internet,我甚至花了很多的时间阅读网上的内容。所以我有了一个之前没谈到的、准备现在讲的好主意:当介绍一个新工具的时候,如gprof或者GDB,我给你那些你必须知道的方向,然后你可以去自己习惯的搜索引擎中查找相关问题。这也是其他教科书所没有的(这样的内容还不少呢)。

除非特别地说明,本书的内容遵从ISO C99和C11标准。为了使你明白这意味着什么,下面给你介绍一点C语言的历史背景,让我们回顾一下主要的C标准(而忽略一些小的改版和订正)。

K&R(1978前后)

Dennis Ritchie、Ken Thompson以及一些其他的贡献者发明了C语言,并编写了UNIX操作系统。Brian Kernighan和Dennis Ritchie最终在他们的书中写下了第一版关于这个语言的描述,同时这也是C语言的第一个事实上的标准[Kernighan,1978]。

ANSI C89

后来Bell实验室向美国国家标准协会(ANSI)交出了这个语言的管理权。1989年,ANSI出版了他们的标准,并在K&R的基础上做出了一定的提高。K&R的书籍的第2版包含了这个语言的完整规格,也就是说在几万名程序员的桌子上都有这个标准的印刷版[Kernighan,1988]。1990年,ANSI标准被ISO基本接受,没有做重大的改变,但是人们似乎更喜欢用ANSI,89这个词来称呼这个标准(或者用来做很棒的T恤衫标语)。

10年过去了。C成为了主流,考虑到几乎所有的PC、每台Internet服务器的基础代码或多或少都是用C编写的,C语言已经成为了主流,这已经是人类的努力可以达到的最大的极限了。

在此期间,C++分离出来并大获成功(虽然也不是那么大)。C++是C身上发生的最好的事情了。当所有其他的语言都在试图添加一些额外的语法以跟随面向对象的潮流,或者跟随其作者脑袋里的什么新花招的时候,C就是恪守标准。需要稳定和可移植性的人使用C,需要越来越多的人把大量的金钱投入到了C++语言上,这样的结果就是:你好我好,大家过年,每个人都高兴。

ISO C99

10年之后,C标准经历了一次主要的改版。为数值和科学计算增添了一些附加功能,如复数的标准数据类型以及泛型(type-generic)函数。一些从C++中产生的便利措施被采纳,包括单行注释(实际上起源于C的前期语言,BCPL),以及可以在for循环的开头声明变量。因为一些新添加的关于如何声明和初始化的语法,以及一些表示法上的便利,使用泛型函数变得更加容易。出于安全考量以及并不是所有人都说英语原因,一些特性也被做了调整。

当你想着单单C89的影响其实并不大,以及全球大范围地运行着C代码时,你就理解了ISO做出的任何改变都是会被广泛批评的——甚至你不做任何改变,别人还想找茬骂你呢[7]。的确,这个标准是有争论的。有两种常用的方式来表达一个复数(直角坐标和极坐标)——那么ISO会采用哪一个?既然所有的好代码都没采用变长的宏输入机制来编写,为什么我们还需要这个机制?换句话说,纯洁主义者批评ISO是屈服于外界的压力才给C语言增加了更多的特性。

当我写这本书的时候,多数的编译器在支持C99的同时都增加或减少了一些特性;如long double 类型看起来就引发了很多问题。然而,这里还是有一个明显的特例:Microsoft至今拒绝在其Visual Studio C++编译器中添加C99支持。在本书第6页“1.2 在Windows下编译C”一节中讲述了几种在Windows环境中编译C的方法,所以不能使用Visual Studio最多也就是有点不方便,这好比一个行业奠基人告诉我们不能使用ISO标准的C,这样标准就更有punk rock风格了。

C11

觉察到了对所谓背叛行业趋势的批评后,ISO组织在第三版的标准中做出了为数不多的几个重大改变。我们有了可以编写泛型函数的方法,并且对安全性和非英语支持做出了进一步的改进。

C11标准在2011年12月发布后不久,编译器的开发者以惊人的速度完成了对新标准的支持。目前一些主流编译器已经声称做到了几乎全部的标准兼容。但是标准定义了编译器的行为,也定义了标准库和库支持。如线程和原子性等,有些系统上实现了,有些系统上还在开发。

事物的规律就是这样,伴随着C语言的进化,这门语言同时也和UNIX操作系统协同发展,并且你将会从本书中看到,这种相互协同的发展对日常工作是有意义的。如果某件事情在UNIX命令行中很容易利用,那么很有可能是因为这件事情在C中也很容易实现;某些UNIX工具之所以存在,也是为了帮助C代码的编写。

UNIX

C和UNIX都是在20世纪70年代由Bell实验室设计的。在20世纪的多数时间里,Bell一直面临垄断调查,并且Bell有一项与美国联邦政府达成的协议,就是Bell将不会把自身的研究扩张到软件领域。所以UNIX被免费发放给学者们去研究和重建。UNIX这个名字是一个商标,原本由Bell实验室持有,但随后就像一张棒球卡一样在数家公司之间转卖。

随着其代码被不断研究、重新实现,并被黑客们以不同的方式改进,UNIX的变体迅速增加。因此带来了一点不兼容的问题,即程序或脚本变得不可移植,于是标准化工作的迫切性很快就变得显而易见。

POSIX

这个标准最早由电气和电子工程师协会(IEEE)在1988年建立,提供了一个类UNIX操作系统的公共基础。它定义的规格中包括shell脚本如何工作,像ls、grep之类的命令行应该如何工作,以及C程序员希望能用到的一些C库等。举个例子,命令行用户用来串行运行命令的管道机制被详细地定义了,这意味着C语言的popen(打开管道)函数是POSIX标准,而不是ISO C标准。POSIX标准已经被改版很多次了;本书编写的时候是POSIX:2008标准,这也是当我谈到POSIX标准的时候所指代的。POSIX标准的操作系统必须通过提供C99命令来提供C编译器。

这本书用到POSIX标准的时候,我会告诉大家。

除了来自Microsoft的一系列操作系统产品,当前几乎所有你可以列举出的操作系统都是建立在POSIX兼容的基础上:Linux、Mac OS X、iOS、WebOS、Solaris、BSD——甚至Windows Servers也提供POSIX子系统。对于那些例外的操作系统,1.2“在Windows下编译C程序”一节将告诉你如何安装POSIX子系统。

最后,有两个POSIX的实现版本因为有较高的流行度和影响力,值得我们注意。

BSD

在Bell实验室发布UNIX给学者们剖析之后,加州大学伯克利分校的一群好人做了很多明显的改进,最终重写了整个UNIX基础代码,产生了伯克利软件发行版(Berkeley Software Distribution,BSD)。如果你正在使用一台Apple公司生产的电脑,你实际上在使用一个带有迷人图形界面的BSD。BSD在几个方面超越了POSIX,因此我们还会看到,有一两个函数虽然不属于POSIX,但是如此有用而不容忽略(其中最重要的救命级函数是asprintf)。

GNU

GNU这个缩写代表GNU’s Not UNIX,代表了另一个独立实现和改进UNIX环境的成功故事。大多数的Linux发行版使用GNU工具。有趣的是,你可以在你的POSIX机器上使用GNU编译器组合(GNU Compiler Collection,gcc)——甚至BSD也用它。并且,gcc对C和POSIX的几个方面做了一点扩充并成为事实上的标准,当本书中需要使用这些扩充的时候我会加以说明。

从法律意义上说,BSD授权比GNU授权稍微宽容。由于很多群体对这些授权的政治和商业意义深感担心,实际上你会经常发现多数工具同时提供GNU和BSD版本。例如,GNU 的编译器组合(gcc)和BSD的clang都可以说是顶级的C编译器。来自两个阵营的贡献者紧密跟随对方的工作,所以我们可以认为目前存在的差异将会随着时间逐渐消失。

法律解读

 

美国法律不再提供版权注册系统:除了很少的特例,只要某人写下什么就自然获得了该内容的版权。

发行某个库必然要通过将其从一个硬盘复制到另一个硬盘这样的操作,而且即便带有一点争议,现实中还是存在几种常用机制允许你有权利复制一个有版权的内容。

  • GNU公共许可证:其允许无限制地复制和使用源代码和可执行文件。不过有一个前提:如果你发行一个基于GPL许可证的源代码程序或库,你也必须将你的程序的源代码伴随程序发行。注意,如果你是在非商业环境下使用这样的程序,你不需要发行源代码。像用gcc编译你的源代码之类的运行GPL许可证的程序本身并不会使你具有发行源代码的义务,因为这个程序的数据(比如你编译出的可执行文件)并不认为是基于或派生于gcc的。例如:GNU科学计算库。

  • 次级GPL许可证:与GPL有很多相似,但是具有一个明显的区别:如果你以共享库的方式连接一个LGPL库,你的代码不算作派生的代码,也没有发行源代码的义务。也就是说,你可以采用不暴露源代码的方式发行一个与LGPL库连接的程序。例如:Glib。

  • BSD许可证:要求使用者维持BSD授权原始码原有的版权声明和免责声明,但不要求同时提供你的原始码。

请注意以下的免责声明:笔者不是律师,这段小常识只是完整法律文件的简单声明,读者如果无法判断自身所处的状况或者相关细节,请阅读原始文件或者请教律师。

我以前是一个愤世嫉俗主义者,认为如果你写了第2版,那么你的主要目的就是让那些卖你第1版二手书的人不开心。本书的第2版如果没有第1版被发表的话,是不会,也不可能这么快的就出版的。(反正现在很多读者都在阅读电子版本了。)

与第1版相比,最大的增加就是并发线程,也就是并行计算部分了。它集中描述了OpenMP和原子变量和结构。OpenMP并不是C语言标准,但它是C生态系统中非常可靠的一部分,所以它应该在本书的范围内。原子变量是在2011年12月发布的C标准修订版中加入的,一年以后本书第1版出版的时候,还没有编译器去支持它。现在好了,我们不仅可以在理论上进行讲解,同时还可以有现实的实现和测试代码了。参考第12章。

第1版本得到了很多细心读者的反馈。他们发现了很多可能导致bug的内容,从一些我在命令行上使用的斜杠,到句子中的一些可能会引起误会的词。这个世界上没有什么东西是没有错的,但是有了读者的反馈,这本书现在更加的正确和有用了。

本版中添加的其他内容:

本书使用如下排版约定:

斜体(italic)

用来表示新术语、URL、E-mail地址、文件名、文件扩展名等。

等宽字体Constant width

用来表示程序列表,同时在段落中引用的程序元素(例如变量、函数名、数据库、数据类型、环境变量、声明和关键字等)也用该格式表示。

等宽斜体Constant width italic

用于表示应该以用户提供的值或根据上下文决定的值加以替换的文本。

 

这个图标代表诀窍、建议和一般性的说明。

 

这个图标表示警告或错误。

这是一本试图帮助你解决实际问题的书。总的来说,你可以在你的程序和文档中用本书的代码。除非你复制了太多的部分,你并不需要得到我的许可。例如,你的程序里使用了几段本书的代码并不需要得到许可。但是销售或发行含有O’Relly出版书籍中的源代码的确需要许可。通过引用本书及其源代码的方式来回答一个问题不需要得到许可。在你的文档中合并大量来自本书的代码不需要得到许可。

本书中用到的示范代码可以在以下地址找到:https://github.com/b-k/21st-Century-Examples。

我们感谢,但并不要求,您在引用时注明出处。一个引用通常包括书名、作者、出版商以及ISBN书号。例如,“C程序设计新思维(第2版)Ben Klemens(O’Reilly)。版权2014 Ben Klemens, 978-1-491-90389-6”。

如果你感觉你对示范代码的使用可能超出了上面列举的合理情况,你可以随时通过以下邮件地址联系我:permissions@oreilly.com。

 

Safari网上书店(www.safaribooksonline.com)是一个点播方式的数字图书馆,可以下载世界顶级技术和商业作家的专业书籍和视频。

专业技术人员、软件开发者、Web设计者、商业和创意人士使用Safari网上书店作为他们首选的研究、解决问题、学习和认证培训的信息资源。

Safari网上书店为组织、政府机关和个人提供了一系列的产品组合和定价套餐。订阅人可以从一个可统一检索的出版商的数据库中找到几千本书籍、培训视频和预发布的手稿O’Reilly Media,Prentice Hall Professional、Addison-Wesley Professional、Microsoft Press、Sams、Que、Peachpit Press、Focal Press、Cisco Press、John Wiley & Sons、Syngress、Morgan Kaufmann、IBM Redboks、Packet、Adobe Press、FT Press、Apress、Manning、New Riders、McGraw-Hill、Jones & Bartlett、Course Technology。请登录其网站了解Safari网上书店的详情。

如有对本书的评论或问题,请联系出版商:

美国

O’Reilly Media, Inc。
1005 Gravenstein Highway North。
Sebastopol, CA 95472。

中国

北京市西城区西直门南大街2号成铭大厦C座807室(100035)。
奥莱利技术咨询(北京)有限公司。

Nora Albert:慷慨的支持,豚鼠。

Jerome Benoit:Autoconf技巧。

Bruce Field、Dave Kitabjian、Sarah Weissman:严谨和彻底的审阅。

Patricj Hall:Unicode知识。

Nathan Jepson和Shawn Wallace:社论。

Andreas Klein:指出intptr_t的价值。

Rolando Rodriguez:测试、试用和调查。

Rachel Steely:出品。

Ulrik Sverdrup:指出我们可以使用重复指定初始化来设定默认值。

[1] “it rocks”,此处原文为双关语,借用英语中rock的不同含义,即“摇滚乐”和“很棒”。标题中的“punk rock”为流行于20世纪70年代的一种摇滚乐风格,以狂野反叛为特色,国内也称为“朋克”。——译者注

[2] 这篇前言明显地,而且是必须向Punk Rock Language: A Polemic致敬,作者是Chris Adamson。

[3] 像“can’t get to heaven with a three-chord song”这样的歌词,可能会让Sleater-Kinney被归类在后punk时期?不幸的是,没有ISO punk标准为各种音乐提供精确的定义。

[4] 从下文可以看到,该书的出版被认为是C语言诞生的标志性事件,业内常称为K&R。——译者注

[5] 这里作者将问题与解答比喻为C语言函数入口的参考引用。——译者注

[6] 这里指调用C语言的语言。——译者注

[7] 译者注:世界上就两种语言,没人用的和大家骂的。


在脚本语言花园围墙外的旷野里,有大量解决C语言的那些烦恼的工具,当然你必须自己去寻找。我之所以这么说,因为其中一些工具对于你轻松编写代码是完全必要的。如果你不使用调试器(无论是独立存在的还是集成于IDE环境的),你简直就是自找苦吃。

有很多已经存在的库,你可以在你的代码中去使用。这样你可以集中精力去处理手头的问题,而不是重新实现什么链表、解析器和其他一些基础性的东西。当使用外部库的时候,要保证你的程序的编译也尽量简单。

本书第1部分的内容简述如下。

第1章讲述如何设定基本开发环境,包括找到一个包管理器并利用这个工具安装所用的工具。这些背景知识足够我们体会有趣的内容,比如用从别处得到的库来编译程序。整个过程非常标准化,包括一小部分环境变量的设定和配置。

第2章介绍调试、文档管理和测试工具,因为直到调试、编档和测试都完成的时候,你的代码才能显示出良好的一面。

第3章讨论Autotools,这是一个用于打包并发布你的程序的工具。这一章选择了一种比较详尽的介绍方法,因此还包含了编写shell脚本和makefile的方法。

我们可不能像某些人那样把生活搞得太复杂。第4章介绍Git,一个用来追踪你和同事们的硬盘文件版本的微小改变的工具,以便使你尽可能简单地融合不同的版本。

其他的语言也是现在C语言开发环境的重要因素,因为太多的语言提供了C接口。第5章提供了一些如何编写这些接口的建议,并给你一个基于Python的扩展例子。


对于完成一些实际的项目,C语言的标准库是不够的。

事实上,C语言的生态系统已经延伸到了C语言标准之外。也就是说,如果你不想局限于只完成作业本上的习题,那么你必须知道如何方便地调用那些非ISO标准库以外的函数。如果你想处理XML文件、JPEG文件或者TIFF文件,那么你需要那些不是标准但是可以免费使用的libxml、libjpeg以及 libtiff库。不幸的是很多教科书都把这部分内容忽略了,这就使得读者不得不自己进行探索。这就是为什么很多批评C语言的人会说出不公平的言论:C语言是一门40岁的老语言了,所以你必须从头开始写很多有用的函数。这些读者根本就不知道如何使用外部的库。

以下是本章的主题。

设定必要的工具集

比起需要自行准备各种组件的黑暗时期,如今已经轻松很多了。你只需要10~15分钟就可以建立起完整的开发环境(当然得加上下载所需要的时间)。

编译一个C程序

你已经知道怎么做了,但是我们还需要设定一下需要链接哪些函数库,以及那些函数库所在的位置;只是输入cc myfile.c已经不够了。Make几乎是最简单的编译工具程序,我们就以它作为切入点开始介绍,先介绍一个最简单的makefile,虽然它很简单,但是有非常大的空间可以继续改进和扩充。

设定一些变量并加入一些新的库

无论我们使用什么系统,它们都会利用一小部分环境变量对自身进行定制。所以我们首先介绍这些环境是什么以及如何设定它们。一旦我们完成了基本的设定工作,只要稍微调整这些环境变量,就可以使用新加入的函数库了。

建立一个编译系统

作为回报,我们可以利用以上介绍的这些知识,建立一个简单的编译系统。在这个系统中,我们可以在命令行编译那些复制来的代码了。

对于使用IDE(集成开发环境)的用户来说,有一点值得特别说明:即使你不使用make,但是本部分内容依然和你有关。make使用的很多步骤,在IDE中都有对应的步骤。如果你知道make的内部机理,你就有能力对IDE做出更适合你的一些调整和设定。

如果你没有使用过包管理工具,那你太落伍了。

这里介绍包管理工具有以下几个原因:首先,有些读者可能还没有安装过程序。对于这部分读者,我把这部分内容放到本书的开始,就是为了让你尽快地获得这些工具。一个好的包管理工具会让你很快地建立起一个POSIX子系统,其中包含很多你听说过的语言的编译器、很多的游戏、一些常见的办公应用软件以及几百个C语言的库等。

其次,对于C语言的开发者,包管理工具是日后我们安装C语言函数库的一个重要工具。

最后,当你从一个包的使用者成长为一个包的开发者的时候,本书会教你如何让自己的包更易于安装,这样当包存储库的管理员决定把你的代码包含进存储库的时候,管理员可以无障碍地建立最后的包。

如果你是一个Linux用户,并已经通过包管理器配置了电脑,你就已经知道了软件获取的过程非常简单。对于Windows系统的用户来说,我会在下面介绍Cygwin。对于使用Mac的用户有一些选项,例如Fink和Homebrew或者Macports,所有的这些选项都依赖于Apple的Xcode包。你可以在系统的安装光盘上找到它,或者到苹果的APP商店获取,或者通过注册成为苹果的开发者而获取(依赖于你的Mac电脑的具体生产年份)。

你需要什么包呢?这是C开发过程中最基本的问题。因为每一个系统都有不同的组织方式,所以包可能被包装在不同的地方,或者在基本包中默认安装,或者被命名为其他古怪的名字。如果你也不是很确定,那就先安装它再说,毕竟我们的电脑不会因为安装过多软件就变得不稳定或者运行慢了。但是你可能没有那么大的带宽来下载,或者没有那么大的硬盘来安装所有的包,所以你还需要做一些选择。如果你发现自己没有安装某些包,还可以回过头来重新安装。以下这些包是必须安装的。

在本章的后面,我还会介绍一些强力工具。

当然,还有一些C库能够避免你重新发明“轮子”(更准确的类比是,重新发明“火车头”)。你可以获得更多的库,下面这些库是我们要在本书中用到的。

C语言库没有一致的命名标准,你必须要了解你的包管理器是如何将一个单独的包分解成不同的子部分的。一般有一个供用户使用的包,同时还有一个供开发者使用的包,所以请在选择基本的包的同时,选择带有-dev或者-devel的包。某些系统将文档分拆在独立的包中。也有的要求你单独下载带有调试符号表的包。如果你在没有调试符号表的包上运行GDB,那么GDB会要求你下载带有调试符号表的包。

如果你使用POSIX系统,在安装完所有前面提到的工具后,你就有了完整的开发环境,现在你可以开发你的程序了。对于Windows的用户,下面介绍如何在Windows操作系统下设置开发环境以便和Windows主系统进行沟通。

在大多数的系统下,C语言是主要的开发语言,其他的工具都以C语言为参照进行开发。但是Windows操作系统很奇怪地忽略了C语言。

因此我们需要一点时间来说明如何在Windows环境下设置开发环境。如果你不使用Windows系统,可以忽略这部分内容,直接跳到1.3“链接函数库的方式”一节。

由于C语言起源于UNIX,并与UNIX一起演变,我们很难将两者分开来讨论。我想从POSIX开始介绍应该比较简单。如果想在Windows平台下编译其他平台下开发的程序,这是最自然的方法。

就作者所知,所有的操作系统可以分为两大类。

POSIX兼容并不代表系统的外观和UNIX相像,例如大部分Mac使用者完全不知道自己使用的是一个带有华丽界面的BSD系统。但是了解这部分知识的人却可以从应用程序→工具文件夹中启动终端(Terminal)应用程序,然后在其中运行ls、grep或者make等各种工具。

另外,并不是所有的系统都100%符合标准(例如Fortran 77编译器)。就本书来说,我们需要有一个基本的类似POSIX shell的shell、一些工具(sed、grep、make等)、一个C99编译器,以及fork和iconv等标准C语言库以外的函数库。这些工具和库可以作为主系统的补充。包管理工具相关的底层脚本、Autotools以及所有开发可移植代码的工具都在某种程度上依赖上面提到的那些工具和库。所以即使你不愿意与命令行打交道,你依然需要安装这些工具和库。

在作为服务器的操作系统以及完整的 Windows 7系统中,微软公司提供以前称为INTERIX,现在称为Subsystem for UNIX-based Applications(SUA)的子系统,包含常用的POSIX系统调用、Korn shell以及gcc。这个子系统默认不安装,需要你另行下载。目前版本的Windows不再提供SUA,Windows 8也不提供了,因此无法依赖微软自己提供的POSIX子系统了。

所以我们需要使用Cygwin。

如果想要从头建立Cygwin,可以参考下面的介绍。

1.为Windows撰写C函数库,提供所有的POSIX函数。这需要调整Windows和POSIX系统间的差异,例如Windows系统使用C:代表硬盘,而POSIX使用统一文件系统(Unified Filesystem)。对这种情况,可以为C:建立cygdrive/c,为D:建立cygdrive/d等别名。

2.现在可以编译POSIX标准程序,链接到上一步介绍的函数库,产生新的Windows版本的ls、bash、grep、make、gcc、x、rxvt、libglib、perl、python等。

3.建立好很多的程序和函数库以后,接着建立包管理工具,让使用者可以自己选择安装软件。

但是作为Cygwin的使用者,你不需要完成上面介绍的那些麻烦的步骤,只需要从Cygwin网站(http://cygwin.com)下载包管理工具,选择要安装的包,当然包括上面清单列出来的那些程序,再加上一个合适的终端(Terminal)(可以试试Mintyy,或者安装X系统中的Xterm,这两者都比Windows 自带的cmd.exe友好)。安装完后,你就可以看到开发系统所需要的各种豪华工具都在其中了。

在“路径”部分,我讨论了影响编译的各种环境变量,包括搜索文件的包含路径。这一部分并不是POSIX所独有的,Windows也有环境变量,你可以在控制面板的环境设置部分找到它们。如果你把Cygwin的bin路径(C:\cygwin\bin)加到Windows的PATH变量中,那么使用Cygwin的时候会更加方便。

现在你可以编译C代码了。

微软公司在Visual Studio中提供了C++编译器,提供了与C89相容的模式(通常称为ANSI C,虽然C11是ANSI的当前标准)。这是目前微软公司提供的编译C语言的唯一方式,微软公司的很多代表都表示不会去支持C99标准(更别提C11标准了),Visual Studio是唯一一个还使用C89标准的编译器,因此我们需要其他替代方案。

当然,Cygwin提供了gcc,如果按照前面介绍的步骤安装好了Cygwin,那么你就有了完整的开发环境。

在Cygwin下编译的程序会依赖于Cygwin1.dll库中所提供的POSIX函数(不论你的程序是否使用任何POSIX相关的调用)。在有安装Cygwin的机器上执行这些程序不会有任何问题,使用者可以通过双击执行这些程序,系统能够找到cygwin1.dll。如果要在没有安装cygwin1.dll的机器上运行这个程序,你必须与程序同时提供cygwin1.dll文件。在作者的机器上,路径是/bin/Cygwin1.dll,Cygwin1.dll采用类似GPL的授权方式(参考前言部分的“法律解读”),如果将dll从Cygwin分离出来而单独与你的应用程序一起分发,那你必须提供程序的源代码[1]

如果有困难,你需要用不依赖于Cygwin1.dll的方式来重新进行编译。也就是要你的程序中使用MinGW,而不去使用POSIX相关的函数(像fork和popen函数),就像我们后面介绍的那样。利用cygcheck可以发现你的应用程序依赖于那些DLL,从而验证你的执行程序是否连接到了Cygwin1.dll。

 

查看一个程序或者动态链接库依赖于哪些库,用下面的命令:

  • Cygwin: cygcheck libxx.dll。

  • Linux: ldd libxx.so。

  • Mac: otool -L libxx.dylib。

如果你的程序不需要调用POSIX函数,你可以使用MinGW(Minimalist GNU for Windows),它提供了一个标准的C编译器和一些基础工具。MSYS是与MinGW伴生的,提供了另外的一些工具和shell。

MSYS提供了一个POSIX shell(你可以使用Mintty或者RXVT终端来运行你的shell)。或者干脆不使用任何命令行,只是使用Code::blocks,这是一个在Windows上使用MinGW的IDE集成环境。Eclipse是一个功能更加丰富的可以配置成与MinGW一起工作的IDE集成环境,虽然这需要更多一点的配置工作。

不过如果POSIX命令行能让你感觉更舒服些,那就安装Cygwin,下载提供MinGW版本的gcc,用这些编译环境而不是用与POSIX连接的、默认版本的Cygwin gcc。

如果你还没有遇到过Autotools,那你会很快遇到。通过Autotools建立软件包的三个标志性命令是:./configure、make和make install。MSYS提供了足够的机制以保证可能让包在MinGW环境下安装;否则你就必须从Cygwin的命令行来安装这个包,但是你可以用下面的命令来配置这个包,使用Cygwin的MinGW32编译器来产生与POSIX无关的代码:

/.configure –host=ming32

然后像通常那样运行make和make install。

在MinGW下编译后,不管是通过命令行还是Autotools,你都会得到一个Windows本地的二进制代码。因为MinGW并不知道cygwin1.dll的存在,你的程序并不会有任何POSIX调用。你得到的将是一个真正的Windows程序,没有人会知道你是从POSIX环境编译出来的。

然而,MinGW的真正问题是缺乏预编译库[2]。如果你想摆脱cygwin1.dll,那么你也不能使用与Cygwin一起发行的libglib.dll版本。你将必须从源代码重新将GLib编译成一个Windows 动态链接库——但是Glib在国际化方面依赖GNU的gettext,所以你需要先把那个库编译一遍。现代代码依赖于现代的库,所以你可能会发现自己花费了很多时间来处理类似的工作,而在别的系统上可能只是一行包管理器的命令。如果这样,我们就真的如某些人所说,C已经40岁了,你需要从头写每样东西。

所以,下面就是几句忠告。微软公司拒绝在C语言标准的支持上与别人沟通,任由其他人实现后grunge时代的C编译器。Cygwin完成了这些工作并提供了全功能的包管理器,带有可以满足你的多数或全部工作的足够多的库,但这些都伴随着POSIX风格的代码和Cygwin的DLL。如果你觉得这是一个问题,那么你将不得不多做很多工作,以重建那些优雅的代码所需要的整个环境和库。

有了编译器,有了POSIX的工具包,还有一个可以用来方便地安装几百个库的包管理器。现在我们开始用这些工具来编译程序。

我们必须从编译器命令行开始,这会很快给我们带来很多麻烦,好在还可以用三个(有时候是三个半)相对简单的步骤结束。

1.设置一个变量,代表编译器使用的编译选项。

2.设置一个变量,代表要链接的那些库。所谓的半个步骤是指,有时你不得不设定一个唯一的变量,指定编译时使用的函数库;或者有时不得不设定两个变量,分别用在编译时和运行时的链接。

3.设置一个使用这些变量来协调编译的系统。

为了使用一个库,你必须告诉编译器你将从库中两次导入函数:一次是为了编译,一次是为了链接。对于一个在标准位置的库,这两次导入一次发生在通过程序中的#include指令时,另外一次发生在通过编译选项−l进行编译时。

例1-1展示了一个小例子,可以用来做一些神奇的计算(至少对于我来说是有趣的;如果统计学术语对你来说就像希腊文一样,你也不用太在意)。erf(x)是C99标准的误差函数,是与平均数为0、均方差为的从0到x的正则分布的积分紧密相关。这个例子中,我们用erf来验证一个在统计学家中流行的领域(一个标准大样本假设的95%置信区间)。我们把这个文件命名为erf.c。

例1-1 一个使用标准库的只有一行的程序(erf.c)

#include <math.h>    //erf, sqrt
#include <stdio.h>   //printf

int main(){
    printf("The integral of a Normal(0, 1) distribution "
           "between -1.96 and 1.96 is: %g\n", erf(1.96*sqrt(1/2.)));
}

你应该已经很熟悉#include行了。编译器将把math.h和stdio.h文件的内容添加在源文件的这个地方,并因此导入了printf、erf和sqrt的声明。在math.h中的声明并没有具体指定erf函数做什么,只是说这个函数接收一个double型参数,也返回一个double类型的值。这些已经足够编译器去检查我们使用的合法性并产生一个目标文件了。这个目标文件中带着一个给计算机的标记,这个标记告诉计算机,一旦你看到这个标记,就去找erf函数,并用erf的返回值替代这个标记。

而链接器的任务是确实找到erf这个函数来取代目标库中的标记,这个函数就在你硬盘的某个库里。

在math.h中声明的数学函数分散在它们自己的库中,你需要通过一个-lm编译选项告诉链接器。这里,-l是一个选项,用来指示某个库需要被链接进来。而本例中的库有一个用单个字母表示的名字:m。你不用设定任何选项就可以使用printf,因为在链接命令行的末尾,有一个隐含的-lc选项来要求链接器链接标准libc库。随后,我们将看到Glib 2.0通过-lglib-2.0被链接进来,GNU科学计算库也通过-lgsl被链接进来,依此类推。

所以,如果文件名为erf.c,那么完整的gcc编译器命令行应该如下所示(这里包括几个选项,在后面将会详细介绍)。

gcc erf.c -o erf -lm -g -Wall -O3 -std=gnu11

这样就能告诉编译器通过程序中的#include包含数学函数,并告诉链接器通过命令行中的-lm链接数学库。

-o选项用来给出输出文件的名字;否则将得到一个默认的可执行文件名a.out。

 

警告

在Mac系统中,c99是一个特别修改的gcc版本,可能并不是使用者预期的版本。如果你有一个不符合要求的c99版本,或者它根本就不存在,那就自己建立一个。把一个叫作c99的文件放在你的搜索路径中的目录里:

或者如果你愿意,就用

并通过chmod +x c99让它成为可执行的文件。

在后面你将看到我几乎每次都用到一些编译器选项,并且我建议你也使用它们。

c99 erf.c -o erf -lm -g -Wall -O3
gcc --std=gnu99 $*

在后文将介绍的makefile中,我通过设定一个变量CC=c99实现这个效果。

clang $*

 

提示

要坚持使用编译器警告。即使你对你的代码质量已经非常挑剔了,即使你可能已经熟知C语言标准了,你也不可能比你的编译器更挑剔和更熟知C。旧的C教材连篇累牍地警告你注意=和==的差别,或者检查是否所有的变量在使用前都被初始化了。作为一本更加现代的书的作者,我可以轻松一点了,因为我可以把所有的警告总结为一点:永远都要用你的编译器警告。

如果编译器建议你做一个改变,不要怀疑或试图碰运气而放弃修改。尽可能去:(1)理解你为什么得到了警告;(2)修改代码直到不产生任何警告和错误。编译器信息是出了名的难懂,所以如果你在第(1)步有困难,把警告信息贴在搜索引擎上,就能看到有多少人在你之前也面对了类似的问题。你可能想加上-Werror编译选项,这样你的编译器将把警告当作错误来处理。

在笔者的硬盘中有超过70万个文件,声明sqrt和erf函数的头文件只是其中之一,而且还有一个是包含了这些函数对应的被编译后的目标文件[3]。编译器需要知道在哪个目录中去查找正确的头文件和目标文件,当开始使用非标准C库的时候,这个问题就会变得更加严重。

在一个典型的安装中,库可能存放的地方至少有三个。

操作系统标准的路径一般不会引发什么问题,编译器也应该知道如何查找那些路径,并找到标准C库以及伴随其安装的任何文件。POSIX标准用“通常位置”来指代上面所说的目录。

但是对于其他的东西,你必须告诉编译器如何查找。这使得状况变得有点复杂:没有一个统一的方法去寻找那些不按标准位置安装的库。这一点是人们对C比较恼火的地方。不过令人感到欣慰的是,编译器知道如何在通常位置查找,而库的提供者也倾向于将库安放在通常位置,所以你可能从来没有真正手工去指定这些路径。再者,也有几种方法使你可以指定路径。最后,一旦你把非标准库安装在系统上,你可以在shell脚本或makefile中的变量中设定这个路径,然后就再也不会有路径的烦恼了。

假设你在计算机上安装了一个叫作Libuseful的库,并且你知道与之相关的文件放在/usr/local/目录下,也就是你的系统管理员安装本地函数库的位置。你已经把#include <useful.h>放在了你的代码里,现在你必须把下面一行放在你的命令行中:

gcc -I/usr/local/include use_useful.c -o use_useful -L/usr/local/lib –luseful
gcc specific.o -lbroad –lgeneral

任何其他顺序,比如gcc -lbroad -lgeneral specific.o,都可能失败。你可以这样理解链接器的工作方式,链接器首先查看第一个目标——specific.o,将无法解析的函数、结构和变量名记入一个列表。然后链接器查看下一个目标——lbroad,并在这个目标内搜索列表中仍然缺失的项目,同时有可能在列表中添加新的项目;接着在-lgeneral查找仍然缺失的项目。如果直到搜索完最后的目标仍然存在未解析的符号(包括在最后的隐含的-lc),链接器将终止运行并向用户给出最后剩下的未解析项目。

现在回到路径问题:要链接的库到底在哪里呢?如果安装库的包管理器和安装操作系统其他部分的包管理器相同,那么库最可能在通常路径,你也不用去担心这个。

你可能想不清你自己的本地库应该放在何处,如/usr/local,还是/sw或者/opt。你无疑可以用硬盘搜索的方式来查找,如在你的机器或POSIX环境中使用:

find /usr -name 'libuseful*'

来搜索/usr 中以libuseful开头的文件。当你发现Libuseful库的共享目标文件在/some/path/lib中,那么几乎可以肯定对应的头文件一定在/some/path/include中。

在硬盘里到处找库文件是一件很恼人的事情,为了解决这个问题,pkg-config维护了一个包含配置信息和位置信息的资料库,然后pkg-config会报告编译时需要的这些信息。在命令行中输入pkg-config;如果你得到一个错误提示说“没有指定包名字”,那么很好,说明你有pkg-config命令了,你可以用它来做研究了。例如,在我个人计算机的命令行上输入以下两行命令:

pkg-config --libs gsl libxml-2.0
pkg-config --cflags gsl libxml-2.0

得到下面两行输出:

-lgsl -lgslcblas -lm -lxml2
-I/usr/include/libxml2

这些正是我用来编译GSL和LibXML2所需要的所有选项。-l选项揭示出GNU科学计算库依赖于基本线性代数子程序库(BLAS),而GSL的BLAS库依赖于标准数学库。看起来所有这些库都在通常路径,因为这里没有-L选项,但是-I选项表明LibXML2的头文件的特殊位置。

回到命令行,shell提供了一个方法,就是当你把一个命令行用单引号包围时,这个命令行会被其自身的输出替代。就是说,输入:

gcc 'pkg-config --cflags --libs gsl libxml-2.0' -o specific specific.c

编译器看到的是:

gcc -I/usr/include/libxml2 -lgsl -lgslcblas -lm -lxml2 -o specific specific.c

所以pkg-config会为我们做很多工作,但是这并不足以使它成为一个标准,不是所有平台都有pkg-config命令,也不是每个库都用它注册。如果你没有pkg-config,你就必须自己研究,比如读这个库的手册,或者像前面那样搜索。

 

警告

有很多与路径有关的环境变量,比如CPATH、LIBRARY_PATH或者C_INCLUDE_PATH。你可以在.bashrc或别的用户定义的环境变量列表中设定它们。它们都不是标准的——连Linux和Mac中的gcc都分别使用不同的变量,别的编译器自然也使用自己的变量。我发现在每个项目的makefile或类似机制的基础上,用-I和-L来设定这些路径相对更容易。如果你喜欢这些路径变量,可以在你的编译器的帮助文件的末尾查找符合你的情况的相关环境变量。

即便用pkg-config,我们也显然需要某种工具帮我们把所有这些自动执行。每个元素都很容易理解,但是组合起来却是一个冗长且重复的琐碎工作。

编译器连接静态库的时候,是将库里的相关内容直接复制到最终的可执行文件中的。所以程序本身或多或少是一个独立的系统。而共享库与你的程序是在运行时链接的,就是说我们在运行时会遇到像编译器在编译时寻找库的路径那样的问题。甚至更糟的是,你的程序的用户也存在同样的问题。如果你的库在一个非标准的路径,那你需要找到一个修改运行时搜索库的路径的方法。有以下选择。

LDADD=-Llibpath -Wl,-Rlibpath

到相应的makefile中。-L选项告诉编译器到哪里去找到库以解析符号;-Wl选项从gcc/Clang/icc传递这个选项到链接器,而链接器将给定的-R嵌入所链接的库的运行时搜索路径。不幸的是,pkg-config经常不知道运行时路径,所以你可能必须手工输入这些信息。

export LD_LIBRARY_PATH=libpath:$LD_LIBRARY_PATH      #Linux, Cygwin
export DYLD_LIBRARY_PATH=libpath:$DYLD_LIBRARY_PATH  #OS X

有些人反对过度使用LD_LIBRARY_PATH(万一有人把恶意伪装的库放到那个路径,在你没有察觉的情况下代替了真正的库怎么办?),但是如果你所有的库都放在一个路径,将这个路径加入进来也是合理的。

自己动手:下面是编程世界著名的hello.c程序,就两行:

把这个源文件和前面的makefile存在同一个目录中,然后试着按前面的步骤编译和运行程序。

makefile提供一个解决所有以上这些麻烦的方案。它基本上可以看作结构化的变量和一系列一行的shell脚本。POSIX标准的make程序读入makefile的内容作为指令和变量,然后自动化处理那些冗长、烦琐的命令行。在这部分讲解之后,就没有什么必要去直接调用编译器了。

#include 
int main(){ printf("Hello, world.\n"); }

在“makefile与shell脚本”中,会讲述关于makefile的更多细节;这里,先给出一个最小的、实用的并且能够编译一个依赖于一个库的基本程序的makefile,它只有6行:

P=program_name
OBJECTS=
CFLAGS = -g -Wall -O3
LDLIBS=
CC=c99

$(P): $(OBJECTS)

用法:

 

在C代码中,可以用getenv函数来得到环境变量。getenv非常简单、易用,所以你可以从命令行中尝试设定不同的选项。

例1-2是一个打印示例程序,只要用户需要,随时打印一个信息到屏幕。环境变量msg用于设定要打印的信息,同时通过reps设定重复的次数。请注意我们是如何设定它们的默认值10和“Hello.”的,在调用getenv返回NULL(典型的含义是这个环境变量没有被设定)的时候,默认值才被设定。

例1-2 环境变量提供了一个改变程序细节的快速方式(getenv.c)

就像之前看到的,我们可以用一行命令导出一个变量,这样可以使得向程序发送变量更加方便。用法:

你可能觉得这个用法很奇怪——程序的输入应该跟在程序名后面才对,真是可恨——先不管这些奇怪的事情,你可以看到程序自身进行了一些设置工作,我们几乎没费什么精力就立即得到了来自命令行的命名参数。

当你的程序有更多的程序变量的时候,可以研究一下getopt或者是基于GNU的argp_parse,它按照通常的方法取得输入参数。

很快我们会介绍makefile的实际应用,但你可能注意到前述makefile里6行中有5行是关于变量设定的(目前多数被设定为空),这意味着我们还需要多花一点时间在环境变量的细节上。

 

警告

历史上,曾经出现过两种主流的shell语法:一种基本上是基于Bourne shell的,另一种主要基于C shell。C shell的变量语法稍有不同,例如,采用set CFLAGS="-g-Wall-O3"来设定CFLAGS的值。但是POSIX标准是写成Bourne类型的语法,也就是我这本书余下部分所采用的。

shell和make用$来指代变量的值,但是shell用$var,而make要求任何多于一个字母的变量必须用括号括起来:$(var)。所以,前面的makefile中,$(P):$(OBJECTS)将相当于:

program_name:
#include <stdlib.h> //getenv, atoi
#include <stdio.h>  //printf
 
int main(){
 char *repstext = getenv("reps");
 int reps = repstext ? atoi(repstext) : 10;
 
 char *msg = getenv("msg");
 if (!msg) msg = "Hello.";
 
 for (int i=0; i < reps; i++)
 printf("%s\n", msg);
}

有以下几种办法来让make识别变量:

reps=10 msg="Ha" ./getenv
msg="Ha" ./getenv
reps=20 msg=" " ./getenv
export CFLAGS='-g -Wall -O3'

我自己经常忽略这个makefile中的第一行,P=program_name,取而代之的是在每个会话中通过export P=program_name来设定,这样我就不用重复编辑 makefile了。

PANTS=kakhi env | grep PANTS

你将看到正确的变量及其他的值。这是为什么shell不让你在等号附近放空格的原因:空格是用来分割命令行中的赋值操作和后续的命令行的。

在这个方法中设定和导出变量应该在一行实现。如果你在命令行中执行了上面这条命令,再次运行env | grep PANTS,就会发现PANTS不再是一个被导出的变量了。

只要你愿意,你可以指定任意数量的变量:

PANTS=kakhi PLANTS="ficus fern" env | grep 'P.*NTS'

这个技巧出现在shell规范的“简单命令”描述部分,也就是说赋值必须在一个实际的命令之前。这在你使用非命令的shell结构时非常重要。编写:

VAR=val if [ -e afile ] ; then ./program_using_VAR ; fi

将失败并伴随一个晦涩的语法错误。正确的方式是:

if [ -e afile ] ; then VAR=val ./program_using_VAR ; fi
make CFLAGS="-g -Wall" Set a makefile variable.
CFLAGS="-g -Wall" make   Set an environment variable visible to make and its children.

对makefile而言,上面所有这些手段都是相等的;例外之处在于,被make调用的子程序只知道新的环境变量,而不知道任何makefile中的变量。

C中的环境变量

make也提供一些内置的变量。下面是其中一些(POSIX标准)变量的介绍,你可能在随后学习“规则”一节中用到这些变量。

$@

返回完整的目标文件名。所谓目标(target)就是指需要被生成的文件,比如从一个a.c文件中编译而得到的a.o文件,或者一个通过链接.o文件生成的程序。

$*

不带文件名后缀的目标文件。如果目标文件是prog.o,$就是prog,而$.c就成为prog.c。

$<

触发和制作该目标的文件的名称。如果我们正在制作prog.o,有可能是因为prog.c文件刚被修改,所以$<就是prog.c。

 

提示

如果你想看到你的make内置的全部默认规则和变量的列表,可以尝试:

现在让我们专注地了解一下makefile的执行过程,并了解变量是如何影响这个过程的。

先不讨论变量的事情,来看一下makefile的代码片段,一般有以下的形式:

target: dependencies
 script
make -p > default_rules

假设输入命令make target,则target被调用,那么dependencies(依赖项)将被检查。如果target是一个文件,dependencies也是文件,并且target是比dependencies时间上更新的文件,那就是说这个文件已经是最新的,所以系统也不会再做什么。否则,针对target的处理将被暂停,所有的dependencies将首先被运行或重新产生,当这一切结束后,target段落的script(脚本)部分才会被执行。

例如,在本文成书之前,我的博客上贴出了一系列的文章(在http://modelingwithdata. org)。每篇博客都是用HTML和PDF格式上传的,也都是用LaTeX产生的。为了让这个例子简单,我忽略了很多细节(如latex2html的很多配置选项),但这是一个人们经常编写和运行的makefile。

 

警告

如果你将这些makefile片段从屏幕或者书本上复制到makefile文件中,不要忘记每行代码开头的空白部分必须是制表符(Tab键)而不是空格(Space键)。要怪就怪POSIX标准吧。

all: html doc publish

doc:
    pdflatex $(f).tex

html:
    latex -interaction batchmode $(f)
    latex2html $(f).tex

publish:
    scp $(f).pdf $(Blogserver)

我们通过类似export f=tip-make的命令来设定f。然后在命令行输入make的时候,第一个目标all被检测到。就是说,不指定目标的make命令默认指定makefile文件中的第一个目标all。这个目标依赖于html、doc和publish,所以那些目标也被依次调用。如果我知道这还没有准备好递交给这个世界,我可以调用make html doc并仅完成其中的两个目标。

在之前那个简单的makefile中,我们仅有一组target/dependency/script。例如:

P=domath
OBJECTS=addition.o subtraction.o

$(P): $(OBJECTS)

这个和我的博客里的makefile遵循同样的支持项和脚本执行次序,但是脚本是隐含的。这里,P=domath是被编译的程序,并且依赖于目标文件addition.o和subtraction.o。因为addition.o没有被列出来当作一个处理目标,所以make用一条如下所述的隐含的规则来从.c文件编译.o文件。对subtrction.o和domath.o也是一样的操作(因为GNU make隐含假设domath依赖于这里给出设置的domath.o)。一旦所有的目标文件被建立了,因为没有在$(P)目标处指定运行的脚本,那么GNU make使用它的默认脚本来链接.o文件而生成一个可执行文件。

POSIX标准的make有一个特殊的从a.c源文件到a.o的编译方法:

$(CC) $(CFLAGS) $(LDFLAGS) -o $@ $*.c

这里$(CC)变量代表你的C编译器;POSIX标准规定一个默认的CC=c99,但是GNU make的当前版本设定为CC=cc,并且一般就是gcc的一个链接。在本段最早的那个最小的makefile中,$(CC)被明确设定为c99,$(CFLAGS)被按照之前的选项列表设定,$(LDFLAGS)没有被设定,因此以空值传入。所以如果make认为它必须产生your_program.o,根据以上的makefile中,会运行下面的命令行:

c99 -g -Wall -O3 -o your_program.o your_program.c

当GNU make觉得你需要从目标文件链接出一个可执行文件时,它用下面的方法实现:

$(CC) $(LDFLAGS) first.o second.o $(LDLIBS)

如果想起在链接器中存在次序问题,那么我将需要两个链接器变量。在前面的例子中,我们需要:

cc specific.o -lbroad -lgeneral

作为链接命令的对应的部分。比较实际的编译命令和预设的编译命令,我们可以看到需要设定LDLIBS=-lbroad -lgeneral。

所以,这个游戏就是:找到合适的变量并把它们设置在makefile中。你还是要探究一下正确的选项分别是什么,但是至少你可以把它们写在makefile中,之后便再也不必去为这个事情而烦心了。

自己动手:修改makefile以便编译erf.c文件。

如果你用一个IDE,或者CMAKE,或者任何POSIX标准make的替代品,你都可以做这个“找到合适的变量”的游戏。我还会继续讨论前面的最小makefile,在你的IDE中应该不难找到对应的变量。

CFLAGS='pkg-config --cflags apophenia glib-2.0' -g -Wall -std=gnu11 -O3
LDLIBS='pkg-config --libs apophenia glib-2.0'

或者,手工指定-I、-L和-l变量,如:

CFLAGS=-I/home/b/root/include -g -Wall -O3
LDLIBS=-L/home/b/root/lib -lweirdlib
export CFLAGS='-g -Wall -O3 -std=gnu11'
export LDLIBS='-lm'
make erf

看看make如何用它的C编译知识来做余下的工作。

选择什么链接器选项来建立共享库?

 

说实话,我根本不知道。不同的类型和不同的年份的操作系统中都是不同的,甚至在同一个系统中,规则也经常有点混乱。

不过,我们将在第3章中介绍的工具Libtool了解每个操作系统中的每个共享库的每个制作过程的细节。我建议你花点时间去了解Autotools,那么就可以一举解决共享目标文件的编译问题,而不是花时间了解每种系统的正确编译器选项和链接过程。

到目前为止,我们讲的都是如何用make来编译自己的代码。而编译别人的代码则是另外一回事了。

让我们试一个简单的包,即包含大量数值计算函数的GNU科学计算库(GSL)。

GSL是用Autotools打包的,而Autotools是一个为在任何机器上都能使用函数库的工具的集合,其原理是测试所有已知的特殊细节并加上适当的解决方法。Autotools主要关注现代的代码是如何发布的,3.3“用Autotools打包你的代码”将详细讲述如何用它来打包你自己的程序和库。但是到现在,我们可以从用户的角度,来享受快速安装有用的库文件的便利。

GSL经常是由包管理器预编译好的,但是为了达到掌握编译库的每一步的目的,这里我们先得到GSL的源代码并手工把它安装好,假设你具有计算机的root权限。

wget ftp://ftp.gnu.org/gnu/gsl/gsl-1.16.tar.gz ❶
tar xvzf gsl-*gz                               ❷
cd gsl-1.16
./configure                                    ❸
make
sudo make install                              ❹

❶ 下载源文件的zip压缩包。如果你还没有wget,使用包管理器安装它,或者在你的浏览器里直接键入这个URL。

❷ 解压缩包。x=抽取,v=详细模式,z=用gzip解包,f=文件。

❸ 检测。如果configure步骤给你一个“缺失一个元素”的出错信息,那么用包管理器来获取这个元素,然后重新运行configure。

❹ 将GSL安装到正确的位置上——需要具备相应权限。

如果你是在自己家里尝试这个,那么你可能已经有了root权限,上面步骤执行起来会很顺利。如果你是在工作场所并是在使用一个共享的服务器,则拥有超级用户权限的概率应该不会很大,由于最后一步要求超级用户权限,而你没有密码。如果这样,你就屏住呼吸直到下一部分吧。

它安装了吗?例1-3是一个使用GSL函数来找到95%的置信空间的小程序;编译一下这个例子,看看你能否把这个例子链接起来并运行。

例1-3 利用GSL来重做一次例1-1(gl_erf.c)

#include 
#include 

int main(){
    double bottom_tail = gsl_cdf_gaussian_P(-1.96, 1);
    printf("Area between [-1.96, 1.96]: %g\n", 1-2*bottom_tail);
}

为了使用刚才安装的库,我们需要修改会用到这个库的程序的makefile文件,以指定库及其位置。

你可以采用下列语句的任何一种,取决于你是否安装了pkg-config:

LDLIBS='pkg-config --libs gsl'
#or
LDLIBS=-lgsl -lgslcblas -lm

如果库没有被安装在标准的位置并且pkg-config也没有被安装,你需要把路径加在定义行的开头,例如,CFLAGS=-I/usr/local/include和LDLIBS=-L/usr/local/lib –Wl, -R/usr/local/lib。

你在工作场所的共享机器里可能没有root权限,或者你被有特别的权限的管理员控制。那你就必须做点地下工作,制作一个属于自己的root目录。

第一步很简单,即创建这个目录:

mkdir ~/root

由于我已经有了一个~/tech目录,用来保存我所有的技术文档、手册和源代码,所以我建立的是~/tech/root目录。名字其实无所谓,但是我还是喜欢用~/root作为本书的示范目录。

 

提示

shell可以将波浪线替换为你个人主目录的完整路径,节省你很多打字的时间。POSIX标准只要求shell在第一个词或者冒号后的第一个字母(路径类型的变量会使用冒号)才这样处理,但是多数的shell扩展支持了词中间的波浪线。其他的程序,比如make,或许能,或许不能识别你个人主目录的波浪线。这种情况下,使用POSIX强制要求的HOME环境变量,如下面的例子所示。

第二步,把新建的root系统添加到所有相关的路径上去。修改.bashrc (或其他shell的该配置文件)的PATH变量如下。

PATH=~/root/bin:$PATH

如果把你的新的目录的bin子目录添加在你的原来的PATH前面,这个子目录就将被首先查找到,并且你放在那里的任何程序都将被率先找到。这样你就可以把标准共享目录中的任何程序的替代版本放在那里。

对于那些你想链接的C程序库,请注意将新的搜索路径加入到在前面的makefile文件中:

LDLIBS=-L$(HOME)/root/lib       (plus the other flags, like -lgsl -lm ...)
CFLAGS=-I$(HOME)/root/include  (plus -g -Wall -O3 ...)

现在你已经有了一个本地的root目录,你也可以在别的系统上使用它,比如Java的CLASSPATH。

最后一步是在新的root目录上安装程序。如果你有源代码并使用Autotools,你只需要在合适的位置添--prefix=$HOME/root:

./configure --prefix=$HOME/root && make && make install

这些程序和库都在你的主目录中,所使用的许可权不会超出你所具有的范围,系统管理员不能抱怨你做了任何危害他人的事情。如果你的系统管理员还是有所抱怨,那么即使有点难过,你也应该和他分手了。

手册

 

我记得以前的确有印刷版本的手册,不过现在它们都存在于man命令中了。例如,用man strtok来阅读关于strtok函数的内容,一般包括需要包含什么样的头文件、输入参数,以及它的基本用法的解释。手册文档倾向于简洁、明了,有时候缺乏示例,并且假设读者已经有了一些这个函数的基本用法的知识。如果你需要一个更加基本的教程,可以用常用的搜索引擎在Internet上找到几个(对于strtok这个例子,你可以参见9.1.4 “strtok的颂歌”)。GNU C库的手册,也很容易在网上找到,对初学者而言是非常易懂的。

  • 如果你无法想起要找的函数的名字,每个手册页都有一个一行长的简述,man -k searchterm将搜索那些简述。许多系统还提供apropos命令,它和man -k类似但是多了一些别的功能。为了进一步的利用,我经常把apropos命令的输出用管道导出给grep命令。

  • 手册分为几段。第1段是命令行命令,第3段是库函数。如果你的系统有一个命令行程序叫作printf,那么man printf将展示这个命令的文档,而man 3 printf将展示C库函数中的printf的文档。

  • 如果想了解更多关于man命令的用法(比如各段的完整列表),可以使用man man。

  • 你的文本编辑器或者IDE可能有某种快速打开手册页的方法。例如,vi的使用者可以把鼠标放在一个词语上,用K键来打来这个词语的手册页。

到此,你应该已经看出编译过程的模式了。

1.设定一个描述编译器选项的变量。

2.设定一个描述链接器选项的变量,包括为你用的所有函数库加上正确的-l选项。

3.用make命令或者你的IDE的操作来把这些变量转换为完整的编译和链接命令。

本章的余下部分将把以上步骤最后做一次,并采取一种非常简短的设置:仅仅用shell。如果你思维敏捷,可以通过摘录一些语句段落到解析器上来学习脚本,你也将可以同样地把C代码贴在你的命令行上。

gcc和Clang有一个用于包含头文件的方便的配置。例如:

gcc -include stdio.h

这和在C源文件的开头放入下面这行命令等价的:

#include <stdio.h>

同样地,也可以用clang -include stdio.h。

通过把以上内容加入到编译器调用,最终我们可以把hello.c程序写成只有一行:

int main(){ printf("Hello, world.\n"); }

通过以下方式顺利编译:

gcc -include stdio.h hello.c -o hi --std=gnu99 -Wall -g -O3

或者利用shell命令:

export CFLAGS='-g -Wall -include stdio.h'
export CC=c99
make hello

这个关于-include的窍门是随编译器而不同的,用于把信息从代码转移到编译指令中,如果你认为这是一个坏习惯,那好,就忘了它吧。

自己动手:为自己写一个通用的头文件,让我们称之为allheads.h,然后把你用过的所有头文件都扔到里面去,那么它看起来会像:

我其实无法告诉你它确切长得什么样,因为我不知道你每天都用什么文件。

现在你有了这个集成的头文件,只需在所有文件的开头加上这么一句:

这样你就不用再操心头文件了。的确,加上这个头文件会扩展出10000行额外的代码,而且其中大多数和手头的程序无关。但是你不会留意到,而且没用的声明也不会改变最终的可执行文件。

请允许我在下面的几段中先跑一下题,去聊聊头文件。一个有用的头文件必须包含类型定义、宏定义、函数的类型声明、宏以及包含这个头文件的代码文件中使用的那些函数。同时,它不应该包含那些代码文件中并没有使用的类型定义、宏定义以及函数声明。

#include <math.h>
#include <time.h>
#include <stdio.h>
#include <unistd.h>
#include <stdlib.h>
#include <gsl/gsl_rng.h>

为了切实满足上面的要求,你需要为每一个单独的代码文件写独立的头文件,这个头文件只包含和代码文件相关的内容。但是没有人真的这样做。

很久以前,编译器处理一个哪怕不算复杂的程序也要花费好几秒甚至几分钟的时间,所以减少一些编译器不得不做的工作,可以产生一些显而易见的好处。我现在的stdio.h和stdlib.h每个都有1000多行(可以用wc -l /usr/include/stdlib.h验证),time.h也有400行,这就意味着下面这个仅7行的小程序,实际上是一个大约2400行的程序。

#include <time.h>
#include <stdio.h>
#include <stdlib.h>
int main(){
    srand(time(NULL));       // Initialize RNG seed.
    printf("%i\n", rand());  // Make one draw.
}
#include <allheads.h>

而今,编译器已经不再认为2400行是一个大问题,这种编译也就花费不到1秒。那么我们为何还要花时间去为一个特定的程序挑选正确的头文件呢?还不如把更多内容放入一个头文件好。

你会看到一个使用Glib的例子,在头部包含了#include<glib.h>。这个头文件包含74个子头文件,覆盖了所有的Glib库的所有方面。Glib团队设计的这个用户界面非常好,因为那些不想花时间去挑选正确的头文件的用户可以只用一行就解决了包含头文件的问题。而那些需要精确控制的人也可以不用这一行,而去74个子头文件中挑选自己需要的。如果C语言的标准库也有这种快速、简单的头文件就好了;这并不是20世纪80年代的风格,但是做一个出来也很简单。

如果你为其他用户写一个公共的头文件,那么按照规则,一个头文件是不应该包含不必要的元素的,你的头文件也许不应该包含用来读入全部标准库的声明和定义的#include "allhead.h"——事实上,有可能你的公共头文件中没有任何公共库的元素。通常来说这是真的:你的库有一个代码片段,使用了Glib的链表,但是这意味着你需要在代码文件中包含#include<glib.h>,而不是在公共的头文件中包含它。

让我们继续讨论在命令行上进行快速编译的话题,有一个统一的头文件可以让你更快地写出程序。一旦你有了一个统一的头文件,即使#include<allheads.h>这一行也是多余的,因为你可以把-include allheads.h加到CFLAGS这个环境变量中,这样你就不需要在你的项目中包含这个文件了。

here文档是POSIX标准shell的一个特性,你可以用在C、Python、Perl或者别的什么语言中,它们也使得这本书更加有用和有趣。并且,如果你想要有一个多语言的脚本,here文档是一个实现的方便法门。在Perl中做解析,在C语言中做算数,然后用Gnuplot产生漂亮的图片,并且集成在一个文本文件里。

这是一个Python的例子。通常,你通过下面的方法告诉Python去运行一个脚本:

python your_script.py

你可以使用“-”以使stdin作为输入文件:

echo "print 'hi.'" | python -

理论上,你可以通过echo把一些长脚本放到命令行上,但是你很快就会看到有很多短小、不符合期望的解析在进行——比如,你可能需要用\"hi\"而不是"hi"。

那么,here文档实际上根本就不会被解析。比如:

python - <<"XXXX"
lines=2
print "\nThis script is %i lines long.\n" %(lines,)
XXXX

现在回到C的世界中来:我们可以用here文档来通过gcc或Clang编译那些贴到命令行的C代码,或者在一个多语言的脚本插入几行C语言程序。

我们不再用makefile,所以我们需要一个单独的编译命令。为了让生活变得不那么痛苦,让我们给它起个别名。把它粘贴到你的命令行,或者把它添加在你的.bashrc、.zshrc中,或者任何适合的地方。

go_libs="-lm"
go_flags="-g -Wall -include allheads.h -O3"
alias go_c="c99 -xc - $go_libs $go_flags"

这里的allheads.h是你之前放在一起的集成的头文件。当你写C代码的时候,用-include选项是你最不应该考虑的事情,并且我也发现当C代码中出现#字符的时候,bash的history命令会受到影响。

在编译行,用'-'取代了文件名,这意味着用stdin而不是从一个命名的文件输入。-xc认为这是C代码,因为gcc代表GNU编译器组合,而不是GNU C编译器,也没有类似.c的输入文件名来提示它,所以我们必须指明这不是Java、Fortran、Objective C、Ada,或者C++(对Clang也一样,即使它的名字是有意在提示是C语言)。

无论你在makefile中对LDLIBS和CFLAGS做了多少定制化,这里仍要做。现在你已经扬帆出海了,可以在命令行中编译C代码了:

go_c << '---'
int main(){printf("Hello from the command line.\n");}
---
./a.out

我们可以使用here文档来粘贴简短的C程序到命令行,并写一点带有争议性的测试程序。不仅是你不需要一个makefile,你甚至也不需要一个输入文件。

不要把这种模式当成你的主要工作状态。但是剪切、粘贴代码段落到命令行是有趣的,并且在一个较长的shell脚本内可以用一步完成C也是非常神奇的。

[1] Cygwin有Red Hat, Inc.运营的项目,该公司也允许用户购买不按照GPL版权发布自己源代码的权利。

[2] 虽然MinGW有一个包管理器,可以安装一些基本的系统和一些库(大部分库都是MinGW自己需要的)。和一些其他包管理器相比,这些包的数量与典型的包管理器提供的上百个包相比显得很苍白。事实上,Linux包管理器提供的编译完的库比MinGW包管理器提供的多。写作本书的时候是这样,不过当你阅读本书的时候,有些用户也许向MinGW添加了更多的包了。

[3] 你可以在任何POSIX标准系统中试一下find / -type f | wc –l,用来得到一个粗略的文件数。


停止另一场比赛,重新开始—

——选择Bob Dylan在1965年新港民谣音乐节巡演闭幕式的歌曲

“It’s All Over Now Baby Blue(它到处都是,淡蓝色)”

你可能会大叫:等等!作者你可说过,我可以用库来使自己的工作更轻松,我在自己的领域已经是一个专家,我四处搜罗,但是我还是没有发现一个适合我用的库!

如果这个人是你,那么现在是时候揭秘我写这本书的秘密企图了:作为一个C用户,我希望有更多的人来写出优秀的库为我所用。如果你已经读完了本书,你就知道如何基于其他库来写出现代意义上的C代码,如何知道写出一套围绕几个简单的对象的函数,如何使得接口用户友好,如何撰写代码文档方便地测试,有哪些工具可用,如何用一个Git版本库,以便这样别人也能提交工作,以及如何用Autotools为一般大众打包工作。C是现代计算的基础,所以当你用C解决了某个问题的时候,这个解决方案就可以被世界各地的各种平台所利用。

Punk Rock是一种自力更生的艺术形式。一个共识是,音乐由我们自己创造,你并不需要某个公司的审查委员会的许可就能写点什么新的东西并向世界发行。事实上,我们已经具备了所有的工具,剩下的只是让梦想成真。


Alignment(对齐)

一种对数据的元素必须在一定边界处开始的要求。例如,给定一个8位的对齐要求,一个结构如果有一个1位的char变量,后面跟着一个8位的int,则需要在char之后有7位的填充,这样int就是从一个8位的边界上开始了。

ASCII

美国信息交换标准代码(American Standard Code for Information Interchange)。一个把英语本身的字母对应0~127的数字的映射。提示:在很多系统中,man ascii将打印出代码表。

Automatic allocation(自动分配)

对于一个自动分配变量,它在内存中的空间是在这个变量被声明的时候由系统分配的,并且在它的范围之外被移除。

Autotools

一系列来自GNU的、用于简化在任何系统中的编译过程的程序,包括Autoconf、Automake和Libtool。

Benford’s law

在一个大数据集的首数字倾向于一个类log的分布:1出现的概率为30%,2大约是17.5%……,9大约是4.5%。

Boolean(布尔值)

即真/假。以Geoge Boole—一位生活在19世纪早期到中期的英国数学家的名字命名。

BSD

伯克利软件发行版(Berkeley Software Distribution)。一个POSIX实现。

callback function(回调函数)

一个函数(A)被作为输入发送给另一个函数(B),这样函数B可以通过它自己的操作来调用函数A。例如,泛化的排序函数通常是用一个输入函数来比较2个元素。

call graph

一个用来显示函数调用与被调用关系的方块-箭头图。

Cetology

对鲸类(海洋哺乳类)的研究学科。

compiler(编译器)

正式地说,就是把一个程序从(人类可阅读的)文本转化为(人类难以辨认的)机器指令的程序。一般被用来指代预处理器+编译器+连接器。

debugger(调试器)

一个需要和编译好的程序互动地执行的程序,使得用户可以暂停程序,检查和修改变量值,等。一般对理解bug有用。

deep copy(深度复制)

一个含有指针的结构的复制,其跟踪所有的指针并对这些指针所指向的数据做出复制。

encoding(编码)

将人类语言字符转换为计算机可处理的数字代码的方法。参见ASCII、 multibyte encoding和wide- character encoding。

environment variable

一个代表某个程序的环境的变量,由其父程序(一般是shell)来设定。

external pointer

参见Opaque Pointer。

floating point(浮点数)

一种类似于科学记数法的表述数字的方法,类似2.3×104,具有一个指数部分(本例中是4)和一个尾数部分(本例中是2.3)。在写下尾数之后,把指数部分想象为允许小数点漂浮到正确的位置上。

frame(帧)

堆栈中的空间,其中存放函数的信息(例如输入参数和自动变量)。

gdb

GNU调试器。

global(全局)

当一个变量的作用域是整个程序,它就是全局的。C实际上并不存在全局作用域,但是如果一个变量处于可以被这个程序的所有代码文件所包含的头文件中,它实际上就相当于一个全局变量。

glyph

一种用于书面交流的符号。

GNU

代表Gnu’s Not UNIX。

GSL

代表GNU科学计算库。

heap(堆)

用于手动分配内存的内存空间。可与stack(堆栈)对比。

IDE

即集成开发环境(Integrated Development Environment)。通常是基于一个图形界面的文本编辑器,并带有编译、调试和其他服务于程序员的特性的工具。

integration test(集成测试)

一项运行一系列的步骤以测试一个代码库中的多个部分的测试(每个部分应该有自己的单元测试)。

library(库)

基本上,一个没有main函数的程序,因此它是为其他程序所准备的一系列的函数、typedef和变量。

linker(连接器)

一个程序,用于把一个程序的各分散的部分(比如独立的目标文件和库)连接起来,并调整对外部目标文件函数和变量的引用。

Linux

技术上讲,是一种操作系统内核,但是一般用来指代一个与完整的BSD/ GNU/ Internet Systens Consortium/ Linux/ Xorg/等捆绑的所形成的统一的包。

macro(宏)

一个(通常是)较短的文字段落,用于替代(通常是)较长的文字。

manual allocation(手工分配)

应程序的请求,用malloc或者calloc在heap(堆)中分配一个变量,并应用户的要求用free释放。

multibyte encoding

一种用可变数量的字符来代表单字符人类语言的编码方法。可与wide-character encoding对比。

mutex(互斥锁)

mutual exclusion的缩写,一个用来确保在同一时刻只有一个线程可以使用资源的数据结构。

NaN

即Not-a-Number。IEEE 754(浮点数)标准定义为数学上不可能的计算的输出,比如0/0或者log(-1)。经常被用于作为缺少数据或坏数据的标志。

object(对象)

一个关联了对其操作的函数的数据结构。理论上讲,对象封装了一个概念,为其他代码与该对象互动提供了有限的入口点。

object file(目标文件)

一个包含机器可阅读指令的文件。通常是对源代码文件运行编译器的结果。

opaque pointer(不透明指针)

一个指向数据的指针,处理该指针的函数不能读到指针本身,但是被传递到其他的函数中后可以读到数据。一个脚本语言的函数可以调用一个返回指向C侧数据的不透明指针的C函数,在脚本语言中后来的函数可以用那个指针来操作C侧的数据。

POSIX

指代The Portable Operating System Interface。一个类UNIX操作系统遵从的IEEE标准,描述了一系列的C函数、shell和一些基本工具。

preprocessor(预处理器)

概念上说,一个程序在编译器紧前运行的程序,执行类似#include和#define这样的指令。在实践中,通常是编译器的一部分。

process(过程)

一个运行中的程序。

profiler(优化器)

一个用来报告你程序中的运行时间花费在何处的程序,这样你在提速优化的时候就知道应该聚焦在哪里。

pthread

POSIX线程。一个用在POSIX标准中定义的C语言线程接口产生的线程。

RNG

随机数产生器,其中“随机”基本上意味着你可以合理地期望一个序列的随机数不与任何另外的序列有系统性的关联。

RTFM

即“请读手册(Read the manual)”的缩写。

Sapir-Whorf假说

关于我们所说的语言决定着我们具有的思维能力的假说。简单地说,我们经常用词汇思考,这是显然的;复杂地说,我们不能想象也无法由语言描述的事物,这显然不对。

scope(作用域)

一个变量被声明和可使用的代码范围。好的代码风格都会尽量缩小变量的作用域。

script(脚本)

一个用可解释的语言,比如shell,编写的程序。

segfault

段错误(segmentation fault)。

segmentation fault

你正在接触为你的程序分配的内存之外的内存。

SHA

安全哈希算法(Secure Hash Algorithm,SHA)。

shell

一个支持用户与操作系统互动的程序,可以是命令行或者脚本的形式。

SQL

结构化查询语言(Structured Query Language)。一种与数据库交互的标准方法。

stack(堆栈)

函数执行发生的内存空间。特别是,自动变量被放在这里。每个函数都得到一个帧,而且每次一个子函数被调用,它的帧在概念上讲是被堆放在调用它的函数的帧的上方。

static allocation(静态分配)

使得变量或者文件范围的方法,其中变量在函数中由static关键词分配。分配发生在程序执行之前,而且直到程序退出,变量一直存在。

test harness(测试工具)

用来运行一系列的单元测试和整合测试的环境。提供简单的辅助结构的配置/释放,允许检查可能(正确地)引发主程序的崩溃。

thread(线程)

计算机独立于其他线程运行的一系列的指令。

token(标识)

被当作语法单元的一组字符,比如一个变量名、一个关键字,或者一个类似*或+的操作符。解析文本的第一步就是把它打散为token;strtok_r和strtok_n就是为此设计的。

type punning(类型转换)

把一个变量从一种类型转化为另一种类型,从此强迫编译器将这个变量按照第二种类型处理。例如,假设struct {int a; char *b;} astruct,那么(int) astruct就是一个整数(但是为了安全地转换,参见11.1.2“C,更少的缝隙”)。经常是无法移植的;通常是不好的形式。

type qualifier(类型修饰符)

一个对于编译器应该如何处理一个变量的描述符。与变量的类型(int、float等)无关。C中唯一的类型修饰符是const、restrict、volatile和_Atomic。

union(联合)

可以被理解为多种类型的一块内存。

unit test(单元测试)

一段用来测试代码库中某段代码的代码。对比参见integration test。

UI

即用户界面。对于C语言的库来说,需要提供用户使用这个库时与之兼容的类型定义、宏定义和函数声明。

UTF

即Unicode Transformation Format。

variadic function

输入参数的数量可变的函数(例如,printf)。

wide-character encoding

一种文本编码方式,其中每个人类语言字符都由一个固定长度的char类型表示。例如,UTF-32就明确每个Unicode均用4字节表示。对每个人类语言字符用多个字节表示,但是这个定义又与multibyte encoding不同。

XML

即扩展标记语言(Extensible Makeup Language)。


自从于加州理工学院获得社会科学博士后,Ben Klemens就一直从事统计分析和人口的计算机辅助建模工作。他的观点是,写代码一定应该是趣味横生的,并先后非常愉快地为布鲁金斯学会、世界银行、美国国家精神健康中心等机构写过分析和建模代码(主要是C代码)。他作为布鲁金斯学会的非常驻研究员,与自由软件基金会一道,做了很多工作来确保有创意的程序员拥有保留其作品使用权的权利。他目前为美国联邦政府工作。


本书封面上的动物是斑袋貂(Spilocuscus maculatus),一种生活在澳大利亚、新几内亚和附近小岛上热带雨林和红树林中的有袋目哺乳动物。它的头部呈圆形,耳朵小而不明显,皮毛厚实,有一条有助于攀援的可卷曲的尾巴。卷曲的尾巴是斑袋貂的典型特征,尾巴上部贴近身体的部分覆盖着绒毛,而下半部分的内侧覆盖着粗糙的鳞甲,适合缠住树枝。它的眼睛可以看到黄色、橙色及红色,视野像蛇一样狭窄。 斑袋貂通常非常胆小,所以很少被人发现。它是夜间活动的,在夜间捕猎和进食,白天在树枝上自己搭建的巢穴中睡眠。它行动缓慢,有时很懒——因此经常被误认为是树懒、负鼠,甚至是猴子。 典型的斑袋貂是独居动物,独自筑巢和进食,与其他个体特别是具有竞争性的雄性之间的互动常是有侵略性和冲突性的。雄性斑袋貂以气味标识它们的领地以警告其他雄性,从它们的身体和气味排泄腺散发出具有穿透性的麝香气味。它们在树枝上散布唾液以警示出现在它们领地上的其他个体。如果它们在自己的领地上遇到其他的雄性个体,它们会发出吠叫、咆哮和嘶叫的声音,并且直立起来保护它们的领地。 斑袋貂具有非特异性的牙齿排列方式,使得它们可以食用多种植物。已知它们可以吃花朵、小动物,偶尔也吃蛋。斑袋貂的天敌包括蟒蛇和一些掠食性鸟类。

异步社区(www.epubit.com.cn)是人民邮电出版社旗下IT专业图书旗舰社区,于2015年8月上线运营。

异步社区依托于人民邮电出版社20余年的IT专业优质出版资源和编辑策划团队,打造传统出版与电子出版和自出版结合、纸质书与电子书结合、传统印刷与POD按需印刷结合的出版平台,提供最新技术资讯,为作者和读者打造交流互动的平台。

我们出版的图书涵盖主流IT技术,在编程语言、Web技术、数据科学等领域有众多经典畅销图书。社区现已上线图书1000余种,电子书400多种,部分新书实现纸书、电子书同步出版。我们还会定期发布新书书讯。

社区内提供随书附赠的资源,如书中的案例或程序源代码。

另外,社区还提供了大量的免费电子书,只要注册成为社区用户就可以免费下载。

很多图书的作译者已经入驻社区,您可以关注他们,咨询技术问题;可以阅读不断更新的技术文章,听作译者和编辑畅聊好书背后有趣的故事;还可以参与社区的作者访谈栏目,向您关注的作者提出采访题目。

您可以方便地下单购买纸质图书或电子图书,纸质图书直接从人民邮电出版社书库发货,电子书提供多种阅读格式。

对于重磅新书,社区提供预售和新书首发服务,用户可以第一时间买到心仪的新书。

用户帐户中的积分可以用于购书优惠。100积分=1元,购买图书时,在里填入可使用的积分数值,即可扣减相应金额。

特别优惠


购买本电子书的读者专享异步社区优惠券。 使用方法:注册成为社区用户,在下单购书时输入“57AWG”,然后点击“使用优惠码”,即可享受电子书8折优惠(本优惠券只可使用一次)。

社区独家提供纸质图书和电子书组合购买方式,价格优惠,一次购买,多种阅读选择。

您可以在图书页面下方提交勘误,每条勘误被确认后可以获得100积分。热心勘误的读者还有机会参与书稿的审校和翻译工作。

社区提供基于Markdown的写作环境,喜欢写作的您可以在此一试身手,在社区里分享您的技术心得和读书体会,更可以体验自出版的乐趣,轻松实现出版的梦想。

如果成为社区认证作译者,还可以享受异步社区提供的作者专享特色服务。

您可以掌握IT圈的技术会议资讯,更有机会免费获赠大会门票。

扫描任意二维码都能找到我们:

异步社区

微信订阅号

微信服务号

官方微博

QQ群:436746675

社区网址:www.epubit.com.cn

官方微信:异步社区

官方微博:@人邮异步社区,@人民邮电出版社-信息技术分社

投稿&咨询:contact@epubit.com.cn



相关图书

代码审计——C/C++实践
代码审计——C/C++实践
C/C++代码调试的艺术(第2版)
C/C++代码调试的艺术(第2版)
大规模C++软件开发 卷1:过程与架构
大规模C++软件开发 卷1:过程与架构
C/C++程序设计竞赛真题实战特训教程(图解版)
C/C++程序设计竞赛真题实战特训教程(图解版)
C/C++函数与算法速查宝典
C/C++函数与算法速查宝典
C程序设计教程(第9版)
C程序设计教程(第9版)

相关文章

相关课程