c语言程序设计:现代方法-


  指示预处置器翻开一个名字为stdio. 并将它的内容加到以后的顺序中。 预处置器的输入是一个c语言顺序, 顺序能够包含指令。 不再包含指令。 预处置器的输出被直接交给编译器, 编译器检查顺序是否有错误, 并经顺序翻译为目的代码(机器指令)。

  为了展现预处置器的作用, 我们将它应用于2. h的内容参与来呼应#include指令。 由于长度的原因, 这里没有将stdio. 预处置器也删除了#define指令, 并且替换了该文件中稍后出如今任何位置上的freezing_pt和scale_factor。 请留意预处置器并没有删除包含指令的行, 而是简单地将它们替换为空。

  正如这个例子所展现的那样, 特别值得留意的是, 它将每一处注释都替换为一个空格字符。 有一些预处置器还会进一步删除不必要的空白字符, 包括在每一行开端用于缩进的空格符和制表符。

  在c语言较早的时期, 预处置器是一个独自的顺序, 如今, 我们依然将它们认为是不同的顺序。 实际上, 使用户可以看到预处置器的输出。 一些编译器在翻开特定的选项时(unix环境下通常是-p)仅产生预处置器的输出。 如果需求更多的信息,

  留意, 预处置器仅知道少量c语言的规则。 因此, 它在执行指令时十分有能够产生非法的顺序。 经常是源顺序看起来没成果, 使错误难以查找。 关于较复杂的顺序, 检查预处置器的输出能够是找到这类错误的有效途径。 #define指令定义一个宏, #undef指令删除一个宏定义。

  l文件包含。 #include指令招致一个指定文件的内容被包含到顺序中。

  l条件编译。 #if、#ifdef、#ifndef、#elif、#else和#endif指令可以根据编译器可以测试的条件来将一段文本块包含到顺序中或排除在顺序之外。 较少用到。 独一一个我们不会在这里详细讨论的指令是#include, 因为它会在15.

  在进一步讨论之前, 在#后是指令名, 接着是指令所需求的其他信息。 例如, 上面的指令是合法的:

  l指令总是在第一个换行符处完毕, 如果想在下一行继续指令, 我们必须在以后行的末尾使用\字符。 按字节计算:

  l指令可以出如今顺序中任何中央。 我们通常将#define和#include指令放在文件的开端,

  l注释可以与指令放在同一行。 实际上, 在一个宏定义的后面加一个注释来解释宏的意义是一种比较好的习惯:

  我们从第2章以来使用的宏被称为简单的宏, 它们没有参数。 本节会先讨论简单的宏, 然后再讨论带参数的宏。

  简单的宏定义有如下格式:

  替换列表是一系列的c语言记号, 当预处置器遇到一个宏定义时, 在文件后面的内容中, 不管标识符在任何位置出现,

  不要在宏定义中放置任何额外的符号, 否则它们会被作为替换列表的一部分。 但不幸的是, 编译器会将每一处使用这个宏的中央标为错误, 而不会直接找到错误的本源——宏定义本身, 因为宏定义已经被预处置器删除了。

  简单的宏主要用来定义那些被kernighan和ritchie称为“明示常量”(manifestconstant)的东西。 我们可以给数值、字符和字符串命名。 一个认真选择的名字可以协助读者理解常量的意义。 使读者难以理解。 我们仅需求改变一个宏定义, 特别是有时分当他们以略微不同的方式出现时。 如果一个顺序包含一个长度为100的数组, 它能够会包含一个从0到99的循环。 那么就会漏掉99。 假设数值常量3. 14159在顺序中少量出现,

  虽然简单的宏常用于定义常量名, 但是它们还有其他应用。 实际上, 我们可以通过定义宏的方式给c语言符号添加别名, 例如, 改变c语言的语法通常不是个好主意, 因为它会使顺序很难被其他顺序员所理解。 在5. 2节中, 但类型定义(7. 6节)依然是定义新类型的最佳办法。 如将在14. 4节中看到的那样, 宏在控制条件编译中起重要的作用。 例如, 在顺序中出现的宏定义能够标明需求将顺序在“调试形式”下停止编译, 来使用额外的语句输出调试信息:

  这里顺便提一下, 宏定义中的替换列表为空是合法的。

  当宏作为常量使用时, c顺序员习惯在名字中只使用大写字母。 但是并没有如何将用于其他目的的庞大写的一致做法。 所以一些顺序员更喜欢使用大写字母来惹起留意。 即依照kernighan和ritchie编写的

  2,

  例如, 假定我们定义了如下的宏:

  如今如果后面的顺序中有如下语句:

  预处置器会将这些行替换为

  如这个例子所显示的, 带参数的宏经常用来作为一些简单的函数使用。 max相似一个从两个值中选取较大的值的函数。 该函数当参数为偶数时返回1, 否则返回0。

  上面的例子是一个更复杂的宏:

  这个宏检测一个字符c是否在a与z之间。 如果在的话, 来计算出c所对应的大写字母。 如果c不在这个范围, 像这样的字符处置的宏十分有用, 所以c语言库在<ctype. h>(23. 其中之一就是toupper, 与我们上面的toupper例子作用分歧(但会更高效, 可移植性也更好)。

  带参数的宏可以包含空的参数列表, 如下例所示:

  空的参数列表不是一定确实需求, 但可以使getchar更像一个函数。 (没错, h>中的getchar, 不是函数——虽然它的功用像个函数。 )

  使用带参数的宏替代实际的函数有两个优点:

  l顺序能够会略微快些。 而一个宏的调用则没有这些运转开销。 与函数的参数不同, 宏的参数没有类型。 因此, 只要预处置后的顺序依然是合法的, 宏可以接受任何类型的参数。 我们可以使用max宏从两个数中选出较大的一个, 数的类型可以是int, longint, 每一处宏调用都会招致插入宏的替换列表, 由此招致顺序的源代码增加(因此编译后的代码变大)。 当宏调用嵌套时, 这个成果会相互叠加从而使顺序愈加复杂。 思考一下, 当一个函数被调用时, 编译器会检查每一个参数来确认它们是否是正确的类型。 如果不是, 或者将参数转换成正确的类型, 或者由编译器产生一个出错信息。 预处置器不会检查宏参数的类型, 也不会停止类型转换。

  l无法用一个指针来指向一个宏。 7节中将看到的, 这一概念在特定的编程条件下十分有用。 宏会在预处置进程中被删除, 宏不能用于处置这些状况。

  l宏能够会不止一次地计算它的参数。 函数对它的参数只会计算一次, 多次计算参数的值能够会产生意外的结果。 考虑上面的例子, 那么i能够会被(错误地)增加了两次, 如果我们已经写烦了语句

  因为每次要显示一个整数x都要使用它。 我们可以定义上面的宏, 编译器不会识别这两种运算符相反, 它们会在预处置时被执行。

  #运算符将一个宏的参数转换为字符串字面量。 它仅允许出如今带参数的宏的替换列表中。 )

  #运算符有少量的用途, 这里只来讨论其中的一种。 假设我们决定在调试进程中使用print_int宏作为一个便捷的办法, 来输出一个整型变量或表达式的值。 #运算符可以使print_int为每个输出的值添加标签。 因此, 因此上边的语句等价于:

  当顺序执行时, printf函数会同时显示表达式i/j和它的值。 如果i是11, j是2的话, 输出为

  ##运算符可以将两个记号(例如标识符)“粘”在一起, (无需惊讶, )如果其中一个操作数是宏参数, “粘合”会在当方式参数被相应的实际参数替换后发生。 预处置器首先使用自变量(这个例子中是1)替换参数n。 预处置器将i和1连接成为一个记号(i1)。 想找到一些使用它的状况是比较困难的。 为了找到一个有实际意义的##的应用, 如我们所见, 当max的参数有反作用时会无法正常任务。 往往一个max函数是不够的。 我们能够需求一个实际参数是int值的max函数, 还需求参数为float值的max函数, 等等。 除了实际参数的类型和返回值的类型之外, 这些函数都一样。 因此, 这里还有个成果, 如果我们是用宏来创立多个max函数, (c语言不允许在同一文件中出现两个同名的函数。 上面是宏的显示方式:

  请留意宏的定义中是如何将type和_max相连来构成新函数名的。

  如今, 上面是如何使用generic_max宏来定义函数:

  预处置器会将这行展开为上面的代码:

  如今我们已经讨论过简单的宏和带参数的宏了, 例如, 我们可以用宏pi来定义宏two_pi:

  当预处置器在后面的顺序中遇到two_pi时, 预处置器会重新检查替换列表, 看它是否包含其他宏的调用(在这个例子中, 调用了宏pi)。 直到将一切的宏名字都替换掉为止。

  l预处置器只会替换残缺的记号, 而不会替换记号的片断。 因此, 例如, 假设顺序含有如下代码行:

  预处置后, 这些代码行会变为:

  l一个宏定义的作用范围通常到出现这个宏的文件末尾。 由于宏是由预处置器处置的, 他们不听从通常的范围规则。

  l宏不可以被定义两遍, 但是宏的替换列表(和参数, 如果有的话)中的记号都必须分歧。 #undef指令有如下方式:

  其中标识符是一个宏名。 (如果n没有被定义成一个宏, #undef指令没有任何作用。 )#undef指令的一个用途是取消一个宏的现有定义,

  在我们后面定义的宏的替换列表中有少量的圆括号。 确实需求它们吗?答案是相对需求。 如果我们少用几个圆括号, 宏能够有时会失掉意料之外的——而且是不希望有的——结果。

  关于在一个宏定义中哪里要加圆括号有两条规则要恪守。 首先, 如果宏的替换列表中有运算符, 那么始终要将替换列表放在括号中:

  其次, 如果宏有参数, 编译器能够会不按我们希冀的方式应用运算符的优先级和结合性规则。

  为了展现为替换列表添加圆括号的重要性, 考虑上面的宏定义, 产生的结果并不是希冀的结果。

  当宏有参数时, 例如, 假设scale定义如下:

  在预处置进程中, 这条语句等价于

  当然, 我们希望的是

  在创立较长的宏时, 特别是可以使用逗号运算符来使替换列表包含一系列表达式。 例如, 上面的宏会读入一个字符串, 因此使用逗号运算符连接它们是合法的。 这种方式并不奏效。 假设我们将echo宏用于上面的if语句:

  将echo宏替换会失掉上面的结果:

  编译器会将头两行作为残缺的if语句:

  编译器会将跟在后面的分号作为空语句, 因为它不属于任何if语句。 但是这样做会使顺序看起来有些怪异。

  逗号运算符可以处置echo宏的成果, 但并不能处置一切宏的成果。 假设一个宏需求包含一系列的语句, 这时逗号运算符就起不到协助的作用了。 因为它只能连接表达式, 处置的办法是将语句放在do循环中, 因此我们不会遇到在if语句中使用宏那样的成果了。 为了看到这个技巧(嗯, 应该说是技术)的实际作用, 让我们将它用于echo宏中:

  当使用echo宏时, 一定要加分号:

  在c语言中预定义了一些有用的宏, 见表14. 1。 这些宏主要是提供以后编译的信息。 宏__line__和__stdc__是整型常量, 其他3个宏是字符串字面量。 因此这里将重点放在其他的宏上。

  __date__宏和__time__宏指明顺序编译的工夫。 例如, 顺序都会显示上面两行:

  这样的信息可以协助区分同一个顺序的不同版本。

  我们可以使用__line__宏和__file__宏来找到错误。 考虑上面这个检测被零除的除法的发生位置的成果。 通常没有信息指明哪条除法运算招致错误。 上面的宏可以协助我们查明错误的本源:

  check_zero宏应该在除法运算前被调用:

  如果j是0, 会显示出如下方式的信息:

  相似这样的错误检测的宏十分有用。 实际上, c语言库提供了一个通用的、用于错误检测的宏——assert宏(24. 1节)。

  c语言的预处置器可以识别少量用于支持条件编译的指令。 经常需求保管这些printf函数调用, 以备以后使用。 条件编译允许我们保管这些调用, 但是让编译器忽略它们。 首先定义一个宏, 并给它一个非0的值:

  宏的名字并不重要。 接上去, 我们要在每组printf函数调用的前后加上#if和#endif:

  在于处置进程中, 由于debug的值非0, 因此预处置器会将这两个printf函数调用保管在顺序中(但#if和#endif行会消失)。 如果我们将debug的值改为0并重新编译顺序, 预处置器则会将这4行代码都消失。 所以这些调用就不会在目的代码中占用空间, 也不会在顺序运转时浪费工夫。 我们可以将#if-#endif保管在最终的顺序中, 这样如果顺序在运转时出错, 可以继续产生这些诊断信息(将debug改为1并重新编译)。

  普通来说, 会计算常量表达式。 如果表达式的值为0, 否则, #if指令会把它当作是值为0的宏对待。 如果省略debug的定义, 而测试

  会成功。 3节中介绍过运算符#和##, 还有另外一个运算符——defined, 当defined应用于标识符时, 如果标识符是一个定义过的宏返回1, #defined运算符通常与#if指令结合使用, 允许写成

  仅当debug被定义成宏时, #if和#endif之间的代码会被保管在顺序中。 debug两侧的括号不是必需的, 因此可以简单写成

  由于defined运算符仅检测debug是否被定义为宏, 换而言之, 最好随着嵌套层次的增加而增加缩进。 预处置器提供了#elif和#else指令:

  #elif指令和#else指令可以与#if指令、#ifdef指令和#ifndef指令组合使用,

  条件编译关于调试是十分方便的, 上面是其他一些罕见的应用:

  l编写在多台机器或多种操作系统之间可移植的顺序。 由此选择了一个特定的操作系统。

  l编写可以使用不同的编译器停止编译的顺序。 这些版本之间会有一些差别。 一些会接受规范c, __stdc__宏允许预处置器检测编译器是否支持规范c, 如果不支持, 我们能够必须修正顺序的某些方面, 如果没有, 则提供一个默许的定义。 例如, 如果它还没有被定义的话, 上面的代码会定义宏buffer_size:

  l暂时屏蔽包含注释的代码。 但是, 2节会讨论另外一种条件编译的常用用途:维护头文件以避免重复包含。

  在本章的最后, 我们将简明地了解一下#error指令、#line指令和#pragma指令。 实际上, 本书中的一切顺序都不会使用它们, 因此你可以放心地跳过本节。 当你准备成为一个c语言专家时,

  #error指令有如下格式:

  其中, 消息是一个c语言标记序列。 如果预处置器遇到一个#error指令, 出错消息的详细方式也能够会不一样。 上面是一个典型的示例:

  碰到#error指令预示着一个严重的顺序错误, 大多数编译器会立即终止编译而不去找出其他错误。

  #error指令通常与条件编译指令一起用于检测正常编译进程中不应出现的状况。 例如, 假定我们需求确保, 一个顺序在一台无法使用int类型来存储大于100000的数的机器上不能编译。 最大允许的int值用int_max宏(23. 所以我们需求做的就是当int_max宏不超越100000时调用#error指令:

  如果试图在一台整型以16位存储的机器上编译这个顺序, 将产生一条出错消息:

  #line指令是用来改变给顺序行编号的方式的。 (顺序行通常是按1, …来编号的。 )我们也可以使用这条指令使编译器认为它正在从一个有不同名字的文件中读取一个顺序。

  #line指令有两种方式。 在一种方式中, 更重要的是, 例如, 假设下列指令出如今文件foo. c的开头:

  如今, 假设编译器在foo. c的第5行监测到一个错误。 c的第13行, c的第5行。 (为什么是第13行呢?因为指令占据了foo. c的第1行, 并将这一行作为bar. c的第10行。 为什么要使出错消息指向另一行, 它主要用于那些产生c代码作为输出的顺序。 顺序yacc(yetanothercompiler-compiler)是其中著名的顺序之一。 yacc是一个unix工具, 用于自动生成部分的编译器。 在使用yacc之前, 通过这个文件, 并合并了顺序员所提供的代码。 顺序员接着依照正常办法编译y. 通过在y. tab. c中插入#line指令, 于是, 任何编译y. 其最终结果是:调试变得更容易,

  #pragma指令为要求编译器执行某些特殊操作提供了一种办法。 这条指令对十分大的顺序或需求使用特定编译器的特殊功用的顺序十分有用。 记号是普通c语言的记号。

  一些编译器允许#pragma指令所包含的不只是简单的命令。 特别是有些编译器允许#pragam指令带参数:

  #pragma指令中出现的命令集在不同的编译器上是不一样的。 你必须通过查阅你所使用的编译器的文档来了解哪些命令是可以使用的, 以及这些命令的功用。 顺便提一下, 如果#pragma指令包含了无法识别的命令, 编译器必须忽略这些#pragma指令, 不允许产生出错消息。

  问与答

  问:我看到在有些顺序中#独自占一行。 这样是合法的吗?

  答:是合法的。 这就是所谓的空指令, 空行也可以。 不过#可以协助读者看出模块的范围。

  问:我不清楚顺序中哪些常量需求定义成宏。 195)

  答:一条首要的规则是每一个数字常量, 如果不是0和1, 就需求定义成宏。 字符常量和字符串常量有一点复杂, 根据第二条规则, 我不会像这样使用宏:

  虽然有些顺序员会使用。

  C语言问:如果要被“字符串化”的参数包含或\字符, \转换为\\。 考虑上面的宏:

  问:我无法使上面的宏正常任务:

  答:感谢那些连kernighan和ritchie都承认“怪异”的规则, 这里的成果在于concat(a, c)首先得出bc, bc)给出abc。 位于##运算符之前和之后的宏参数在替换时不被扩展, 结果, c))扩展成aconcat(b, c), 而不会进一步扩展, 因为没有名为aconcat的宏。 但不太美观。 concat2(b, 预处置器将会同时扩展concat2(b, c)。 如果这个也不行, 那也不必担忧, 如果#x出如今替换列表中, 其中x是一个宏参数, 其对应的实际参数也不会被扩展。 因此, 处置的办法与我们在concat例子中相似:定义第二个宏来调用str。

  问:如果预处置器再重新扫描时又发现了最后的宏名会如何处置呢?例如上面的例子:

  问:预处置器会将n替换为(2m), 接着将m替换为(n+1)。 199)

  答:一些早期的预处置器确实会进入无限循环, 但契合规范c的预处置器则不会。 依照c语言规范, 如果在扩展宏的进程中原先的宏名重复出现的话, 上面是预处置后i的赋值语句的方式:

  一些大胆的顺序员会通过编写其名字与保管字或规范库中的函数名匹配的宏来应用这一行为。 以库函数sqrt为例。 sqrt函数(23. 3. 如果参数为负数则返回一个由完成定义的值, 我们能够希望当参数为负数时返回0。 由于sqrt是规范库函数, 我们无法很容易地修正它。 但是我们可以定义一个宏, 并将它替换成上面的条件表达式。 因此会被保管由编译器处置。

  问:我觉得预处置器就是一个编辑器。 它如何计算常量表达式呢?(p. 202)

  答:预处置器比你想的要复杂。 它足够“了解”c语言, 所以可以计算常量表达式。 虽然它不会完全依照编译器的方式去做。 预处置器认为一切未定义的名字的值为0。 其他的差别太深奥, 就不再深入了。 )在实际使用中, 预处置器常量表达式中的操作数通常为常量、表示常量的宏或defined运算符的应用。

  问:为什么c提供#ifdef指令和#ifndef指令, 203)

  答:#ifdef指令和#ifndef指令从20世纪70年代就在c语言中存在了, 因此, 我们如今可以使用#if和defined运算符来测试任意数量的宏, 而不再是只能使用#ifdef和#ifndef对一个宏停止测试。 例如, 上面的指令检查是否foo和bar被定义了而baz没有被定义:

  问:我想编译一个还没有写完的顺序, 预处置记号相似于c语言记号(标识符、运算符、数等)。 当预处置器试图将第一行分解为记号时, 会遇到haven(一个合法的标识符), 一些预处置器会跳过#if指令和#endif指令之间一切的行而不会检查预处置记号, 但这些预处置器并没有严厉恪守c语言规范的规则。 但它同时也能够是许多难以发现的错误的本源。 我依然建议过度地使用它, 现代的c语言编程作风呼吁增加关于处置器的依赖。 对语言的改变使得可以更进一步限制预处置器的使用。
文章由迪尔马奇怎么样整理,请容许保留出处

posted on 2011-04-24 17:12  jiyizhen3721  阅读(541)  评论(0编辑  收藏  举报