class-notes/课件/C语言/25年上学期C语言讲义.md
2025-02-14 18:24:26 +08:00

32 KiB
Raw Permalink Blame History

预处理器

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

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

预处理器的工作原理

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

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

image-20250214153516183

#include指令告诉预处理器打开一个特定的文件将它的内容作为正在编译的文件的一部分“包含”进来。例如代码行

#include <stdio.h>

指示预处理器打开一个名字为stdio.h的文件并将它的内容加到当前的程序中。stdio.h包含了C语言标准输入/输出函数的原型。)

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

/* Converts a Fahrenheit temperature to Celsius */

#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_PT和SCALE_FACTOR。请注意预处理器并没有删除包含指令的行而是简单地将它们替换为空。

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

预处理指令

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

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

剩下的#error、#line和#pragma指令是更特殊的指令较少用到。本章将深入研究预处理指令。

  • 指令都以#开始。#符号不需要在一行的行首,只要它之前只有空白字符就行。在#后是指令名,接着是指令所需要的其他信息。

  • 在指令的符号之间可以插入任意数量的空格或水平制表符。例如,下面的指令是合法的:

    #     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 */
    

宏定义

简单的宏

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

[#define指令(简单的宏)]
#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和;两个记号。

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

简单的宏主要用来定义那些被Kernighan和Ritchie称为“明示常量”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.1416或3.14195。

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

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

    #define BEGIN {
    #define END   }
    

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

    #define LOOP for (;;)
    

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

  • 对类型重命名。在5.2节中我们通过重命名int创建了一个布尔类型 虽然有些程序员会使用宏定义的方式来实现此目的,但typedef仍然是定义新类型的最佳方法。

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

当宏作为常量使用时C程序员习惯在名字中只使用大写字母。但是并没有如何将用于其他目的的宏大写的统一做法。由于宏特别是带参数的宏可能是程序中错误的来源所以一些程序员更喜欢全部使用大写字母来引起注意。有些人则倾向于小写即按照Kernighan和Ritchie编写的The C Programming Language一书中的风格。

带参数的宏

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

[#define指令带参数的宏]

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

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

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

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

#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>头文件

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

#define getchar()  getc(stdin)

空的参数列表不是必需的但这样可以使getchar更像一个函数。没错这就是<stdio.h>中的getchar。

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

  • 程序可能会稍微快些。程序执行时调用函数通常会有些额外开销——存储上下文信息、复制参数的值等,而调用宏则没有这些运行开销。

  • 宏更“通用”。与函数的参数不同宏的参数没有类型。因此只要预处理后的程序依然是合法的宏可以接受任何类型的参数。例如我们可以使用MAX宏从两个数中选出较大的一个数的类型可以是int、long、float、double等。

  • 编译后的代码通常会变大。每一处宏调用都会导致插入宏的替换列表由此导致程序的源代码增加因此编译后的代码变大。宏使用得越频繁这种效果就越明显。当宏调用嵌套时这个问题会相互叠加从而使程序更加复杂。思考一下如果我们用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);

#运算符

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

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

#运算符有许多用途这里只来讨论其中的一种。假设我们决定在调试过程中使用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和它的值。例如如果i是11j是2的话输出为

i/j = 5

样例

#include <stdio.h>

#define PRINT_FLOAT(n) printf(#n " = %f\n", n)

int main(){

    PRINT_FLOAT(1/2);
    PRINT_FLOAT(1/2.0);
    PRINT_FLOAT(1/(float)2);
    PRINT_FLOAT(1/2.f);

    return 0;
}

##运算符

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

#define MK_ID(n) i##n

当MK_ID被调用时比如MK_ID(1)预处理器首先使用实际参数这个例子中是1替换形式参数n。接着预处理器将i和1合并成为一个记号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函数构造不同的名字。下面是宏的形式

#include <stdio.h>

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

GENERIC_MAX(int);

int main()
{
    
    printf("%d\n", int_max(1, 2));

    return 0;
}

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

宏的通用属性

  • 宏的替换列表可以包含对其他宏的调用。例如我们可以用宏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");
    

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

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

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

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

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

  • 宏可以使用#undef指令“取消定义”。#undef指令有如下形式 [#undef指令] #undef 标识符 其中标识符是一个宏名。例如,指令

    #undef N
    

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

宏定义中的圆括号

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

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

#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语言中最让人讨厌的错误。程序通常仍然可以编译通过而且宏似乎也可以工作仅在少数情况下会出错。

创建较长的宏

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

#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宏中

#include <stdio.h>

#define ECHO(s)               \
    do                        \
    {                         \
        fgets(s, 100, stdin); \
        puts(s);              \
                              \
    } while (0)

int main()
{
    char str[100];

    ECHO(str);

    return 0;
}

预定义宏

名字 描述
_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 Dec 23 2010 at 22:18:48

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

我们可以使用__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会显示出如下形式的信息

*** Attempt to divide by zero on line 9 of file foo.c ***

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

如果编译器符合C标准C89或C99__STDC__宏存在且值为1。通过让预处理器测试这个宏程序可以在早于C89标准的编译器下编译通过。

条件编译

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

#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指令] #if  常量表达式
#endif指令则更简单
[#endif指令] #endif

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

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

#if DEBUG

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

#if !DEBUG

会成功。

defined运算符

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

#if defined(DEBUG)
...
#endif

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

#if defined DEBUG

由于defined运算符仅检测DEBUG是否有定义所以不需要给DEBUG赋值

#define DEBUG

#ifdef指令和#ifndef指令

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

[#ifdef指令] #ifdef  标识符

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

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

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

#ifdef 标识符

等价于

#if defined(标识符)

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

[#ifndef指令] #ifndef  标识符

指令

#ifndef 标识符

等价于指令

#if !defined(标识符)

#elif指令和#else指令

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

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

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

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

[#elif指令] #elif  常量表达式
[#else指令] #else

#elif指令和#else指令可以与#if指令、#ifdef指令和

#ifndef指令结合使用来测试一系列条件

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

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

使用条件编译

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

编写在多台机器或多种操作系统之间可移植的程序

下面的例子中会根据WIN32、MAC_OS或LINUX是否被定义为宏而将三组代码之一包含到程序中

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

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

编写可以用不同的编译器编译的程序

不同的编译器可以用于识别不同的C语言版本这些版本之间会有一些差异。一些会接受标准C另外一些则不会。一些版本会提供针对特定机器的语言扩展有些版本则没有或者提供不同的扩展集。条件编译可以使程序适应于不同的编译器。考虑一下为以前的非标准编译器编写程序的问题。__STDC__宏允许预处理器检测编译器是否支持标准C89或C99

#if __STDC__
函数原型
#else
老式的函数声明
#endif

为宏提供默认定义

条件编译使我们可以检测一个宏当前是否已经被定义了如果没有则提供一个默认的定义。例如如果宏BUFFER_SIZE此前没有被定义的话下面的代码会给出定义

#ifndef BUFFER_SIZE
#define BUFFER_SIZE 256
#endif

临时屏蔽包含注释的代码

我们不能用/*...*/直接“注释掉”已经包含/*...*/注释的代码。然而,我们可以使用#if指令来实现

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

将代码以这种方式屏蔽掉经常称为“条件屏蔽”。

练习题

  1. 编写宏来计算下面的值。  (a) x的立方。  (b) n除以4的余数。  (c) 如果x与y的乘积小于100则值为1否则值为0。  你写的宏始终正常工作吗如果不是哪些参数会导致失败呢
  2. 编写一个宏NELEMS(a)来计算一维数组a中元素的个数。提示见8.1节中有关sizeof运算符的讨论。
  3. 假定DOUBLE是如下宏 #define DOUBLE(x) 2*x (a) DOUBLE(1+2)的值是多少? (b) 4/DOUBLE(2)的值是多少? (c) 改正DOUBLE的定义。
  4. 针对下面每一个宏,举例说明宏的问题,并提出修改方法。 (a) #define AVG(x,y) (x+y)/2 (b) #define AREA(x,y) (x)*(y)

结构、联合和枚举

结构变量

结构类型

嵌套的数组和结构

联合

枚举


指针的高级应用

动态内存分配

动态分配字符串

动态分配数组

释放存储空间

链表

指向指针的指针

指向函数的指针


标准库

标准库的使用

C89标准库概述

C99标准库更新

<stddef.h>:常用定义

<stdbool.h>:布尔类型和值


标准库库对数值和字符数据的支持

<float.h>:浮点类型的特性

<limits.h>:整数类型的大小

<math.h>数学计算C89

<math.h>数学计算C99

<ctype.h>:字符处理


输入/输出

文件操作

格式化的输入/输出

字符的输入/输出

行的输入/输出

块的输入/输出

文件定位

字符串的输入/输出