C程序设计新思维(txt+pdf+epub+mobi电子书下载)


发布时间:2020-08-12 00:14:37

点击下载

作者:[美]克莱蒙(Ben Klemens)著

出版社:人民邮电出版社

格式: AZW3, DOCX, EPUB, MOBI, PDF, TXT

C程序设计新思维

C程序设计新思维试读:

前言

Is it really punk rockLike the party line?它真的是朋克摇滚么,就像政治路线?——选自Wilo的歌曲“Too Far Apart(遥远)”C就是Punk Rock[1]

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

这门语言已经有40年的历史了,可以说已经进入了中年。创造它的人是少数对抗管制的人,遵从完美的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音乐人。对这种持续的进化,一种反应是画一条界限,将原来的风格称为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精神的书。我对将本书的代[2]码和1978年Kernighan和Ritchie出版的书中的C标准进行对比毫无兴趣。既然连我的电话机都有512M字节内存,为什么还在我的书里花费章节讲述如何为可执行文件减少几K的字节呢?我正在一个廉价的红色上网本上写这本书,而它却可以每秒运行3 200 000 000条指令,那为什么我还要操心8位和16位所带来的一个操作的差异呢?我们更应该关注如何做到快速编写代码并且让我们的合作者们更容易看懂。毕竟我们是在使用C语言,所以我们那些易读但是并没有被完美优化的代码运行起来还是会比很多繁琐的语言明显地快。[3]Q&A(本书的参考引用)

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

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

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

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

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

你最好有一定的C语言基础,但是没有必要特别精通——既然我将描述那些细节,如果你从来没有学过它们也许更好。如果你是白纸一张,只是对C语法充满敬意,那它还是非常简单易学的,而且你也可以用搜索引擎找到很多在线教材;如果你有其他语言的经验,用一两个小时就可以基本掌握。

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

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

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

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

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

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

A:C语言在过去的20年里真的进化了很多。就像我下面要讨论的那样,像“要支持所有C编译器”等必要的工作准则也变化了不少,因为自从ANSI标准发布后又发布了两个新的C语言标准。也许你应该读一下第10章,找找有什么能叫你感到惊讶的。

并且,开发环境也进化升级了。Autotools已经改变了代码发布的方式,意味着可以更加可靠地调用其他的库,意味着我们的代码可以花费更少的时间在重建常用结构和函数上,而是更多地调用本书下面将讨论的库。

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

A:的确。良好的C实践需要具备精良的C工具。如果你没有使用调试器(独立的或者集成在你的IDE中),你就是在自讨苦吃。如果你告诉我追踪内存的泄露是不可能的,那么就意味着你还没有听说过Valgrind,一个用来确切地指出是哪一行产生内存泄漏并发生错误的系统。Python及其附带工具有内建的包管理器;而本书将告诉你,属于C的、事实上的跨平台打包系统,即Autotools,它是一个独立的系统。

如果你在使用一个不错的集成开发环境(IDE)作为这些工具的调用界面,你仍然可以从了解“IDE如何处理环境变量以及其他隐藏的细节,同时还能为你处理错误抛出”等问题中受益。

Q:你所谈论的一些工具有点老了。难道没有更现代的工具能替代这些基于shell的工具么?

A:如果我们嘲笑那些仅仅因为事物是新的就对其抵制的人,那么我们也没有理由仅仅因为事物是旧的就加以抵制。

其实很容易找到可靠的来源证明第一件六弦的吉他出现在1200年左右,第一个四弦的小提琴出现在大约1550年,带键盘的钢琴出现在1700年左右。有趣的是,你今天听到的多数(如果不是全部)音乐都与以上乐器中的某种有关。Punk rock当初并没有拒绝吉他,只不过用得更加有创造性,比如将吉他的输出接连到新的滤波器上。

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

A:的确。在 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的书籍的第二版包含了这个语言的完整规格,也就是说在几万名程序员的桌子上都有这个标准的印刷版[Kernighan,1988]。1990年ANSI标准被ISO基本接受,没有做重大的改变,但是人们似乎更喜欢用ANSI 89这个词来称呼这个标准(或者用来做很棒的T恤衫标语)。

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

在此期间,C++分离出来并大获成功(虽然也不是那么大)。C++是C身上发生的最好的事情了。当所有其他的语言都在试图添加一些额外的语法以跟随面向对象的潮流,或者跟随其作者脑袋里的什么新花招的时候,C就是恪守标准。需要稳定和可移植性的人使用C,需要越来越多特性以便可以像在百元大钞里洗澡一样无休止地沉溺于其中的人使用C++,这样每个人都很高兴。

ISO C99

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

当你想着单单C89的影响就有那么大,以及全球是如何运行C代码时,你很难想出ISO能够拒绝某种新事物而不被广泛批评——甚至拒绝做出改变也会被唾骂。的确,这个标准是有争论的。有两种常用的方式来表达一个复数(直角坐标和极坐标)——那么ISO会采用哪一个?既然所有的好代码都没采用变长的宏输入机制来编写,为什么我们还需要这个机制?换句话说,纯洁主义者批评ISO是在背叛那些为C语言增加更多特性的趋势。

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

C11

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

我是在2012年写的这本书,就在C11标准在2011年12月发布之后的不久,此时已经有了一些来自编译器和库的支持。

POSIX标准

事物的规律就是这样,伴随着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’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许可部分的版权的同时,保持自己工作的版权,但是不要求你再发行那些BSD许可的源代码。[例如Libxml2,就是带有与BSD许可类似的MIT许可证。]注释

本书使用的排版约定

本书使用如下排版约定:

斜体(italic)

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

等宽字体()

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

等宽黑体()

用于表示需要用户逐字符输入的命令或其他文本。

等宽斜体()

用于表示应该以用户提供的值或根据上下文决定的值加以替换的文本。这个图标代表诀窍、建议和一般性的说明。自己动手:这里是一个练习,帮助你从实践中学习,并给你一个把手放在键盘上的指标。这个图标表示警告或错误。

使用示例代码

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

本书中用到的示范代码可以在以下地址找到:http://excamples.oreilly.com/063620025108/。▶▶第一部分开发环境

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

当你编译自己的程序的时候,如果在使用外部库的时候感觉不是很方便,就可能很快失去兴趣。虽然这并不难,但你还是要学学如何去做。幸运的是,你面前已经有了这本书。

本书第一部分的内容简述如下:

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

第2章介绍调试、文档管理和测试工具,因为直到调试、编档和测试的时候才能看到一段代码的好处。

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

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

其他的语言也是现在C语言开发环境的重要因素,因为太多的语言提供了C接口。第5章提供了一些如何编写这些接口的建议,并给你一个基于Python的扩展例子。第1章 准备方便的编译环境小心啦,亲爱的,因为我要用点技术。——选自Iggy Pop的歌曲“Search and Destroy(寻找与毁灭)”

仅仅有C标准库是不足以成就大事的。

实际上C的生态环境已经扩展到了C标准之外,也就是说,如果你想顺利完成本书的练习(即“自己动手”),你就必须了解如何轻松调用那些常用却并非ISO标准的库。不幸的是,这一点恰恰被多数教科书所忽略掉,而需要你自己去寻求解决办法。这也是为什么你发现C的诽谤者们会告诉你一些自相矛盾的言论,比如C已经有40年的历史了,所以你必须自己完成程序的每个部分——他们恐怕根本从来没真正连接过一个库。

本章概要如下:● 安装必要的工具。比起那些必须自力更生地寻找每个组件的黑暗

时代,现在我们身处的环境要好多了。只要10到15分钟(外加

下载这些好东西的时间)你就可以安装好一个完整的开发环境。● 如何编译一个C程序。你其实已经知道了怎么做这事儿,但是我

们需要一个可以调用库和它们的位置路径的配置;仅仅输入cc

myfile.c并不会解决这个问题。make命令是用来提供用于编译程

序的最简单的系统,因此它提供了一个好的讨论模型。在这一部

分我将展示给大家一个“最小巧但可用”的makefile,并提供了

扩展的空间。● 无论我们用什么系统,我们都会基于组来讨论环境变量,讨论这

些环境变量是做什么的,以及如何配置它们。而当我们准备好了

所有的编译机制之后,就会面临一个最容易的环境配置调整问

题,即添加新的库。● 作为一个附加内容,我们可以用至此学习的知识,准备一个相对

简单的编译系统,并将允许我们剪切和粘贴代码到命令行。

针对IDE用户,有一点需要特别说明:你可能不会去使用make命令,但并不因此就说这一章和你没有关系。因为在任何编译方法中make命令都会被执行,所以IDE环境也有一个类似的方法。如果你知道make命令是如何工作的,将更加容易去配置IDE环境。1.1 使用包管理器

嘿,老兄,如果你还没有在使用包管理器的话,那你可真的错过了好东西。

有几个原因促使我讲述包管理器。

首先,你们中的一些人可能连基本的包管理工具都没有安装。为了这部分读者,我把这部分放在本书的最开始,因为你们需要尽快得到这些工具。一个好的包管理器将为你快速安装POSIX子系统、任何你听说过的语言的编译器、游戏、常用的办公室效率工具,以及几百种C库,等等。

其次,作为一个C程序员,包管理器是我们用来安装常用工作库的关键手段。

再次,当你写过一定的代码,你将开始渴望发布你的代码,从而完成一个从包的下载者到包的贡献者的嬗变。本书将送你一程,为你讲解怎么为自动安装过程准备安装包,这样当一个包库的管理者决定在包库中包含你的代码的时候,他也能顺利地制作最终的包。

如果你是一位Linux用户,你的计算机已经带了包管理器,而且你已知道使用它获取软件是多么容易。对于Windows用户,我将详细讲解Cygwin。Mac用户有几种选择,比如Fink和Macports。所有的Mac下的包管理器都依赖于Apple的Xcode包,也就是一般在系统安装光盘中所使用的(或者含有安装程序的目录,视情况而定),或通过注册为Apple的开发者而得到。

有了包管理器,那么我们需要什么包呢?这里有一个常见的列表。因为每个系统有不同的组织模式,他们中的一些可能也是以不同的方式组合起来的、作为基本包默认安装的,或者有着奇怪的名字。如果你对一个包不是很确定,不妨下载下来安装一下,毕竟系统由于安装了太多的软件包导致不稳定或者变慢的时代已经过去。不过,你可能没有足够的带宽(或者甚至没有足够的硬盘空间)来安装每个可以找到的包,所以还是需要一点判断和选择的。要是你发现错过了什么东西,随时可以折返回来寻找。必须准备的包如下。● 一个编译器。gcc是必须安装的;也可能需要Clang。● gdb,一个调试器。● Valgrind,以便测试你的C内存使用错误。● gprof,一个代码剖析器。● make,这样你就再也不用直接调用编译器了。● pkg-config,用来寻找库。● Doxugen,用来产生文档。● 一个文本编辑器。理论上说你可以有几百种选择。这里,笔者推

荐几种:

——Emacs和vim是那些死硬的极客们喜欢的工具。Emacs是包

罗万象的(E代表extensible,扩展);vim则相对采取最简化主义

思路并且对键盘输入者比较友好。如果你觉得自己可能会花费上

百小时盯在一个文本编辑器上,那么花点时间了解一下它们是值

得的。

——Kate的界面是友好而吸引人的,还提供了一些程序员们所

期望拥有的便利工具,比如语法高亮检查。

——最后建议你试一下nano,该工具非常简约,并且是基于文[1]

本界面的,以至于你连一个GUI界面都不需要。● 如果你钟情于IDE,那就安装一个——甚至可以同时安装几个。

这方面你同样有很多选择:

——Anjuta:属于GNOME项目。与GNOME的GUI工具Glade可

以兼容工作。

——KDevelop:属于KDE项目。

——Code::block:相对简单,在Windows环境下运行。

——Eclipse:简直就是有很多杯托和把手的豪车。并且是跨平

台的。

还有一些软件你应该安装,这些工具在随后的几章里我们会频繁用到:● Autotools:Autoconf、Automake、libtool● Git● shell的替代品,比如Z shell。

当然,还有C库这些库可以使你免于“重新发明轮子”的麻烦(或者,用更加精确的比喻:重新发明火车头)。你可能想要更多的库,但是本书所使用的一般也就限于如下几种:● libcURL● libGlib● libGSL● libSQLite3● libXML2

C库包的命名机制并没有统一标准,所以你必须弄清楚你的包管理器把一个库打进子包的习惯。典型的习惯是,一方面为最终用户准备一个包,另一方面同时为将要在自己程序里使用这些库的开发者也准备一个包;所以要确认选定了基本包和-dev或-devel的包。有的系统把文档也分散进一个单独的包中。也有的要求你单独下载调试符号表。而且一旦需要,gdb工具将在第一次运行并在发现你缺少调试符号表的时候引导你逐步完成。

如果你正在使用POSIX系统,那么完成前面的工具安装后,你已经拥有一个完整的开发系统,可以进入编程阶段了。对于Windows用户,我们将简短说明一下这个安装的系统是如何与Windows主系统互动的。1.2 在Windows下编译C

在多数系统中,C享有一个中心的、贵宾礼遇的地位,以至于所有其他工具都处于从属的地位;但是在Windows机器中,C语言却被奇怪地忽略了。

所以我不得不花点时间讨论如何来准备好一台Windows机器以便用来写C程序。如果你现在不需要在Windows机器上编程,你可以直接跳到“1.3库的路径”。

这并非是针对Microsoft的,请不要用这样的角度来理解这一节。我无意去推测Microsoft的动机和商业战略。不过,如果你想在Windows机器上用C来工作,你需要知道实际状况(坦白地说,是不太友好)以及如何应对。

1.2.1 Windows中的POSIX环境

因为C和Unix是共同进化的,很难在谈到其中一个的时候不提及另一个。我个人认为从POSIX开始会比较容易些。并且,读者中那些希望在Windows机器上编译来自其他环境的代码的人会发现这么讲述也很自然。

总的来说,文件系统的世界可以(稍微有点重叠地)分为两个阵营:● POSIX兼容系统● Windows操作系统族

POSIX兼容并不一定意味着整个系统的外观和使用习惯都和Unix机器一样。比如,典型的Mac用户并不会意识到他们正在用一个界面漂亮的标准BSD系统,但是知道这一点的用户可以到Accessories->Uilities目录中,打开Terminal程序,并为他们心仪的内容运行ls、grep和make。

说实话,我怀疑很多系统是不是真的100%符合标准的要求(比如要具备一个Fortran′77编译器)。为了我们的目的,我们需要一个可以像POSIX shell那样运行的shell程序、一堆工具(sed、grep、make等)、一个C99编译器,以及一些标准C库之外的附加库,比如fork和iconv。这些都可以被添加进主系统。包管理器相关的脚本、Autotools和几乎所有的编写可移植代码的工具都在某种程度上依赖于上面这些工具,所以即便你不想整天盯着命令行,安装那些工具也是会带来一些方便的。

在服务器级的操作系统和Windows 7的全功能版本中,Microsoft提供了一个叫作SUA的子系统(Subsystem for Unix-based Application)——该系统以前称为INTERIX,用来提供通常的POSIX系统调用、Korn shell,以及gcc。这个子系统一般并不是默认提供的,但是可以作为一个插件安装。但是在其他的Windows版本,包括Windows 8上,并不提供SUA,所以我们不能依赖Microsoft为它自己的操作系统提供一个POSIX子系统。

既然这样,可以尝试Cygwin。

如果你计划从头编译安装Cygwin,你需要按照下面的步骤进行:

1. 写一个Windows下的C库,在该库中提供所有的POSIX函数。这需要你抹平一些Windows和POSIX系统之间的差异性,比如Windows中使用明确的盘符(例如:C:),而POSIX采用一个统一的文件系统。在这种情况下,需要为C:与 /cygdrive/c,D:与 /cygdrive/d等等路径表达之间建立别名。

2. 然后你就可以通过连接这个库来编译POSIX标准的程序了,可以尝试:制作一个Windows版本的ls、bash、grep、gcc、X、rxvt、libglib、perl、python,等等。

3. 一旦你已经编译了几百个程序和库,就需要安装一个包管理器以便用户可以从中选择需要安装的组件。

但作为Cygwin的用户,你所要做的只是从Cygwin的官方网站(http://cygwin.com/第2章 调试、测试和文档我爬上你的窗台,我看起来很困惑其实只是在等待……去完成心中的诡计。——选自Wire乐队的歌曲“I Am the Fly(我是苍蝇)”

本章讲述用于调试、测试和编写文档的工具——这些工具是保证你的代码从“或许有用的草稿”到“大家可以依赖的成果”的要诀。

C给了你对内存做蠢事的自由,调试过程意味着不仅要针对程序逻辑问题例行检查(用gdb),还要检查包括内存滥用和泄漏等更加技术的问题(用Valgrind)。在文档编写方面,本章讲述了一个在可用于接口级文档管理的工具(Doxygen),以及另外一个帮助你针对程序的每一步归档和开发的工具(CWEB)。

本章还概要介绍了测试工具,可以帮助你为自己的代码快速生成很多测试。所有的测试都完成了,就不会有什么问题发生;不过本章还是包含了一些关于错误报告的内容。2.1 使用调试器

先给大家一个简洁明了的关于调试器的提示:

永远使用调试器。

可能有些读者认为这算不上什么提示,难道还有人真的不用调试器么?但是很多从其他语言转来的用户习惯了刚出错就抛出一个回溯信息,他们也从来没有把读到C教科书介绍调试器的那部分(这部分一般是在类似“其他话题”的章节,说不定已经是“第15章”了)。那么现在我们都在同一个讨论的起点上了:调试器是存在的。[1]

另外,我发现有些人担心,bug包括了我们理解上的比较宽泛的错误,而调试器只能给出来自底层的状态变化和回溯的信息。的确,当你用调试器精确定位了一个bug,是值得花时间去探讨你所发现的问题和理解错误的根源,以及这个bug是否会在代码中重现。一些死亡证明会包括对于死因的大胆判断:检验对象的死因为   ,死因为   ,死因为   ,死因为   ,死因为   。当你利用调试器建立了对自身代码的更深刻的理解之后,你可以把这些理解用在更多的单元测试中。

注意前面所说的“永远”:在一个调试器的环境下运行程序实际上不会产生什么代价。调试器也不是程序出错崩溃的时候才跳出来的[2]东西。Linus Torvalds解释说:“我一直都用gdb……把它作为可以破[3]解程序编码意图的一个强化版的反编译器来使用。”你会觉得下面这些操作很不错:可以在任何地方暂停下来;利用一个快速的print verbose++来提升信息输出级别;通过print i=100和continue来强制一个像for (int i=0; i<10; i++)的循环超出范围运行;或者通过插入一系列的输入来测试一个函数。那些交互式语言的粉丝这么做多少是正确的,因为与代码互动将持续地改进你的开发过程;他们只是从来没有开始读C语言教科书的调试器那一章,因此也从来没有意识到那些互动的习惯其实也适用于C语言。

不管你的意图如何,你总归需要那种编译到程序里、对任何调试者都有用的人类可理解的调试信息(变量名和函数名)。为了包含调试符号,可在编译器开关(即CFLAGS变量)中加入-g选项。不使用-g选项其实是没什么理由的——它并不会降低你的程序运行速度,并且你的可执行文件所增加的1K字节在多数情况下也没什么问题。

本章只介绍GDB调试工具,因为在多数POSIX系统中,这是你[4]“唯一的选择”。你可能是从一个IDE中工作的,或者一个你每次点击“run”菜单就可以在GDB下运行你的程序的可视化前端。这里我会介绍GDB命令行中的命令,相信你在把这些命令转换成屏幕上的鼠标操作时不会有什么困难。依靠这些前端,你可以使用.gdbinit定义的宏。

使用GDB工作时,你可能需要一个文本编译器来显示代码。简单的GDB/编辑器组合就已经可以带来一个IDE所能提供的很多便利,足够你使用的了。堆栈帧为了启动程序,我们要求系统运行一个叫做main的函数。此时计算机会产生一个帧,并把这main函数的信息放在里面,比如输入参数(对于main函数习惯上被命名为argc和argv)和函数产生的变量。假设,在main的执行过程调用另外一个函数get_agents。main的执行就会停止了,并且为get_agents产生一个新的帧,用来存放各种细节和变量。也许get_agents又调用另一个函数agent_address,这样我们得到了一个不断增长的堆栈帧。最终,agent_address结束执行,此时堆栈将被弹出而get_agent函数继续执行。那么,如果你的问题只是“我在哪里?”,简单的回答是看源代码中的行号即可,有时这也正是你所需要的。但是更多的情况下,你可能会问“我怎么走到这里的?”,这个问题的答案就是回溯,也就是一个堆栈帧的列表。这里是一个使用backtrace命令的示例:堆栈帧的顶端是0帧,到main,已经是2帧(这个值将随着堆栈的增长和缩减而变化)。帧号后的十六进制数是一个地址,被调用函数返回的时候程序执行将返回的地址;我总是把它们当作视觉噪音而忽略。地址后面是函数名、它的输入参数(恰好argv也是一个十六进制地址),以及当前执行到的源代码行号。如果你发现agent_address列出的“房子”明显是错的,那么也许agent_number输入也可能是错的,此时你不得不跳到1帧去查看是怎样的get_agents状态导致把agent_address设定了一个奇怪的状态。很多分析程序的技巧就是跳入堆栈中,从一个个的函数的堆栈帧中追踪因果关系。

我们可以拿任何在main函数下至少有一个函数的程序(这样你就会有一个平常的堆栈)来实验GDB,如果你手头没有现成的,可以试一下“12.5 libxml和cURL”中的《纽约时报》头条下载程序。假设可执行文件叫做nyt_feed,则可以在shell命令行中,通过gdb nyt_feed来启动GDB。这样就进入GDB命令行,然后我们可以尝试任意下面的操作:● 将程序在某处暂停。

—break get_rss或者break nyt_feed.c:105,或者你已经在

nyt_feeds.c,用break 105。这些将使程序在执行105行之前停下

来。

—用info break来列出所有的断点。

—关掉一个断点,比如disable 3(这里3是你从info break中得到

的断点号),随后可用enable 3来重新打开它。如果你有很多断

点集合,直接使用disable可以把它们全部关掉,然后可以在需要

的时候把他们中的一两个打开。

—用del 3来彻底删除一个断点。

—当然,没有开始就没有结束;用run命令来运行你的程序,如

果需要命令行参数,可以把它们放在这里:run arg1 arg2。● 获得一个函数中所有变量的当前值。

—假设一个叫url的变量,用print url,或者更简单一些,用p

url。

—对于所有的函数输入,可以用:info args;对于所有的本地函

数,可以用:info local。● 跳入一个父函数,并检查其中的变量值。可先通过backtrace或bt

得到堆栈帧列表,然后用frame 2或f 2来跳到2帧。● 单步通过暂停点,每次一行代码。通常用snuc这几个命令中的一

个,但是还有几个别的选择:

—s:step单步执行一行,甚至包括进入另一个函数。

—n:next下一行,但是不进入子函数,并且可能跳回一个循环

的开头。

—u:until直到从当前开始的下一行(这样可以让一个已经进入

的循环可以直接运行通过到下一步)。

—c:continue继续运行直到下一个断点或者程序的结尾。

—为了从当前函数立刻返回,可以用ret(也可以带一个返回

值,比如ret 0)。

—为了做一个更加全局的动作,j命令用来调到你想跳入的(合

理的)任意行。

—如果你想从GDB命令行中得到你的方位,你可能需要list(或

者l)来得到一个当前行的附近10行。

—按Enter键将重复最近的一条命令,这使得单步执行变得更容

易,或者在l命令后使用,以列出下一个10行。

上述四个主要的项目(断点、显示变量值、跳入另一个帧和单步执行)绝对是一个调试器最基本的功能,所以如果你发现你的调试系统在执行这些任务时有哪一步比较麻烦,那么抛弃它,再找一个。

2.1.1 GDB变量

本书介绍GDB中一些有用的元素,这些可以帮助你尽可能方便地检视数据。下面所有的命令都来自GDB命令行;基于GDB的IDE调试器也经常会提供可以调用这些工具的方式。

这里有个什么都不做的例子程序,可以用来满足审视变量的目的。因为这是一个什么都不做的程序,要确保把编译器优化选项设定为-O0,否则x就会整个消失。

这里是0号提示:@可以用来显示一些列的元素阵列。例如,如果你在这个程序的第3行停下来,你可以显示阵列的前12个元素:

注意表达式开头的星号;没有这个星号,我们会得到一连串的一堆十六进制的地址。

下一个提示可能只有那些没有读过GDB手册[Stallman 2002]的人会感到新鲜,当然也可能对你们大家都很新鲜。即我们可以生成方便变量,节省打字的时间。例如,如果你想要深入地探查一个十六进制结构中的一个元素,你可以这么做:

第一行产生了用来替代长路径名的方便的变量。遵循shell的导引,美元符号指代一个变量。与shell不同的是,你需要在这个变量的首次使用时在前面放上set和一个美元符号。第二行展示了一个简单的用法。这里并没有节省太多的打字输入,但是如果你怀疑某个变量存在恶意行为,那给它一个短名字,这样在进行彻底的调查时它能变得容易些。

这些不仅仅是名字的问题;它们是真正的变量,你可以修改它们的值。在这个无所事事的程序的第3或第4行停下来之后,尝试下面:

第二行实际上在给定位置上改变了值。在第三行,将一个指针加1并传递给列表中的下一个项目(参见“6.3.4我们需要知道的所有指针运算”)。那么,在第三条指令执行之后,$ptr现在指向x[4]。

最后的形式特别有用,我们只要不停地按Enter键就可以重复上一条命令而无须再次输入。因为指针单步向前移动,你每次敲击Enter键都可以得到下一个新值,直到你得到数组的真谛。在你处理一个链表时这也是很有用的。假设你有一个函数,用来显示链表的一个元素,并设定$list等于一个给定的元素,而且我们有放在list_head中的链表的头。即:

并且我们也知道了Enter键将单步走过链表。随后,我们将假想函数来显示一个现实中的数据结构。

但是首先,这里有另一个关于那些$打头的变量的技巧。让我剪切复制几行别人的代码来演示与调试器的互动吧:

你可能根本不想再看它了,但是请注意,如何使得打印输出的信息从$17开始。的确,每个输出都被分配了一个变量名,以至于我们可以像这样用:

为了更简洁地说明,一个单独的$是一个分配给最后的输出的简化变量。所以,如果当你得到一个十六进制的地址并认为你将得到这个地址上的值的时候,就把p*$放在下一行来得到这个值。通过这个方式,上面的步骤可以为:

2.1.2 打印结构

GDB允许你定义简单的宏,这一点在显示常见数据结构时非常有用——这也恰好是调试器常做的工作。天哪,当被显示为一长串的数字的时候,甚至一个简单的二维数组也会刺伤你的双眼。一言以蔽之,每个你常用到的结构都会有一个对应的调试器命令,用来以某种对你真正有用的方式来快速检视这个结构。

这些工具是非常原始的,但是你可能已经写了一个C侧的函数用于打印你需要处理的复杂结构,那么利用宏就可以通过简单地敲几下键盘就调用那个函数。

在GDB的提示中你不能使用任何C预处理器宏,因为它们在调试器能看到你的代码的时候就已经被替换了。所以,如果你在代码中有个有价值的宏,你可能还需要在GDB中重新实现一次。

这里有一个函数,你可以试一下用parse函数(参见“12.5 libxml和cURL”)来中途设置一个断点,同时得到一个doc结构的展示和XML树。这些宏是放在.gdbinit中的。

请注意在这个函数之后的文档;可以用help pxml或help user-defined来查看。宏的作用只是为了节省一点键盘输入,但是因为调试器的原始用途是为了查看数据,那些小的好处就积少成多了。

GLib有一个链表结构,所以我们应该有一个链表查看器。例2-1的代码即实现了这个查看器其中包括两个用户可见的宏(phead用来查看链表的头,而pnext用来单步向前查看),还有一个用户永远也不应该用的宏(plistdata,用来在phead和pnext之间随机移除项目)。例2-1 一组用于在GDB中方便地显示链表的宏——关于你可能会用到的最善于阐述的调试宏(gdb_showlist)

例2-2是一段简单的程序,在代码中使用双向链表GList来存储char*。我们可以在程序的第8或9行执行断点,然后调用上一个宏。例2-2 一些尝试调试的示例代码,或者一个对Glib链表的快速介绍(glist.c)提示你可以定义一个函数在某个命令执行之前或之后运行。例如:将在任何你打印的东西前后加上可爱的括号。最令人兴奋的钩子函数是hook-stop。假设有个变量suspect让你困扰,那么你可以每次程序停止的时候来查看它:当你分析完毕后,重新把hook-stop定义为空函数即可:提示自己动手:GDB宏可以包含while循环,它与例2-2中的if语句看起来很像(以类似while $ptr开始,并且以end结尾)。请以此为例来写一个宏,实现一次性打印整个链表。代码分析不管程序现在运行多快,我们仍然期望它更快一点。在多数语言中,头号建议是用C重写一遍,但是现在你已经是在用C语言写程序了。下一步就是找到最花费时间的函数,随后优化的努力越多,回报也越多。首先,在gcc或icc的CFLAGS中添加-pg(是的,这是存在编译器差异的;gcc将为gprof预处理程序;Intel的编译器将为prof预处理程序,并且和我在这里给出的gcc特有的细节很类似的流程)。通过这个选项,你的程序将每几毫秒就暂停一下,并记下当前是在运行在哪个函数中。这个记录以二进制的方式记录在gmon.out中。只有可执行文件被分析,连接的库。因此,如果你需要在运行一个测试程序时分析一个库,必须把所有库和程序中的代码复制到一个地方并且把所有的一切编译成一个大的可执行文件。运行程序之后,调用gprof your_program > profile,然后在文本编辑器里打开profile文件这里有一个适合阅读的列表,包括函数、它们的调用,以及程序在每个函数上所花费的时间的百分比。当你发现瓶颈在哪里的时候,也许你会觉得很惊讶。2.2 利用Valgrind检查错误

我们花在调试上的时间主要用在找到程序中第一个出现问题的点。好的代码和系统会为你找到那个点。也就是说,一个好的系统很快就会失败。

在这方面C的表现毁誉参半。在某些语言中,类似conut=15的笔误会产生一个新的变量,而且和你想要的那个count完全没有什么关系;在C中,在编译的那一步就失败了。另一方面,C却会允许你把一个9个元素的数组的第十个元素赋值,然后你可能花了很久的时间才发现第十个元素完全是一堆垃圾。

管理内存时发生的失误实在非常恼人,所以已经有很多工具来处理它们。这其中的大赢家就是Valgrind。用你的包管理器安装一个吧。Valgrind运行一个虚拟机,可以比真实的机器更好地标识内存,所以它可以知道你使用了一个9个元素的数组的第十个元素。

在编译完程序后(当然,你需要在gcc或Clang的-g选项来包含调试符号),运行下面的命令:

如果程序存在错误,Valgrind将给你两个回溯信息,看起来非常像调试器给你的那样。第一个是内存滥用被首次探测到的地方,第二个是Valgrind尽量猜测的这个内存滥用发生的代码行数,比如被重复释放的地方,以及最近的采用malloc分配内存的地方。这些错误经常是非常微妙的,因此,给你一个确切的代码行来研究其实只是展开了一条找到bug的漫漫长路。Valgrind的开发很活跃——对程序员而言没有比写一个编程工具更喜欢的了——所以令我惊奇的是,随着时间的推移,越来越多的信息被汇报出来,而且看起来未来还会变得更好。为了演示Valgrind的回溯功能,我在例9-1的代码中插入一个错误。也就是重复了第14行,free(cmd),这样会促使cmd指针在14行和15行分别被释放一次。下面是我得到的回溯信息:

试读结束[说明:试读内容隐藏了图片]

下载完整电子书


相关推荐

最新文章


© 2020 txtepub下载