当前位置: 首页 > article >正文

C现代方法(第14章)笔记——预处理器

文章目录

  • 第14章 预处理器
    • 14.1 预处理器的工作原理
    • 14.2 预处理指令
    • 14.3 宏定义
      • 14.3.1 简单的宏
      • 14.3.2 带参数的宏
      • 14.3.3 #运算符
      • 14.3.4 ##运算符
      • 14.3.5 宏的通用属性
      • 14.3.6 宏定义中的圆括号
      • 14.3.7 创建较长的宏
      • 14.3.8 预定义宏
      • 14.3.9 C99中新增的预定义宏
      • 14.3.10 空的宏参数(C99)
      • 14.3.11 参数个数可变的宏(C99)
      • 14.3.12 __func__标识符(C99)
    • 14.4 条件编译
      • 14.4.1 #if指令和#endif指令
      • 14.4.2 defined运算符
      • 14.4.3 #ifdef指令和#ifndef指令
      • 14.4.4 #elif指令和#else指令
      • 14.4.5 使用条件编译
    • 14.5 其他指令
      • 14.5.1 #error指令
      • 14.5.2 #line指令
      • 14.5.3 #pragma指令
      • 14.5.4 _Pragma运算符(C99)
    • 问与答
    • 写在最后

第14章 预处理器

——总有一些事用什么语言都不好表达,而我们希望能通过程序把它表达出来。

前面的几章用到过#define#include指令,但没有深入讨论。这些指令(以及我们还没有学到的指令)都是由预处理器处理的。预处理器是一个小软件,它可以在编译前处理C程序。C语言(和C++语言)因为依赖预处理器而不同于其他的编程语言。

预处理器是一种强大的工具,但它同时也可能是许多难以发现的错误的根源。此外,预处理器也可能被错误地用来编写出一些几乎不可能读懂的程序。尽管有些C程序员十分依赖于预处理器,我依然建议适度地使用它,就像生活中的其他许多事物一样。

本章首先描述预处理器的工作原理14.1节),并且给出一些会影响预处理指令(14.2节)的通用规则。14.3节和14.4节介绍预处理器最主要的两种能力:宏定义条件编译。(而处理器另外一个主要功能,即文件包含,将留到第15章再详细介绍。)14.5节讨论较少用到的预处理指令:#error#line#pragma

14.1 预处理器的工作原理

预处理器的行为是由预处理指令(由#字符开头的一些命令)控制的。我们已经在前面的章节中遇见过其中两种指令,即#define#include

  • #define指令定义了一个宏——用来代表其他东西的一个名字,例如常量或常用的表达式。预处理器会通过将宏的名字和它的定义存储在一起来响应#define指令。当这个宏在后面的程序中使用到时,预处理器“扩展”宏,将宏替换为其定义内容

  • #include指令告诉预处理器打开一个特定的文件,将它的内容作为正在编译的文件的一部分“包含”进来。例如,代码行#include <stdio.h>指示预处理器打开一个名为stdio.h的文件,并将它的内容加到当前的程序中。(stdio.h包含了C语言标准输入/输出函数的原型。)

预处理器在编译过程中的作用:预处理器的输入是一个C语言程序,程序可能包含指令。预处理器会执行这些指令,并在处理过程中删除这些指令。预处理器的输出是另一个C程序:原程序编辑后的版本,不再包含指令。预处理器的输出被直接交给编译器,编译器检查程序是否有错误,并将程序翻译为目标代码(机器指令)。

可以通过一个程序来展现预处理器的作用:

//预处理之前:
#include <stdio.h> 

#define FREEZING_PT 32.0f 
#define SCALE_FACTOR (5.0f / 9.0f) 

int main(void) 
{ 
    float fahrenheit, celsius; 
    
    printf("Enter Fahrenheit temperature: "); 
    scanf("%f", &fahrenheit); 
    celsius = (fahrenheit - FREEZING_PT) * SCALE_FACTOR; 
    printf("Celsius equivalent is: %.1f\n", celsius); 
    
    return 0; 
} 

/*********************************************************/

//预处理结束后:
空行 
空行 
从stdio.h中引入的行 
空行 
空行 
空行 
空行 
int main(void) 
{ 
    float fahrenheit, celsius; 
    
    printf("Enter Fahrenheit temperature:  "); 
    scanf("%f", &fahrenheit); 
    celsius = (fahrenheit - 32.0f) * (5.0f / 9.0f); 
    printf("Celsius equivalent is: %.1f\n", celsius); 
    
    return 0; 
}

预处理器通过引入stdio.h的内容来响应#include指令。预处理器也删除了#define指令,并且替换了该文件中稍后出现在任何位置上的FREEZING_PTSCALE_FACTOR请注意预处理器并没有删除包含指令的行,而是简单地将它们替换为空

正如这个例子所展示的那样,预处理器不仅仅执行了指令,还做了一些其他的事情。特别值得注意的是,它将每一处注释都替换为一个空格字符。有一些预处理器还会进一步删除不必要的空白字符,包括每一行开始用于缩进的空格符和制表符


C语言较早的时期,预处理器是一个单独的程序,它的输出提供给编译器。如今,预处理器通常和编译器集成在一起,而且其输出也不一定全是C代码(例如,包含<stdio.h>之类的标准头使得我们可以在程序中使用相应头中的函数,而不需要把头的内容复制到程序的源代码中)。然而,将预处理器和编译器看作不同的程序仍然是有用的。实际上,大部分C编译器提供了一种方法,使用户可以看到预处理器的输出。在指定某个特定的选项(GCC 用的是-E)时编译器会产生预处理器的输出。其他一些编译器会提供一个类似于集成的预处理器的独立程序。要了解更多的信息,可以查看你使用的编译器的文档。

注意!!预处理器仅知道少量C语言的规则。因此,它在执行指令时非常有可能产生非法的程序。经常是原始程序看起来没问题,使错误查找起来很难。对于较复杂的程序,检查预处理器的输出可能是找到这类错误的有效途径。

14.2 预处理指令

大多数预处理指令属于下面3种类型之一:

  • 宏定义#define指令定义一个宏,#undef指令删除一个宏定义。
  • 文件包含#include指令导致一个指定文件的内容被包含到程序中。
  • 条件编译#if#ifdef#ifndef#elif#else#endif指令能根据预处理器可以测试的条件来确定,是将一段文本块包含到程序中,还是将其排除在程序之外。

剩下的#error#line#pragma指令是更特殊的指令,较少用到。本章将深入研究预处理指令。唯一一个不会在这里详细讨论的指令是#include,这个指令将在第15章介绍。


下面的规则适用于所有指令:

  • 指令都以#开始#符号不需要出现在一行的行首,只要在它之前只有空白字符就行。在#后是指令名,接着是指令所需要的其他信息。
  • 在指令的符号之间可以插入任意数量的空格或水平制表符。例如,下面的指令是合法的:
#     define     N     100
  • 指令总是在第一个换行符处结束,除非明确地指明要延续。如果想在下一行延续指令,我们必须在当前行的末尾使用\字符。例如,下面的指令定义了一个宏来表示硬盘的容量,按字节计算:
#define DISK_CAPACITY (SIDES *              \ 
                       TRACKS_PER_SIDE *    \ 
                       SECTORS_PER_TRACK *  \ 
                       BYTES_PER_SECTOR)
  • 指令可以出现在程序中的任何地方。但我们通常将#define#include指令放在文件的开始,其他指令则放在后面,甚至可以放在函数定义的中间。
  • 注释可以与指令放在同一行。实际上,在宏定义的后面加一个注释来解释宏的含义是一种比较好的习惯:
#define FREEZING_PT 32.0f   /* freezing point of water */ 

14.3 宏定义

从第2章开始使用的宏被称为简单的宏,它们没有参数。预编译器还支持带参数的宏。本节先讨论简单的宏,然后再讨论带参数的宏。在分别讨论它们之后,我们会研究一下二者共同的特性。

14.3.1 简单的宏

简单的宏C标准中称为对象式宏)的定义有如下格式:

#define 标识符 替换列表

替换列表是一系列的预处理记号,本章中提及“记号”时均指“预处理记号”。

宏的替换列表可以包括标识符关键字数值常量字符常量字面串运算符标点符号。当预处理器遇到一个宏定义时,会做一个“标识符”代表“替换列表”的记录。在文件后面的内容中,不管标识符在哪里出现,预处理器都会用替换列表代替它。

请注意!!不要在宏定义中放置任何额外的符号,否则它们会被当作替换列表的一部分。一种常见的错误是在宏定义中使用'='

#define N = 100   /*** WRONG ***/ 
... 
int a[N];     /* becomes int a[= 100]; */ 

在上面的例子中,我们(错误地)把N定义成两个记号(=100)。

在宏定义的末尾使用分号是另一个常见错误

#define N 100;  /*** WRONG ***/
...
int a[N];   /* becomes int a[100;]; */

这里N被定义为100;两个记号。

编译器可以检测到宏定义中绝大多数由多余符号所导致的错误。但是,编译器只会将每一个使用这个宏的地方标为错误,而不会直接找到错误的根源——宏定义本身,因为宏定义已经被预处理器删除了。

简单的宏主要用来定义那些被KernighanRitchie称为“明示常量”manifest constant)的东西。 我们可以使用宏给数值字符值字符串值命名

#define STE_LEN 80 
#define TRUE    1 
#define FALSE   0 
#define PI      3.14159 
#define CR      '\r' 
#define EOS     '\0' 
#define MEM_ERR "Error: not enough memory" 

使用#define来为常量命名有许多显著的优点:

  • 程序会更易读。一个认真选择的名字可以帮助读者理解常量的意义。否则,程序将包含大量的“魔法数”,很容易迷惑读者。
  • 程序会更易于修改。我们仅需要改变一个宏定义,就可以改变整个程序中出现的所有该常量的值。“硬编码”的常量会更难于修改,特别是当它们以稍微不同的形式出现时。(例如,如果程序包含一个长度为100的数组,它可能会包含一个0~99的循环。如果我们只是试图找到程序中出现的所有100,那么就会漏掉99。)
  • 可以帮助避免前后不一致或键盘输入错误。假如数值常量3.14159在程序中大量出现,它可能会被意外地写成3.14163.14195

虽然简单的宏常用于定义常量名,但是它们还有其他应用:

  • 可以对C语法做小的修改。我们可以通过定义宏的方式给C语言符号添加别名,从而改变C语言的语法。例如,对于习惯使用Pascalbeginend(而不是C语言的{})的程序员,可以定义下面的宏:

    #define BEGIN { 
    #define END   }
    

    我们甚至可以发明自己的语言。例如,我们可以创建一个LOOP“语句”,来实现一个无限循环:

    #define LOOP for (;;)
    

    当然,改变C语言的语法通常不是个好主意,因为它会使程序很难被其他程序员理解。

  • 对类型重命名。在5.2节中,我们通过重命名int创建了一个布尔类型:

    #define BOOL int
    

    虽然有些程序员会使用宏定义的方式来实现此目的,但类型定义typedef(7.5节)仍然是定义新类型的最佳方法。

  • 控制条件编译。如将在14.4节中看到的那样,宏在控制条件编译中起到了重要的作用。例如,在程序中出现的下面这行宏定义可能表明需要将程序在“调试模式”下进行编译,并使用额外的语句输出调试信息:

    #define DEBUG 
    

    这里顺便提一下,如上面的例子所示,宏定义中的替换列表为空是合法的

当宏作为常量使用时,C程序员习惯在名字中只使用大写字母。但是并没有如何将用于其他目的的宏大写的统一做法。由于宏(特别是带参数的宏)可能是程序中错误的来源,一些程序员更喜欢全部使用大写字母来引起注意,而有些人则倾向于小写。

14.3.2 带参数的宏

带参数的宏(也称为函数式宏)的定义有如下格式:

#define 标识符(x1, x2, ..., xn) 替换列表

其中x1, x2,..., xn是标识符(宏的参数)。这些参数可以在替换列表中根据需要出现任意次

请注意!!在宏的名字和左括号之间必须没有空格。如果有空格,预处理器会认为在定义一个简单的宏,其中(x1, x2, ..., xn)是替换列表的一部分。

当预处理器遇到带参数的宏时,会将宏定义存储起来以便后面使用。在后面的程序中,如果任何地方出现了标识符(y1, y2, ..., yn)格式的宏调用(其中y1, y2, ..., yn是一系列记号),预处理器会使用替换列表替代——使用y1替换x1y2替换x2,以此类推。

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

#define MAX(x,y)  ((x)>(y)?(x):(y)) 
#define IS_EVEN(n) ((n)%2==0)

(宏定义中的圆括号似乎过多,但本节后面将看到,这样做是有原因的。)现在如果后面的程序中有如下语句:

i = MAX(j+k, m-n); 
if (IS_EVEN(i)) i++;

//预处理器会将这些行替换为
i = ((j+k)>(m-n)?(j+k):(m-n)); 
if (((i)%2==0)) i++;

如这个例子所示,带参数的宏经常用作简单的函数。MAX类似一个从两个值中选取较大值的函数,IS_EVEN则类似一种当参数为偶数时返回1,否则返回0的函数。


下面的宏也类似函数,但更为复杂:

#define TOUPPER(c) ('a'<=(c)&&(c)<='z'?(c)-'a'+'A':(c)) 

这个宏检测字符c是否在'a''z'之间。如果在的话,这个宏会用c的值减去'a'再加上'A',从而计算出c所对应的大写字母。如果c不在这个范围,就保留原来的c。[<ctype.h>头文件(23.5节)中提供了一个类似的函数toupper,它的可移植性更好。]

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

#define getchar()  getc(stdin) 

空的参数列表不是必需的,但这样可以使getchar更像一个函数。(没错,这就是<stdio.h>中的getchar。我们将在22.4节中看到,getchar经常实现为宏,也经常实现为函数。)

使用带参数的宏替代真正的函数有2个优点:

  • 程序可能会稍微快些。程序执行时调用函数通常会有些额外开销——存储上下文信息、复制参数的值等,而调用宏则没有这些运行开销。[ 注意,C99的内联函数(18.6节)为我们提供了一种不使用宏而避免这一开销的办法。]
  • 宏更“通用”。与函数的参数不同,宏的参数没有类型。因此,只要预处理后的程序依然是合法的,宏可以接受任何类型的参数。例如,我们可以使用MAX宏从两个数中选出较大的一个,数的类型可以是intlongfloatdouble等。

但是带参数的宏也有一些缺点:

  • 编译后的代码通常会变大。每一处宏调用都会导致插入宏的替换列表,由此导致程序的源代码增加(因此编译后的代码变大)。宏使用得越频繁,这种效果就越明显。当宏调用嵌套时,这个问题会相互叠加从而使程序更加复杂。思考一下,如果我们用MAX宏来找出3个数中最大的数会怎样:

    n = MAX(i, MAX(j, k));
    
    //下面是预处理后的语句:
    n = ((i)>(((j)>(k)?(j):(k)))?(i):(((j)>(k)?(j):(k)))); 
    
  • 宏参数没有类型检查。当一个函数被调用时,编译器会检查每一个参数来确认它们是否是正确的类型。如果不是,要么将参数转换成正确的类型,要么由编译器产生一条出错消息。预处理器不会检查宏参数的类型,也不会进行类型转换。

  • 无法用一个指针来指向一个宏。如在17.7节中将看到的,C语言允许指针指向函数,这在特定的编程条件下非常有用。宏会在预处理过程中被删除,所以不存在类似的“指向宏的指针”。因此,宏不能用于处理这些情况。

  • 宏可能会不止一次地计算它的参数。函数对它的参数只会计算一次,而宏可能会计算两次甚至更多次。如果参数有副作用,多次计算参数的值可能会产生不可预知的结果。考虑下面的例子,其中MAX的一个参数有副作用:

    n = MAX(i++, j);
    
    //预处理之后:
    n = ((i++)>(j)?(i++):(j)); 
    

    如果i大于j,那么i可能会被(错误地)增加两次,同时n可能被赋予错误的值。

请注意!!由于多次计算宏的参数而导致的错误可能非常难于发现,这是因为宏调用和函数调用看起来是一样的。更糟糕的是,这类宏可能在大多数情况下可以正常工作,仅在特定参数有副作用时失效。为了自我保护,最好避免使用带有副作用的参数

带参数的宏不仅适用于模拟函数调用,还经常用作需要重复书写的代码段模式。如果我们已经写烦了语句

printf("%d\n", i); 

这是因为每次要显示一个整数i都要使用它。我们可以定义下面的宏,使显示整数变得简单些:

#define PRINT_INT(n) printf("%d\n", n)

一旦定义了PRINT_INT,预处理器会将

PRINT_INT(i/j); 

//预处理之后转换为:
printf("%d\n", i/j);

14.3.3 #运算符

宏定义可以包含两个专用的运算符:###。编译器不会识别这两种运算符,它们会在预处理时被执行。

#运算符将宏的一个参数转换为字面串它仅允许出现在带参数的宏的替换列表中。(#运算符所执行的操作可以理解为“串化”(stringization),这个词你在字典里肯定看不到。)

#运算符有许多用途,这里只来讨论其中的一种。假设我们决定在调试过程中使用PRINT_INT宏作为一个便捷的方法来输出整型变量或表达式的值。#运算符可以使PRINT_INT为每个输出的值添加标签。下面是改进后的PRINT_INT

#define PRINT_INT(n) printf(#n " = %d\n", n)

n之前的#运算符通知预处理器根据PRINT_INT的参数创建一个字面串。因此,调用

PRINT_INT(i/j); 

会变为

printf("i/j" " = %d\n", i/j); 

//C语言中相邻的字面串会被合并。因此上边的语句等价于
printf("i/j = %d\n", i/j);

当程序执行时,printf函数会同时显示表达式i/j和它的值。例如,如果i11j2的话,输出为i/j = 5

14.3.4 ##运算符

##运算符可以将两个记号(如标识符)“粘合”在一起,成为一个记号。(无须惊讶,##运算符被称为“记号粘合”。)如果其中一个操作数是宏参数,“粘合”会在形式参数被相应的实际参数替换后发生。考虑下面的宏:

#define MK_ID(n) i##n 

MK_ID被调用时(比如MK_ID(1)),预处理器首先使用实际参数(这个例子中是1)替换形式参数n。接着,预处理器将i1合并为一个记号(i1)。下面的声明使用MK_ID创建了3个标识符:

int MK_ID(1), MK_ID(2), MK_ID(3); 

预处理后这一声明变为:

int i1, i2, i3;

##运算符不属于预处理器最经常使用的特性。实际上,想找到一些使用它的情况是比较困难的。为了找到一个有实际意义的##的应用,我们来重新思考前面提到过的MAX宏。如我们所见,当MAX的参数有副作用时会无法正常工作。一种解决方法是用MAX宏来写一个max函数。遗憾的是,仅一个max函数是不够的,我们可能需要一个实际参数是int值的max函数、一个实际参数为float值的max函数,等等。除了实际参数的类型和返回值的类型之外,这些函数都一样。因此,这样定义每一个函数似乎是个很蠢的做法。

解决的办法是定义一个宏,并使它展开后成为max函数的定义。宏只有一个参数type,表示实际参数和返回值的类型。这里还有个问题,如果我们用宏来创建多个max函数,程序将无法编译。(C语言不允许在同一文件中出现两个同名的函数。)为了解决这个问题,我们用##运算符为每个版本的max函数构造不同的名字。下面是宏的形式:

#define GENERIC_MAX(type)        \ 
type type##_max(type x, type y)  \ 
{                                \ 
    return x > y ? x : y;          \ 
}

注意,宏的定义中是如何将type_max相连来形成新函数名的。

现在,假如我们需要一个针对float值的max函数。下面是使用GENERIC_MAX宏来定义这一函数的方法:

GENERIC_MAX(float)

预处理器会将这行代码展开如下:

float float_max(float x, float y) { return x > y ? x : y; } 

14.3.5 宏的通用属性

我们已经讨论过了简单的宏带参数的宏,现在来看一下它们都需要遵守的规则:

  • 宏的替换列表可以包含对其他宏的调用。例如,我们可以用宏PI来定义宏TWO_PI

    #define PI  3.14159 
    #define TWO_PI  (2*PI)
    

    当预处理器在后面的程序中遇到TWO_PI时,会将它替换成(2*PI)。接着,预处理器会重新检查替换列表,看它是否包含其他宏的调用(在这个例子中,调用了宏PI)。预处理器会不断重新检查替换列表,直到将所有的宏名字都替换完为止。

  • 预处理器只会替换完整的记号,而不会替换记号的片段。因此,预处理器会忽略嵌在标识符、字符常量、字面串之中的宏名。例如,假设程序含有如下代码行:

    #define SIZE 256 
    
    int BUFFER_SIZE; 
    
    if (BUFFER_SIZE > SIZE) 
        puts("Error: SIZE exceeded"); 
    

    预处理后这些代码行会变为

    int BUFFER_SIZE; 
    
    if (BUFFER_SIZE > 256) 
        puts("Error: SIZE exceeded"); 
    

    尽管标识符BUFFER_SIZE和字符串"Error: SIZE exceeded"都包含SIZE,但是它们没有被预处理影响。

  • 宏定义的作用范围通常到出现这个宏的文件末尾。由于宏是由预处理器处理的,它们不遵从通常的作用域规则。定义在函数中的宏并不是仅在函数内起作用,而是作用到文件末尾。

  • 宏不可以被定义两遍,除非新的定义与旧的定义是一样的。小的间隔上的差异是被允许的,但是宏的替换列表(和参数,如果有的话)中的记号必须都一致。

  • 宏可以使用#undef指令“取消定义”。#undef指令有如下形式:

    #undef 标识符
    
    //比如
    #undef N
    

    会删除宏N当前的定义。(如果N没有被定义成一个宏,则#undef指令没有任何作用。)#undef指令的一个用途是取消宏的现有定义,以便于重新给出新的定义。

14.3.6 宏定义中的圆括号

在前面定义的宏的替换列表中有大量的圆括号。确实需要它们吗?答案是绝对需要。如果我们少用几个圆括号,宏有时可能会得到意想不到的(而且是不希望有的)结果。

至于在一个宏定义中哪里要加圆括号,有2条规则要遵守:

  • 首先,如果宏的替换列表中有运算符,那么始终要将替换列表放在括号中: #define TWO_PI (2*3.14159)
  • 其次,如果宏有参数,每个参数每次在替换列表中出现时都要放在圆括号中:#define SCALE(x) ((x)*10),没有括号的话,我们将无法确保编译器会将替换列表和参数作为完整的表达式。编译器可能会不按我们期望的方式应用运算符的优先级和结合性规则。

为了展示为替换列表添加圆括号的重要性,考虑下面的宏定义,其中的替换列表没有添加圆括号:

#define TWO_PI 2*3.14159 /*  需要给替换列表加圆括号 */

//在预处理时,语句
conversion_factor = 360/TWO_PI;
//会变成下面这样
conversion_factor = 360/2*3.14159;

除法会在乘法之前执行,产生的结果并不是期望的结果。


当宏有参数时,仅给替换列表添加圆括号是不够的。参数的每一次出现都要添加圆括号。例如,假设SCALE定义如下:

#define SCALE(x) (x*10)   /* 需要给x添加括号 */ 

//假设有语句
j = SCALE(i+1);
//预处理过程中,语句会变成下面这样
j = (i+1*10);
//由于乘法的优先级比加法高,该语句等价于
j = i+10;
//然而,我们希望的是
j = (i+1)*10;

请注意!!在宏定义中缺少圆括号会导致C语言中最让人讨厌的错误。程序通常仍然可以编译通过,而且宏似乎也可以工作,仅在少数情况下会出错。

14.3.7 创建较长的宏

在创建较长的宏时,逗号运算符会十分有用。特别是可以使用逗号运算符来使替换列表包含一系列表达式。例如,下面的宏会读入一个字符串,再把字符串显示出来:

#define ECHO(s) (gets(s), puts(s)) 

gets函数和puts函数的调用都是表达式,因此使用逗号运算符连接它们是合法的。我们甚至可以把ECHO宏当作一个函数来使用:

ECHO(str);   /* 替换为 (gets(str), puts(str)); */ 

如果不想在ECHO的定义中使用逗号运算符,我们还可以将gets函数和puts函数的调用放在花括号中形成复合语句:

#define ECHO(s)  { gets(s);  puts(s);  } 

遗憾的是,这种方式并未奏效。假如我们将ECHO宏用于下面的if语句:

if (echo_flag) 
    ECHO(str);  
else 
    gets(str); 

//将`ECHO`宏替换会得到下面的结果
if (echo_flag) 
    { gets(str);  puts(str);  }; 
else 
    gets(str); 

编译器会将头两行作为完整的if语句:

if (echo_flag) 
    { gets(str);  puts(str);  } 

编译器会将跟在后面的分号作为空语句,并且对else子句抛出出错消息,因为它不属于任何if语句。记住,永远不要在ECHO宏后面加分号,这样做就可以解决这个问题。但是这样做会使程序看起来有些怪异。


逗号运算符可以解决ECHO宏的问题,但并不能解决所有宏的问题。假如一个宏需要包含一系列的语句,而不仅仅是一系列的表达式,这时逗号运算符就起不了作用了,因为它只能连接表达式,不能连接语句。解决的方法是将语句放在do循环中,并将条件设置为假(因此语句只会执行一次):

do { ... } while (0)

注意,这个do语句是不完整的——后面还缺一个分号。为了看到这个技巧(嗯,应该说是技术)的实际作用,将它用于ECHO宏中:

#define ECHO(s)         \ 
        do {            \ 
            gets(s);    \ 
            puts(s);    \ 
        } while (0) 

当使用ECHO宏时,一定要加分号以使do语句完整:

ECHO(str);  
/* becomes do { gets(str); puts(str); } while (0);  */ 

14.3.8 预定义宏

C语言有一些预定义宏,每个宏表示一个整型常量或字面串。如表14-1所示,这些宏提供了当前编译编译器本身的信息

表14-1 预定义宏

名字描述
__LINE__当前宏所在行的行号
__FILE__当前文件的名字
__DATE__编译的日期(格式"mm dd yyyy")
__TIME__编译的时间(格式"hh:mm:ss")
__STDC__如果编译器符合C标准(C89或C99),那么值为1

__DATE__宏__TIME__宏指明程序编译的时间。例如,假设程序以下面的语句开始:

printf("Wacky Windows (c) 2010 Wacky Software, Inc.\n"); 
printf("Compiled on %s at %s\n", __DATE__, __TIME__); 

每次程序开始执行时,程序都会显示如下的两行内容:

Wacky Windows (c) 2010 Wacky Software, Inc.
Compiled on Oct 23 2023 at 17:12:25

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

我们可以使用__LINE__宏__FILE__宏来找到错误。考虑被零除的定位问题。当C程序因为被零除而导致终止时,通常没有信息指明哪条除法运算导致错误。下面的宏可以帮助我们查明错误的根源:

#define CHECK_ZERO(divisor)  \ 
    if (divisor == 0) \ 
        printf("*** Attempt to divide by zero on line %d  "  \ 
                "of file %s  ***\n", __LINE__, __FILE__) 

//CHECK_ZERO宏应该在除法运算前被调用:
CHECK_ZERO(j); 
k = i / j;

//如果j是0,会显示出如下形式的信息:
/* output:
*** Attempt to divide by zero on line 9 of file foo.c ***
*/

类似这样的错误检测的宏非常有用。实际上,C语言库提供了一个通用的、用于错误检测的宏——assert宏(24.1节)。

如果编译器符合C标准(C89C99),__STDC__宏存在且值为1。通过让预处理器测试这个宏,程序可以在早于C89标准的编译器下编译通过(14.4节会给出一个例子)。

14.3.9 C99中新增的预定义宏

表14-2 C99中新增的预定义宏

名字描述
__STDC_HOSTED__如果是托管式实现,则值为1;如果是独立式实现,则值为0
__STDC_VERSION__支持的C标准版本
__STDC_IEC_559__如果支持IEC 60559浮点算术运算,则值为1
__STDC_IEC_559_COMPLEX__如果支持IEC 60559复数算术运算,则值为1
__STDC_ISO_10646__被定义为yyyymmL形式的整型常量,意味着可以用wchar_t类型来存储ISO 10646标准所定义的,以及在指定年月所修订和补充的Unicode字符

要了解__STDC_HOSTED__的意义需要介绍些新的名词。C的实现(implementation)包括编译器和执行C程序所需要的其他软件。C99将实现分为两种:托管式(hosted)独立式(freestanding)托管式实现(hosted implementation)能够接受任何符合C99标准的程序,而独立式实现(freestanding implementation)除了几个最基本的以外,不一定要能够编译使用复数类型(27.3节)或标准头的程序。(特别是,独立式实现不需要支持<stdio.h>头。)如果编译器是托管式实现,则__STDC_HOSTED__宏代表常数1,否则值为0

__STDC_VERSION__宏为我们提供了一种查看编译器所识别出的C标准版本的方法。这个宏第一次出现在C89标准的Amendment 1中,该文档指明宏的值为长整型常量199409L(代表修订的年月)。如果编译器符合C99标准,其值为199901L。对于标准的每一个后续版本(以及每一次后续修订),宏的值都有所变化。

C99编译器可能(也可能没有)另外定义以下3种宏。仅当编译器满足特定条件时才会定义相应的宏:

  • 如果编译器根据IEC 60559标准[IEEE 754标准(7.2节)的别名]执行浮点算术运算,则定义__STDC_IEC_559__宏,且其值为1
  • 如果编译器根据IEC 60559标准执行复数算术运算,则定义__STDC_IEC_559_COMPLEX__宏,且其值为1
  • __STDC_ISO__10646__定义为yyyymmL格式(如199712L)的整型常量,前提是wchar_t类型(25.2节)的值由ISO/IEC 10646标准(包括指定年月的修订版本,25.2节)中的码值表示。

14.3.10 空的宏参数(C99)

C99允许宏调用中的任意或所有参数为空。当然这样的调用需要有和一般调用一样多的逗号(这样容易看出哪些参数被省略了)。

在大多数情况下,实际参数为空的效果是显而易见的。如果替换列表中出现相应的形式参数名,那么只要在替换列表中不出现实际参数即可,不需要替换。例如:

#define ADD(x,y) (x+y)

//经过预处理之后,语句
i = ADD(j,k);
//变成
i = (j+k);

//而赋值语句
i = ADD(,k);
//则变成
i = (+k);

当空参数是###运算符的操作数时,其用法有特殊规定。如果空的实际参数被#运算符“串化”,则结果为""(空字符串)

#define MK_STR(x) #x 
... 
char empty_string[] = MK_STR(); 

//预处理之后,上面的声明变成
char empty_string[] = "";

如果##运算符之后的一个实际参数为空,它将被不可见的“位置标记”记号代替。把原始的记号与位置标记记号相连接,得到的还是原始的记号(位置标记记号消失了)。如果连接两个位置标记记号,得到的是一个位置标记记号。宏扩展完成后,位置标记记号从程序中消失。考虑下面的例子:

#define JOIN(x,y,z) x##y##z 
... 
int JOIN(a,b,c), JOIN(a,b,), JOIN(a,,c), JOIN(,,c); 

//预处理之后,声明变成
int abc, ab, ac, c;

漏掉的参数由位置标记记号代替,这些记号在与非空参数相连接之后消失。JOIN宏的3个参数可以同时为空,这样得到的结果为空。

14.3.11 参数个数可变的宏(C99)

C89中,如果宏有参数,那么参数的个数是固定的。在C99中,这个条件被适当放宽了,允许宏具有可变长度的参数列表(26.1节)。这个特性对于函数来说早就有了,所以应用于宏也不足为奇。

宏具有可变参数个数的主要原因是,它可以将参数传递给具有可变参数个数的函数,如printfscanf。下面给出几个例子:

#define TEST(condition, ...) ((condition)? \ 
    printf("Passed test: %s\n", #condition): \ 
    printf(__VA_ARGS__)) 

...记号(省略号)出现在宏参数列表的最后,前面是普通参数。__VA_ARGS__是一个专用的标识符,只能出现在具有可变参数个数的宏的替换列表中,代表所有与省略号相对应的参数。(至少有一个与省略号相对应的参数,但该参数可以为空。)宏TEST至少要有两个参数,第一个参数匹配condition,剩下的参数匹配省略号。

下面这个例子说明了TEST的使用方法:

TEST(voltage <= max_voltage,  
    "Voltage %d exceeds %d\n", voltage, max_voltage); 

//预处理器将产生如下的输出(重排格式以增强可读性):
((voltage <= max_voltage)? 
    printf("Passed test: %s\n", "voltage <= max_voltage"): 
    printf("Voltage %d exceeds %d\n", voltage, max_voltage)); 

如果voltage不大于max_voltage,程序执行时将显示如下消息:

Passed test: voltage <= max_voltage

否则,将分别显示voltagemax_voltage的值:

voltage 125 exceeds 120

14.3.12 __func__标识符(C99)

C99的另一个新特性是__func__标识符。__func__与预处理器无关,所以实际上与本章内容不相关。但是,与许多预处理特性一样,它也有助于调试,所以在这里一并讨论。

每一个函数都可以访问__func__标识符,它的行为很像一个存储当前正在执行的函数的名字的字符串变量。其作用相当于在函数体的一开始包含如下声明:

static const char __func__[] = "function-name"; 

其中function-name是函数名。这个标识符的存在使得我们可以写出如下的调试宏:

#define FUNCTION_CALLED() printf("%s called\n", __func__); 
#define FUNCTION_RETURNS() printf("%s returns\n", __func__); 

对这些宏的调用可以放在函数体中,以跟踪函数的调用:

void f(void) 
{  
    FUNCTION_CALLED();    /* displays "f called" */ 
    ... 
    FUNCTION_RETURNS();   /* displays "f returns" */ 
}

__func__的另一个用法:作为参数传递给函数,让函数知道调用它的函数的名字。

14.4 条件编译

C语言的预处理器可以识别大量用于支持条件编译的指令。条件编译是指根据预处理器所执行的测试结果来包含或排除程序的片段

14.4.1 #if指令和#endif指令

假如我们正在调试一个程序。我们想要程序显示出特定变量的值,因此将 printf函数调用添加到程序中重要的部分。一旦找到错误,建议保留这些 printf函数调用,以备后用。条件编译允许我们保留这些调用,但是让编译器忽略它们。

下面是我们需要采取的方式。首先定义一个宏,并给它一个非零的值:

#define DEBUG 1

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

#if DEBUG 
printf("Value of i: %d\n", i); 
printf("Value of j: %d\n", j); 
#endif

在预处理过程中,#if指令会测试DEBUG的值。由于DEBUG的值不是0,因此预处理器会将这两个printf函数调用保留在程序中(但#if#endif行会消失)。如果我们将DEBUG的值改为0并重新编译程序,预处理器则会将这4行代码都删除。编译器不会看到这些printf函数调用,所以这些调用就不会在目标代码中占用空间,也不会在程序运行时消耗时间。我们可以将#if-#endif保留在最终的程序中,这样如果程序在运行时出现问题,可以(通过将DEBUG改为1并重新编译来)继续产生诊断信息。

一般来说,#if指令的格式如下:

#if 常量表达式

#endif指令则更简单:

#endif

当预处理器遇到#if指令时,会计算常量表达式的值。如果表达式的值为0,那么#if#endif之间的行将在预处理过程中从程序中删除;否则,#if#endif之间的行会被保留在程序中,继续留给编译器处理——这时#if#endif对程序没有任何影响。

请注意!!#if指令会把没有定义过的标识符当作值为0的宏对待。因此,如果省略DEBUG的定义,测试

#if DEBUG

会失败(但不会产生出错消息),而测试

#if !DEBUG 

会成功。

14.4.2 defined运算符

14.3节中介绍过运算符###,还有一个专用于预处理器的运算符——defined。当defined应用于标识符时,如果标识符是一个定义过的宏则返回1,否则返回0defined运算符通常与#if指令结合使用,可以这样写:

#if defined(DEBUG) 
... 
#endif

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

#if defined DEBUG

因为defined运算符仅检测DEBUG是否有定义,所以不需要给DEBUG赋值:

#define DEBUG

14.4.3 #ifdef指令和#ifndef指令

#ifdef指令测试一个标识符是否已经定义为宏:

#ifdef 标识符

#ifdef指令的使用与#if指令类似:

#ifdef 标识符 
当标识符被定义为宏时需要包含的代码 
#endif

严格地说,并不需要#ifdef,因为可以结合#if指令和defined运算符来得到相同的效果。换言之,指令

#ifdef 标识符

等价于

#if defined(标识符)

#ifndef指令与#ifdef指令类似,但测试的是标识符是否没有被定义为宏:

#ifndef 标识符

//等价于指令
#if !defined(标识符)

14.4.4 #elif指令和#else指令

#if指令、#ifdef指令和#ifndef指令可以像普通的if语句那样嵌套使用。当发生嵌套时,最好随着嵌套层次的增加而增加缩进。一些程序员对每一个#endif都加注释,来指明对应的#if指令测试哪个条件:

#if DEBUG 
... 
#endif /* DEBUG */ 

这种方法有助于更方便地找到#if指令的起始位置。

为了提供更多的便利,预处理器还支持#elif#else指令:

#elif 常量表达式

#else

#elif指令和#else指令可以与#if指令、#ifdef指令和#ifndef指令结合使用,来测试一系列条件:

#if 表达式1 
当表达式10时需要包含的代码 
#elif 表达式2 
当表达式10但表达式20时需要包含的代码 
#else 
其他情况下需要包含的代码 
#endif 

虽然上面的例子使用了#if指令,但#ifdef指令或#ifndef指令也可以这样使用。在#if指令和#endif指令之间可以有任意多个#elif指令,但最多只能有一个#else指令。

14.4.5 使用条件编译

条件编译对于调试是非常方便的,但它的应用并不仅限于此。下面是其他一些常见的应用:

  • 编写在多台机器或多种操作系统之间可移植的程序。下面的例子中会根据WIN32MAC_OSLINUX是否被定义为宏,而将三组代码之一包含到程序中:

    #if defined(WIN32) 
    ... 
    #elif defined(MAC_OS) 
    ... 
    #elif defined(LINUX) 
    ... 
    #endif
    

    一个程序中可以包含许多这样的#if指令。在程序的开头会定义这些宏之一(而且只有一个),由此选择了一个特定的操作系统。例如,定义LINUX宏可以指明程序将运行在Linux操作系统下。

  • 编写可以用不同的编译器编译的程序。不同的编译器可以用于识别不同的C语言版本,这些版本之间会有一些差异。一些会接受标准C,另一些则不会。一些版本会提供针对特定机器的语言扩展;另一些版本则没有,或者提供不同的扩展集。条件编译可以使程序适应于不同的编译器。考虑一下为以前的非标准编译器编写程序的问题。__STDC__宏允许预处理器检测编译器是否支持标准(C89C99)。如果不支持,我们可能必须修改程序的某些方面,尤其是有可能必须用老式的函数声明(见第9章末尾的“问与答”部分)替代函数原型。对于每一处函数声明,我们可以使用下面的代码:

    #if __STDC__ 
    函数原型 
    #else 
    老式的函数声明 
    #endif
    
  • 为宏提供默认定义。条件编译使我们可以检测一个宏当前是否已经被定义了,如果没有,则提供一个默认的定义。例如,如果宏BUFFER_SIZE此前没有被定义的话,下面的代码会给出定义:

    #ifndef BUFFER_SIZE 
    #define BUFFER_SIZE 256 
    #endif
    
  • 临时屏蔽包含注释的代码。我们不能用/*...*/直接“注释掉”已经包含/*...*/注释的代码。然而,我们可以使用#if指令来实现:

    #if 0  
    包含注释的代码行 
    #endif 
    

    将代码以这种方式屏蔽,经常称为“条件屏蔽”。15.2节会讨论条件编译的另外一个常用用途:保护头文件以避免重复包含

14.5 其他指令

在本章的最后,我们将简要地了解一下#error指令、#line指令和#pragma指令。与前面讨论过的指令相比,这些指令更专业,使用频率也低得多。

14.5.1 #error指令

#error指令有如下格式:

#error 消息

其中,消息是任意的记号序列。如果预处理器遇到#error指令,它会显示一条包含消息的出错消息。对于不同的编译器,出错消息的具体形式也可能会不一样。格式可能类似:

Error directive: 消息

//或者
#error 消息

遇到#error指令预示着程序中出现了严重的错误,有些编译器会立即终止编译,不再检查其他错误。

#error指令通常与条件编译指令一起用于检测正常编译过程中不应出现的情况。例如,假定我们需要确保一个程序无法在一台int类型不能存储小于100000的数的机器上编译。允许的最大int值用INT_MAX宏(23.2节)表示,所以我们需要做的就是当INT_MAX宏小于100000时调用#error指令:

#if INT_MAX < 100000 
#error int type is too small 
#endif

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

Error directive: int type is too small 

#error指令通常会出现在#if-#elif-#else序列中的#else部分:

#if defined(WIN32) 
... 
#elif defined(MAC_OS) 
... 
#elif defined(LINUX) 
... 
#else 
#error No operating system specified 
#endif

14.5.2 #line指令

#line指令用来改变程序行的编号方法。(正如你所期望的那样,行的编号通常是按1, 2, 3, ...来进行的。)我们也可以使用这条指令使编译器认为它正在从一个有不同名字的文件中读取程序。

#line指令有两种形式。第一种形式只指定行号:

//#line指令(形式1)
#line n

n必须是1~32767C99中是2147483647)范围内的整数。这条指令导致程序中后续的行被编号为nn+1n+2等。

#line指令的第二种形式同时指定行号和文件名:

#line n "文件"

指令后面的行会被认为来自文件,行号由n开始。n和文件字符串的值可以用宏指定。

#line指令的一种作用是改变__LINE__宏(可能还有__FILE__宏)的值。更重要的是,大多数编译器会使用来自#line指令的信息生成出错消息。例如,假设下列指令出现在文件foo.c的开头:

#line 10 "bar.c"

现在,假设编译器在foo.c的第5行发现一个错误。出错消息会指向bar.c的第13行,而不是foo.c的第5行。(为什么是第13行呢?这是因为指令占据了foo.c的第1行,因此对foo.c的重新编号从第2行开始,并将这一行作为bar.c的第10行。)

乍一看,#line指令使人迷惑。为什么要使出错消息指向另一行,甚至是另一个文件呢?这样不是会使程序变得难以调试吗?

实际上,程序员并不经常使用#line指令。它主要用于那些产生C代码作为输出的程序。最著名的程序之一是yaccYet Another Compiler-Compiler),它是一个用于自动生成编译器的一部分的UNIX 工具(yacc的GNU版本称为bison)。在使用yacc之前,程序员需要准备一个包含yacc所需要的信息以及C代码段的文件。通过这个文件,yacc生成一个C程序y.tab.c,并合并程序员提供的代码。程序员接着按照正常方法编译y.tab.c。通过在y.tab.c中插入#line指令,yacc会使编译器认为代码来自原始文件,也就是程序员写的那个文件。于是,任何编译y.tab.c时产生的出错消息会指向原始文件中的行,而不是y.tab.c中的行。其最终结果是,调试变得更容易,因为出错消息都指向程序员编写的文件,而不是由yacc生成的(那个更复杂的)文件。

14.5.3 #pragma指令

#pragma指令为要求编译器执行某些特殊操作提供了一种方法。这条指令对非常大的程序或需要使用特定编译器的特殊功能的程序非常有用。

#pragma指令有如下形式:

#pragma 记号

其中,记号是任意记号。#pragma指令可以很简单(只跟着一个记号),也可以很复杂:

#pragma data(heap_size => 1000, stack_size => 2000) 

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

C89中没有标准的编译提示(pragma),它们都是在实现中定义的。C993个标准的编译提示,都使用STDC作为#pragma之后的第一个记号。这些编译提示是FP_CONTRACT(23.4节)CX_LIMITED_RANGE(27.4节)FENV_ACCESS(27.6节)

14.5.4 _Pragma运算符(C99)

C99引入了与#pragma指令一起使用的_Pragma运算符。_Pragma表达式可以具有如下形式:

_Pragma (字面串)

遇到该表达式时,预处理器通过移除字符串两端的双引号,并分别用字符"\代替转义序列\"\\来实现对字面串(C99标准中的术语)的“去串化”。表达式的结果是一系列的记号,这些记号被当作pragma指令中的记号。例如:

_Pragma("data(heap_size => 1000, stack_size => 2000)") 

#pragma data(heap_size => 1000, stack_size => 2000)

是一样的。

_Pragma运算符使我们摆脱了预处理器的局限性:预处理指令不能产生其他指令。因为_Pragma是运算符而不是指令,所以可以出现在宏定义中。这使得我们能够在#pragma指令后面进行宏的扩展。

现在来看一个GCC手册中的例子。下面的宏使用了_Pragma运算符:

#define DO_PRAGMA(x) _Pragma(#x) 

宏调用如下:

DO_PRAGMA(GCC dependency "parse.y")

拓展后的结果:

#pragma GCC dependency "parse.y" 

这是GCC支持的一种编译提示。[如果指定的文件(本例中是parse.y)比当前文件(正被编译的文件)还要新,会给出警告消息。]需要注意的是,DO_PRAGMA调用的参数是一系列的记号。DO_PRAGMA定义中的#运算符会导致这些记号被串化为"GCC dependency \"parse.y\"";这个字符串随后作为参数传递给_Pragma运算符,该运算符对其进行去串化操作,从而得到包含原始记号的#pragma指令。

问与答

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

答:是合法的。这就是所谓的空指令,它没有任何作用。一些程序员用空指令作为条件编译模块之间的间隔:

#if INT_MAX < 100000 
# 
#error int type is too small 
# 
#endif

当然,空行也可以。不过#可以帮助读者看清模块的范围。

问2:我不清楚程序中哪些常量需要定义成宏。有没有一些可以参照的规则?

答:一条首要的规则是,除了01以外的每一个数值常量都应该定义成宏。字符常量和字符串常量有一点复杂,因为使用宏来替换字符或字符串常量并不总能够提高程序的可读性。我个人建议在下面的条件下使用宏来替代字符或字面串:(1) 常量被不止一次地使用;(2) 以后可能需要修改常量。根据第二条规则,我不会像这样使用宏:

#define NUL '\0'

尽管有些程序员会使用。

问3:如果要被“串化”的参数包含"\字符,#运算符会如何处理?

答:它会将"转换为\"\转换为\\。考虑下面的宏:

#define STRINGIZE(x) #x 

预处理器会将STRINGIZE("foo")替换为"\"foo\""

问4:我无法使下面的宏正常工作:

#define CONCAT(x,y) x##y 

尽管CONCAT(a,b)会如所期望的那样得到ab,但CONCAT(a,CONCAT(b,c))会给出一个怪异的结果。这是为什么?

答:这是那些连KernighanRitchie都认为“怪异”的规则引起的。替换列表中依赖##的宏通常不能嵌套调用。这里的问题在于CONCAT(a,CONCAT(b,c))不会按照“正常”的方式扩展——CONCAT(b,c)首先得出bc,然后CONCAT(a,bc)给出abc。在替换列表中,位于##运算符之前和之后的宏参数在替换时不被扩展,因此CONCAT(a,CONCAT(b,c))扩展成aCONCAT(b,c),而不会进一步扩展,这是因为没有名为aCONCAT的宏。

有一种办法可以解决这个问题,但不太好看。技巧是再定义一个宏来调用第一个宏:

#define CONCAT2(x,y) CONCAT(x,y) 

CONCAT2(a,CONCAT2(b,c))就会得到我们所期望的结果。在扩展外面的CONCAT2调用时,预处理器会同时扩展CONCAT2(b,c)。这里的区别在于CONCAT2的替换列表不包含##。如果这个也不行,那也不用担心,这种问题并不是经常会遇到。

顺便提一下,#运算符也有同样的问题。如果#x出现在替换列表中,其中x是一个宏参数,其对应的实际参数也不会被扩展。因此,假设N是一个代表10的宏,且STR(x)包含替换列表#x,那么STR(N)扩展的结果为"N",而不是"10"。解决的方法与处理CONCAT时的类似:再定义一个宏来调用STR

问5:如果预处理器重新扫描时又发现了最初的宏名,会如何处理呢?如下面的例子所示:

#define N (2*M) 
#define M (N+1) 

i = N;  /* infinite loop? */

预处理器会将N替换为(2*M),接着将M替换为(N+1)。预处理器还会再次替换N,从而导致无限循环吗?

答:一些早期的预处理器确实会进入无限循环,但新的预处理器不会。按照C语言标准,如果在扩展宏的过程中原先的宏名重复出现的话,宏名不会再次被替换。下面是对i的赋值在预处理之后的形式:

i = (2*(N+1));

一些大胆的程序员会通过编写与保留字或标准库中的函数名同名的宏来利用这一行为。以库函数sqrt为例。sqrt函数(23.3节)计算参数的平方根,如果参数为负数则返回一个由实现定义的值。我们可能希望参数为负数时返回0。由于sqrt是标准库函数,我们无法很容易地修改它。但是我们可以定义一个sqrt宏,使它在参数为负数时返回0

#undef sqrt 
#define sqrt(x) ((x)>=0?sqrt(x):0) 

此后预处理器会截获sqrt的调用,并将它替换成上面的条件表达式。在扫描宏的过程中条件表达式中的sqrt调用不会被替换,因此会被留给编译器处理。(注意在定义sqrt宏之前先使用#undef来删除sqrt定义的用法。在21.1节将看到,标准库允许宏和函数使用同一个名字。在定义我们自己的sqrt宏之前先删除sqrt的定义是一种防御性的措施,以防止库中已经把sqrt定义为宏了。)

问6:我在使用__LINE____FILE__等预定义宏的时候得到出错消息。我需要包含特定的头吗?

答:不需要。这些宏可以由预处理器自动识别。请确保每个宏名的前后有两个下划线,而不是一个

问7:区分“托管式实现”和“独立式实现”的目的是什么?如果独立式实现连<stdio.h>头都不支持,它能有什么用?

答:大多数程序(包括本书中的程序)都需要托管式实现,这些程序需要底层的操作系统来提供输入/输出和其他基本服务。C的独立式实现用于不需要操作系统(或只需要很小的操作系统)的程序。例如,编写操作系统内核时需要用到独立式实现(这时不需要传统的输入/输出,因而不需要
<stdio.h>)。独立式实现还可用于为嵌入式系统编写软件

问8:我觉得预处理器就是一个编辑器。它如何计算常量表达式呢?

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

问9:既然我们可以使用#if指令和defined运算符达到同样的效果,为什么C语言还提供#ifdef指令和#ifndef指令?

答:#ifdef指令和#ifndef指令从20世纪70年代就存在于C语言中了,而defined运算符则是在20世纪80年代的标准化过程中加到C语言中的。因此,实际的问题是,为什么将defined运算符加到C语言中?答案就是defined增加了灵活性。我们现在可以使用#ifdefined运算符来测试任意数量的宏,而不再是只能使用#ifdef#ifndef对一个宏进行测试。例如,下面的指令检查是否FOOBAR被定义了而BAZ没有被定义:

#if defined(FOO) && defined(BAR) && !defined(BAZ) 

问10:我想编译一个还没有写完的程序,因此我“条件屏蔽”未完成的部分:

#if 0 
... 
#endif

编译的时候,我得到了一条指向#if#endif之间某一行的出错消息。预处理器不是简单地忽略#if指令和#endif指令之间的所有行吗?

答:不是的,这些代码行不会被完全忽略。在执行预处理指令前,先处理注释,并把源代码分为多个预处理记号。因此,#if#endif之间未终止的注释会引起出错消息。此外,不成对的单引号或双引号字符也可能导致未定义的行为。


写在最后

本文是博主阅读《C语言程序设计:现代方法(第2版·修订版)》时所作笔记,日后会持续更新后续章节笔记。欢迎各位大佬阅读学习,如有疑问请及时联系指正,希望对各位有所帮助,Thank you very much!


http://www.kler.cn/a/107713.html

相关文章:

  • MFC工控项目实例二十九主对话框调用子对话框设定参数值
  • Llama架构及代码详解
  • jmeter介绍、使用方法、性能测试、现参数化和数据驱动、分布式测试、压力测试、接口测试
  • 大模型时代,呼叫中心部门如何自建一套大模型在线客服?
  • 网站小程序app怎么查有没有备案?
  • AMD CPU下pytorch 多GPU运行卡死和死锁解决
  • elementUI el-table实现鼠标悬浮某一行,在鼠标右侧展示提示信息
  • 【Docker】Docker学习之一:离线安装Docker步骤
  • 在类库中使用ASP.NET Core API
  • jvm关闭时的钩子函数
  • 【PythonGIS】基于高德Api实现批量地址查询经纬度
  • 椭圆曲线点加的应用计算
  • Cookie技术
  • Notepad++安装插件和配置快捷键
  • 雨云OSS服务介绍和使用教程,以及Chevereto图床使用雨云OSS的教程
  • 11月14号|Move生态Meetup相约浪漫土耳其
  • node.js - 上传文件至阿里云oss
  • 如何快速排查SSD IO延迟抖动问题?
  • 信钰证券:华为汽车概念股持续活跃 圣龙股份斩获12连板
  • 汇编运算符和表达式
  • 如何使用gpt提高效率
  • 【每日一题】合并两个有序数组
  • 持续集成部署-k8s-服务发现-Ingress
  • vue 无限滚动插件 vue-seamless-scroll
  • 高级深入--day39
  • react关于类组件this指向