对付编译器

编辑源文件

在不同的系统上,编辑源文件的方式不同。各种IDE(集成开发环境)典型地都包括一个编辑器、编译器和调试器,全部整合在一起。这些集成系统很方便,即便常常没有独立的工具那么灵活。(把它看成一把瑞士军刀式编译器。它能处理小型烹饪和屠宰任务,但是对于真正的大活,你可能需要一把真正的厨具和工作台。另一方面,它通常比一个堆满各式各样单件工具的商铺更方便。)

一旦你知道如何创建几个单独的文件,你将需要知道编译器的命名惯例。现在许多C编译器,与附加的用于完全不同语言的编译器,像Fortran、Ada、Pascal和C++捆绑在一起。这个‘C编译器’通常只是一个根据源文件名,分派正确编译器的前端。以‘.c’扩展名结尾的文件将被传递给C编译器,‘.f77’的到Fortran编译器,并且‘.cpp’、‘.c++’甚至‘.C’(大写)的到一个C++编译器。对后一种情况应特别小心——尽管C++非常类似C,两个语言常常有微妙的不同,一个有效的C程序,有时也是一个有效,但做全然不同事情的C++程序。

调用编译器

每个系统调用编译器的方法也是不同的。许多系统现在采用类似UNIX(R) ‘make’的程序,你用它定义一套从分离的源文件,编译出你的程序的规则,或者创建一个具有类似规则的‘项目’文件。只要有其中一个可用,你几乎肯定能学会使用它。一个好的程序生成器能回馈,为了学会它而只在几个编程项目上做的努力。

在许多例子中,编译器默认会运行在‘启用扩展’模式。这即是说,它不会检测能在‘严格’模式中发现的对C标准的违反情况。如果你的部分或全部代码需要可移植,你可能需要改变这种模式。例如,GNU C编译器默认使用各种各样有用和有争议的扩展,来编译相当不同的程序;为了使用严格符合模式,你必须这样调用gcc -ansi -pedantic(两个标志都是被要求的)。

你的编译器默认运行时不会产生最多的警告。大体上,警告通常是有用的——最好的编译器有一些,能告诉你何时犯了错误的优秀警告集,即使C标准并未要求产生一个诊断。可是人们对哪些警告确实合理有不同的意见,所以你可能想调整这些警告集,作为特别适合你自己的风格。如果你没有找到任何特别的风格,你可能尝试开启所有警告。在gcc上这需要多个-W标志;显而易见的-Wall真的表示‘打开gcc开发者认为特别好的很多警告’。

警告、错误和诊断

当C编译器典型地打印‘errors’——信息终止编译——以及‘warnings’无论如何继续编译时,C标准没有对它们进行区分,甚至亦未定义这两个术语。相反标准使用了这种措词‘违反约束’。对一个包含一个或以上‘违反约束’的程序——严格来说,一个‘翻译单元’,任何编译器都必须产生‘至少一个诊断’。标准没有宣布诊断的形式;虽然一个‘error’通常更合适,一个‘warning’也可以。更糟的是,标准没有要求编译器区分必须的诊断和其余非必须的消息。相反一个编译器能简单地,对每一个程序发出一个消息,不管这个程序是否存在一次‘违反约束’。

换言之,一个假象的可悲的编译器,在你每次运行它时,可以打印这个消息:

warning: 该翻译单元可能包含或不包含一个约束违反。
并且不再打印其它文字。这满足C89和C99标准之一的‘规则的意义’,却毫无用处。如果任何代码要求一个诊断,编译器已经产生了一个,如果不是这样的话,产生的诊断是一个被允许的伪造的诊断。

任何这样做的编译器自然不可能行得通;事实上编译器通常,在对每个约束违反发出一个诊断,定位(或至少接近)问题位置,并继续查找其它问题上做得很出色。当然编译器不是无所不知的——确实许多编译器特别愚蠢——它指出来的问题,实际上或者根本不是真正问题所在。随着时间增长,你将学会识别‘误导的’诊断,例如一个诊断指向一行完全正确的代码,却是因为,比如前一行少了一个分号。令人误解的诊断集也因编译器而不同。

应记住,许多编译器在关于某个具体的约束违反,是一个‘error’还是‘warning’上是不同的。两种之一都符合对一个诊断的需要,在大多数情况下,编译器不会在每个警告上标明,它是必须的亦或只是这个具体的编译器,试图给出的帮助信息。换句话说,一个这样的消息:‘warning: 整数到指针的转换,但没有使用显式转换运算符’,通常标记为一个约束违反,虽然‘warning: 整数常量太大以致成为无符号整数’不是一个约束违反。无法通过查看诊断信息,得知你的程序是否能在其它编译器上工作。(例如,整数到指针转换,在一个更严格的编译器上,可能是一个错误。)

当你看到一个警告,仔细检查代码中的约束违反或任何其它问题,是一个好主意。如果代码没有真正出错,或者你需要犯一些固有的‘错误’(但是它能在你的系统上工作,并且达到一些不能被‘正确’完成的效果。) 你或能够单独关闭这个警告,或者把这个代码放在一个编译时屏蔽了那个警告的文件。在一些情况中,可能有一些琐碎的重写代码的方式,来避免这个警告,并且甚至可能使代码更清晰或更好。例如,如果你想使用整数常量65536,它在一些编译器上足够大,以致会变成无符号数,你可以写65536U以在所有编译器上,强制把这个数字变成一个unsigned int。

调试和未定义行为

即使一个程序不包含编译器能明显识别的错误——没有约束违反和其它导致警告的地方——也不必然是正确的。包含标准所谓的‘未定义行为’的程序尤为麻烦。未定义行为实际上是语言的一个‘漏洞’。

‘语言的漏洞’有好和坏的两方面。‘好的方面’是它提供给实现一个扩展语言的挂钩。例如,一个翻译单元可以包含这个指令:

#include <graphics.h>
它实际上导致未定义行为。它表示实现可以自由地做任何事情——包括,现在它能画图,这在标准C里是不可能的。一旦你引起未定义行为,实现者完全脱离C标准中的约定,所以现在他可以提供各类有用但标准可能禁止的特性。未定义行为是一种例外条款。

另一方面,‘坏的方面’也差不多。未定义行为把实现者从‘约定’中解放出来,所以,现在如果任何事情出错了,你只能责怪你自己。而且,如果你做了任何违反应用标准‘constraints’外的‘shall’部分的任何事情,实现者会打出那个例外条款。这能把他从麻烦的工作中解放出来。例如使用一个不包含有效值的指针,或者相加两个有符号整数值时其结果溢出,会产生未定义行为。如果所有实现者都被要求做特别的事情,比如捕获运行时错误并给你一个机会调试它,这将会拖慢所有指针和有符号数相加,在一些系统上的使用。每次你要使用一个指针,实现者都不得不首先检查指针的有效性。每次你要相加两个整数,实现者都不得不检查溢出。

一些人可能提出反对说,现代计算机通常完全忽略整数溢出(在两个这样的整数求和时,产生一个错误但是可预测的结果。) 所以,C标准可以要求忽略这类溢出。然而,实际上一些现代计算机确实并能检测这类溢出,并且不可能争辩说计算一个错误的结果更好,即使它能很快完成;或者要求一个错误的答案,即使获得这个答案更慢。

通过允许实现者这个例外条款——并且把避免未预期未定义行为的责任放在程序员身上——C标准允许实现者生成紧凑和快的代码。通过使用一个‘挂钩’挂接扩展,C标准允许程序员编写故意的机器相关代码,以致于可能要求使用汇编语言。

在任何一种情况,一旦发生错误——通常来自未定义行为——你的开发系统可能向你提供一些方式调试代码。这可以采用事后调试的形式,系统给出程序出错时,它知道的关于程序状态的任何事情,或者交互调试。你的调试器需要一些检查变量的方法,查看活动函数调用,诸如此类。不同调试器可以非常不同,并且一般而言,关于它们没有太多可以细说,除了一件事:如果你的编译器做很多代码优化,你调试的代码可能不同于你所写的。优化的本质在于解释一些源代码结构的含义,并形成产生相同结果的机器代码,但是不一定完全是这样的步骤。调试时可能需要关闭优化。有时这是有帮助的,但是有时它会使错误消失无踪,实际上没有修复真正的问题。例如,假设问题与未定义行为有关,并且一个无效的指针没有被赋值。当优化打开时,那个‘坏’指针恰巧与其它代码共享一个机器寄存器。那个其它代码给寄存器设置一个值导致运行时错误。当你关闭优化,那个‘坏’指针拥有其它值,并且当你使用指针时程序没有失败——但是因为指针实际上是非法,使用它引起其它数据被覆写。有时——或许几分钟,在远离这个缺陷的其它源文件中,程序可能会以某些方式表现失常。

同样,优化过程包括分析程序,它能定位错误,比如使用一个具有垃圾初始值的变量。 (标准数据流分析做这个工作。) 这表示一个编译器有时仅在优化时产生一定的警告。调试未优化的代码自然仍是一个非常有价值的技巧。

然而,最好的调试器只是耳之所闻。阅读代码;看你期望会发生什么;并与真正发生的情况对比。伪称程序行为失常,是有许多线索可循的离奇谋杀事件。变量x在你希望是10时变成7。什么才能够解释它?是Green先生躲在音乐室,用铅管敲它吗?还是Plum教授在图书室,用抓到的教鞭?如果你能解释发生了什么事,并且找到那段不但看起来有错误,而且的确做了那些事的代码,你可以修改代码,重新编译,重新测试——如果程序现在能工作(或者那个问题没有出现了),你可以相当有把握你真的改正了它。


(This Chinese translation isn't confirmed by the author, and it isn't for profits.)

Translator : jhlicc@gmai1.c0m
Origin : http://www.torek.net/torek/c/compiler.html