C语言程序设计教程
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.7 预处理

编译预处理也是C语言区别于其他高级语言的特点,是指系统在对源程序进行编译之前,对程序中某些特殊命令行的处理,预处理程序将根据源代码中的预处理命令修改程序。使用预处理功能,可以改善程序的设计环境,提高程序的通用性、可读性、可修改性、可调试性、可移植性和方便性,易于模块化。

其处理过程如图1-17所示。

图1-17 C语言预处理的执行过程

预处理命令有以下几个特点:

①预处理命令是一种特殊的命令。为了区别一般的语句,必须以#开头,结尾不加分号。

②预处理命令一般放在程序的开头部分,其有效范围是从定义开始到文件结束。

C语言中的预处理命令有宏替换、文件包含和条件编译三类。

1.7.1 宏替换命令

宏提供了一种机制,可以用来替换源程序中的字符串。从本质上说就是替换,用一串字符串替换程序中指定的标识符。因此宏定义也叫宏替换。宏替换有两类:不带参数的宏替换和带参数的宏替换。

(1)不带参数的宏替换

①格式。

      #define 标识符 字符串
  

其中,标识符称为宏名,字符串(不需要双引号)称为宏替换体。

②功能。编译之前,预处理程序将程序中该宏定义之后出现的所有标识符(宏名)用指定的字符串进行替换。在源程序通过编译之前,C的编译程序先调用C预处理程序对宏定义进行检查,每发现一个标识符,就用相应的字符串替换。只有在完成了这个过程之后,才将源程序交给编译系统。

【例1-13】预处理。

      #include <stdio.h>
      #define N 10
      void main( )
      {
        int a[N],k;
        for ( k=0; k<N; k++ )
          scanf ("%d", &a[k]);
        for ( k=0; k<N; k++ )
          printf("%d ",a[k]);
        printf("\n");
      }
  

说明:编译该程序之前,预处理程序首先将程序中所有出现的N用10替换,这个过程叫做宏替换。通过引用宏名而不是直接使用变量,不仅可增强程序的可读性,还使程序的修改调整变得容易。例如,要对本例中不同大小的数组输入/输出数据,只须修改宏定义语句就可以了。

③使用宏应当注意的事项。

a.宏定义仅仅是符号替换,不是赋值语句,因此不做语法检查。

b.为了区别程序中其他的标识符,宏名的定义通常用大写字母。

c.双引号中出现的宏名不替换。

例如:#define PI 3.14159

printf ("PI=%f", PI);

结果为:PI=3.14159,双引号中的PI不进行替换。

d.如果要提前结束宏名的使用,程序中可以使用#undef。

e.使用宏定义可以嵌套,即后定义的宏中可以使用先定义的宏。

使用宏可以有以下好处:

a.在输入源程序时,可以节省许多操作。

b.宏定义之后,可以使用多次,因此使用宏可以增强程序的易读性和可靠性。

c.使用宏系统不需额外的开销,因为宏所代表的代码只在宏出现的地方替换,因此并不会引起程序的跳转。

(2)带参数的宏替换

进行宏替换时,可以像使用函数一样,通过实参与形参传递数据,增加程序的灵活性。

①格式。

      #define 标识(形参表)  形参表达式
  

例:#define S(a,b) (a>b)?(a):(b)

②功能。预处理程序将程序中出现的所有带实参的宏名展开成由实参组成的表达式。

带参数的宏替换原理是:程序中如果有带实参的宏,如S(2,5),则按#define命令中指定的字符串从左到右进行替换。如果字符串中包含宏中的形参,如a,b,则用语句中的实参代替相应的形参,如果宏定义字符串中的字符不是形参字符[如(a>b)?(a):(b)中的“?”“:”]则保留。这样就形成了置换后的字符串。

【例1-14】带参数的宏替换。

      #include <stdio.h>
      #define S(a,b)(a>b)?(a):(b)       / * 定义带参数的宏名S* /
      void main( )
      {
        int x,y;
        scanf ("%d %d", &x, &y);
        printf("%d",S(x,y));            / * 将S(x,y)替换成 (x>y)?(x):(y)* /
      }
  

说明:

①宏名与括号之间不可以有空格。

②有些参数在表达式中必须加括号,否则,在实参表达式替换时,会出现错误。

例如:#define S(x) x*x

在程序中,a的值为5,b的值为8,c=S(a+b),替换后的结果为:

      c=a+b*a+b
  

代入a和b的值之后,c=5+8*5+8,值是53,并不是希望的:

      c=(a+b)*(a+b)=13*13=169
  

带参数的宏与函数类似,都有形参与实参。有时从功能上看两者效果是相同的,但两者是不相同的。其主要区别有以下几点。

①函数的形参与实参要求类型一致,而宏替换不要求类型。

②函数只有一个返回值,宏替换可能有多个结果。

③使用宏有可能给程序带来意想不到的副作用。

【例1-15】求整数的平方。

(1)使用函数。

      #include <stdio.h>
      void main( )
      {
        int  FUN(int);                 //函数声明
        int i=0;
        while ( i<=10 )
        printf ("%d, ", FUN(i++) );
      }
      int  FUN(int k)
      {
        return(k*k);
      }
  
  结果:0,1,4,9,16,25,36,49,64,81,100。
  

(2)使用宏。

      #include <stdio.h>
      #define FUN(a) a*a
      void main( )
      {
        int k=1;
        while ( k<=10 )
        printf ( "%d ", FUN(k++) );
        printf("\n");
      }
  

说明:预处理程序将程序中带实参的FUN替换成k++* k++,因此程序运行结果为:

      第一次循环: k++*k++ 为 1*1=1       k=3
      第二次循环: k++*k++ 为 3*3=9       k=5
      第三次循环: k++*k++ 为 5*5=25      k=7
      第四次循环: k++*k++ 为 7*7=49      k=9
      第五次循环: k++*k++ 为 9*9=81      k=11
  

程序运行过程共循环5次。

应当尽量避免用自增变量做宏替换的实参。类似的还有:

      #define SUM(x) x*x*x
  

程序中y=SUM(++x);替换的结果即:y=++x * ++x * ++x

1.7.2 文件包含命令

文件包含是将一个指定文件的内容完全包含到当前文件中,用#include实现。

格式1:#include <文件名>

格式2:#include "文件名"

功能:用指定的文件名的内容代替预处理命令。

例如,调用系统库函数中的字符串处理函数,需在程序的开始使用:

      #include<string.h>
  

表明将string.h的内容嵌入当前程序中。

对文件包含的几点说明。

①两种格式的区别。

a.按格式1定义时,预处理程序在系统所指定的标准目录下查找指定的文件。

b.按格式2定义时,预处理程序首先在引用被包含文件的源文件所在的目录中寻找指定的文件,如没找到,再按系统指定的标准目录查找。

为了提高预处理程序的搜索效率,通常对用户自定义的非标准文件使用格式2,对使用系统库函数等标准文件使用格式1。

②一个#include命令只能包含一个文件。

③被包含的文件一定是文本文件,不可以是可执行程序或目标程序文件。

文件包含在程序设计中非常重要。当用户定义了一些外部变量或宏,可以将这些定义放在一个文件中。例如head.h,凡是需要使用这些定义的程序,只要用文件包含将head.h包含到该程序中,就可以避免再一次对外部变量进行说明,以节省设计人员的重复劳动,既能减少工作量,又可避免出错。

1.7.3 条件编译命令

预处理程序提供了条件编译的功能。可以按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件。这对于程序的移植和调试是很有用的。条件编译有三种形式,下面分别介绍。

(1)第一种形式

      #ifdef 标识符
      程序段1
      #else
      程序段2
      #endif
  

它的功能是:如果标识符已被#define命令定义过,则对程序段1进行编译;否则对程序段2进行编译。如果没有程序段2(它为空),本格式中的#else可以没有,即可以写为:

      #ifdef 标识符
      程序段
      #endif
  

【例1-16】条件编译预处理命令举例。

      / *源程序1-16.C* /
      #include <stdio.h>
      #define NUM ok
      void main()
      {
        struct stu
        {
          int num;
          char *name;
          char sex;
          float score;
        } *ps;
        ps=(struct stu*)malloc(sizeof(struct stu));
        ps->num=102;
        ps->name="Zhang ping";
        ps->sex='M';
        ps->score=62.5;
        #ifdef NUM
        printf("Number=%d\nScore=%f\n",ps->num,ps->score);
        #else
        printf("Name=%s\nSex=%c\n",ps->name,ps->sex);
        #endif
        free(ps);
      }
  

说明:由于在程序中插入了条件编译预处理命令,因此要根据NUM是否被定义过来决定编译哪一个printf语句。而在程序的第一行已对NUM作过宏定义,因此应对第一个printf语句作编译,故运行结果是输出了学生的学号和成绩。在程序的第一行宏定义中,定义NUM表示字符串ok,其实也可以为任何字符串,甚至不给出任何字符串,写为#define NUM也具有同样的意义。只有取消程序的第一行才会去编译第二个printf语句。读者可上机调试。

(2)第二种形式

      #ifndef 标识符
      程序段1
      #else
      程序段2
      #endif
  

与第一种形式的区别是将“ifdef”改为“ifndef”。它的功能是:如果标识符未被#define命令定义过,则对程序段1进行编译,否则对程序段2进行编译。这与第一种形式的功能正相反。

(3)第三种形式

      #if 常量表达式
      程序段1
      #else
      程序段2
      #endif
  

它的功能是:如常量表达式的值为真(非0),则对程序段1进行编译,否则对程序段2进行编译。因此可以使程序在不同条件下完成不同的功能。

【例1-17】条件编译预处理命令举例。

      / *源程序1-17.C* /
      #include <stdio.h>
      #define R 1
      void main()
      {
        float c,r,s;
        printf ("input a number: ");
        scanf("%f",&c);
        #if R
        r=3.14159*c*c;
        printf("area of round is: %f\n",r);
        #else
        s=c*c;
        printf("area of square is: %f\n",s);
        #endif
      }
  

说明:本例中采用了第三种形式的条件编译。在程序第一行宏定义中,定义R为1,因此在条件编译时,常量表达式的值为真,故计算并输出圆面积。上面介绍的条件编译当然也可以用条件语句来实现。但是用条件语句将会对整个源程序进行编译,生成的目标代码程序很长。而采用条件编译,则根据条件只编译其中的程序段1或程序段2,生成的目标程序较短。如果条件选择的程序段很长,采用条件编译的方法是十分必要的。