在 file2.c 文件中没有定义变量 x,y,ch,而是用 extern 声明 x,y,ch 是外部变量,因此 在 file1.c 中已定义的变量在 file2.c 中可以引用。x,y 在 file1.c 中被赋值,它们在 file2.c 中也 作为全局变量,因此 printf 语句输出 12 和 24。同样,在 file2.c 中对 ch 赋值'a',在 file1.c 中也 能引用它的值。当然,要注意操作的先后顺序,只有先赋值才能引用。
(2)限定本文件的外部变量只在本文件中使用。如果有的外部变量只允许本文件使用而 不允许其他文件使用,则可以在此外部变量前加一个 static,使其有限局部化,称作静态外部 变量。例如:
static int a=3,b=5;
void main() {
……
}
void f1() {
……
}
void f() {
……
}
在本文件中,a,b 为全局变量,但作用域也仅限于本文件。
使用静态外部变量的好处是:当多人分别编写一个程序的不同文件时,可以按照需要命 名变量,而不必考虑是否会与其他文件中的变量同名,以保证文件的独立性。
由此,可以将这四类存储属性的性质总结如表 61 所示。
表 61 四类存储属性的性质
存储属性 register auto static extern
存储位置 寄存器 主存
生存期 动态存储 永久存储
作用域 局部 局部或全局 全局
6.6 编译预处理
编译预处理是在编译前对源程序进行的一些预处理。预处理由编译系统中的预处理程序 按源程序中的预处理命令进行。
C 语言的预处理命令均以“#”打头,末尾不加分号,以区别于 C 语句。它们可以出现在 程序中的任何位置, 其作用域是自出现点到所在源程序的末尾。 前面我们已经使用过两个预处 理命令:#define 和#include。
编译预处理是 C 语言的一个重要特点,合理地使用预处理功能编写的程序便于阅读、修
改、移植和调试,也有利于模块化程序设计。本节介绍常用的几种预处理功能。
6.6.1 宏定义
在 C 语言源程序中允许用一个标识符来表示一个字符串,称为“宏” 。被定义为“宏”的 标识符称为“宏名” 。在编译预处理时,对程序中所有出现的“宏名” ,都用宏定义中的字符串 去代换,这称为“宏代换”或“宏展开” 。
宏定义是由源程序中的宏定义命令完成的,宏代换是由预处理程序自动完成的。
在 C 语言中, “宏”分为有参数和无参数两种。下面分别讨论这两种“宏”的定义和调用。
1.无参宏定义
无参宏的宏名后不带参数。其定义的一般形式为:
#define 宏名 字符串
其中,作为宏名的标识符习惯上用有意义且易理解的大写字母来表示, “字符串”可以是 常数、表达式或格式串等。宏定义一般写在文件开头函数体的外面,有效范围是从定义宏命令 之后到遇到终止宏定义命令#undef 为止,否则其作用域将一直到源文件结束。
例如:
#define PI 3.1415926
即定义了宏名 PI 来代表 3.1415926。在编译预处理时,系统将把该命令之后作用域之内的所有 PI 都自动用 3.1415926 代换, 即进行宏展开。 实际上这就是前面介绍过的符号常量的定义形式。
使用宏定义,一方面可以减少频繁使用的字符串的重复书写;另一方面也使得修改重复 使用的字符串的工作变得简单了,因为只需在宏定义处修改一次即可。
对于宏定义还要说明以下几点:
(1)宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一 种简单的代换,字符串中可以含任何字符,可以是常数,也可以是表达式,预处理程序对它不 作任何检查。如有错误,只能在编译已被宏展开后的源程序时发现。
例如:
#define PI "3.1415926"
那么,在预处理时将把 PI 替换为带双引号的字符串"3.1415926",代入表达式中就会出错。
(2)如果在一行中写不下整个宏定义,需要用两行或更多行来书写时,只需在每行的最 后一个字符的后面加上反斜杆“\” ,并在下一行的最开始接着写即可。
(3)宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作 用域可使用#undef 命令。
例如:
#define PI 3.14159 void main()
{
……
}
#undef PI f1() {
……
}
表示 PI 只在 main 函数中有效,在 f1 中无效。
(4)宏名在源程序中若用引号括起来,则预处理程序不对其作宏代换。
例如,如果程序中有以下语句:
printf("PI=",PI);
在预处理时,将只对第二个 PI 进行代换,而对第一个双引号中的 PI,系统并不对其作代换。
执行该语句会输出:
PI=3.1415926
(5)宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名。在宏展开时由预 处理程序层层代换。
例如:
#define R 5.6
#define PI 3.1415926
#define L 2*PI*R
#define S PI*R*R /* PI,R 是已定义的宏名*/
在宏展开时,编译器会把程序中的 R 用 5.6 来代换,把 PI 用 3.1415926 来代换,而将 S 用 3.1415926*5.6*5.6 来代换。
【例 6.17】 已知半径为 5.0,计算以此为半径的圆的周长和面积,以及圆球体的体积。
#include"stdio.h"
#define R 5.0
#define PI 3.1415926
#define S PI*R*R
#define V 4.0/3.0*PI*R*R*R void main()
{
printf("L=%f\nS=%f\nV=%f\n",L,S,V) ; }
运行结果如下:
L=31.415926 S=78.539815 V=523.598767 2.带参宏定义
C 语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参 数。对带参数的宏,在调用中,不仅要进行宏展开,而且要用实参去代换形参。
带参宏定义的一般形式为:
#define 宏名(形参表列) 字符串 在字符串中含有各个形参。
带参宏调用的一般形式为:
宏名(实参表列);
例如:
#define M(y) y*y+3*y /*宏定义*/
……
k=M(5); /*宏调用*/
……
在宏调用时,用实参 5 去代替形参 y,经预处理宏展开后的语句为:
k=5*5+3*5;
【例 6.18】 求两数中较大者。
#include"stdio.h"
#define MAX(a,b) (a>b)?a:b void main()
{
int x,y,mymax;
printf("请输入 x,y 的值:\n");
scanf("%d,%d",&x,&y);
mymax=MAX(x,y);
printf("max=%d\n",mymax);
}
上例程序的第二行进行带参宏定义,用宏名 MAX 表示条件表达式(a>b)?a:b,形参 a,b 均出现在条件表达式中。程序第 8 行 mymax=MAX(x,y)为宏调用,实参 x,y 将代换形参 a,b。
宏展开后该语句为:
mymax=(x>y)?x:y;
用于计算 x,y 中的较大者。
对于带参的宏定义有以下问题需要说明:
(1)带参宏定义中,宏名和形参表之间不能有空格出现。
例如,把
#define MAX(a,b) (a>b)?a:b 写为:
#define MAX(a,b) (a>b)?a:b
将被认为是无参宏定义,宏名 MAX 代表字符串 (a,b) (a>b)?a:b。宏展开时,宏调用语句:
mymax=MAX(x,y);
将变为:
mymax=(a,b)(a>b)?a:b(x,y);
这显然是错误的。
(2)在带参宏定义中,形式参数不分配内存单元,因此不必作类型定义。而宏调用中的 实参有具体的值,要用它们去替换形参,因此必须作类型说明,这与函数中的情况是不同的。
在函数中,形参和实参是两个不同的量,各有自己的作用域,调用时要把实参值赋予形参,进 行“值传递” 。而在带参宏中,只是符号代换,不存在值传递的问题。
(3)在宏定义中的形参是标识符,而宏调用中的实参可以是表达式。
例如:
#define SQ(y) (y)*(y)
……
sq=SQ(a+1);
……
宏定义中形参为 y,宏调用时实参为 a+1,是一个表达式,在宏展开时,用 a+1 代换 y,
再用(y)*(y) 代换 SQ,得到如下语句:
sq=(a+1)*(a+1);
这与函数的调用是不同的,函数调用时要把实参表达式的值求出来再传递给形参。而宏 代换中对实参表达式不作计算直接照原样代换。
(4)在宏定义中,字符串内的形参通常要用括号括起来以避免出错。
【例6.19】 计算下列函数值:
3 3
f (x)=x +(x+ 1) 其中自变量 x 的值从键盘输入。
如果将计算 x 的值定义为一个带参数的宏,即3
#define F(x) x*x*x
计算函数值 f (x) 的 C 程序写成如下:
#include<stdio.h>
#define F(x) x*x*x main()
{ double f,x;
printf("input x:");
scanf("%lf",&x);
f=F(x)+F(x+1);
printf("f=%f\n",f);
}
运行时, 当输入 1 后, 结果为 5.0, 而不是期望的 9.0。 因为当 C 编译系统编译预处理 F(x+1) 时,经宏展开后,等价于“x+1*x+1*x+1” ,而不等价于“(x+1)*(x+1)*(x+1)” 。因此,为了使 定义的宏展开合理,就必须将宏定义字符串中的参数都用括号括起来,即将上述程序改为:
#include<stdio.h>
#define F(x) (x)*(x)*(x) main()
{ double f,x;
printf("input x:");
scanf("%lf",&x);
f=F(x)+F(x+1);
printf("f=%f\n",f);
}
经编译预处理后等价于下面的程序:
#include<stdio.h>
main()
{ double f,x;
printf("input x:");
scanf("%lf",&x);
f=(x)*(x)*(x)+(x+1)*(x+1)*(x+1);;
printf("f=%f\n",f);
}
在使用带参数的宏定义时,除了应将宏定义字符串中的参数都要用括号括起来,还需要 将整个字符串部分也用括号括起来,否则经过宏展开后,还可能出现意想不到的错误。下面的 例子说明了这个问题。
【例 6.20】 计算下列函数值:
3 2 3 2
f (x, y)=(x +x )[(y 1)+ +(y 1) ] + 其中子变量 x 的值从键盘输入。
如果将计算 x3+ x 2 的值定义为一个带参数的宏,即
#define F(x) (x)*(x)*(x)+(x)*(x)
此时,在程序中就可以将 x3+ x 2 写成 F(x),将 (y 1)+ 3+(y 1) + 2 写成 F(x+1)。计算函数 f (x, y) 的 C 程序就可以写成:
#include<stdio.h>
#define F(x) (x)*(x)*(x)+(x)*(x) main()
{ double f,x,y;
printf("input x,y:");
scanf("%lf,%lf",&x,&y); /*输入的两个数之间用逗号做分隔符*/
f=F(x)*F(y+1);
printf("f=%f\n",f);
}
这个程序经编译预处理宏展开后 ,赋值语句:
f=F(x)*F(y+1);
等价于语句:
3 2 3 2
f =x +x * (y 1)+ +(y 1) + 这显然是错误的。正确的应该是:
3 2 3 2
f =(x +x ) * ((y 1)+ +(y 1) ) +
有这个例子可以看出,为了使定义的宏展开合理,不仅需要将宏定义字符串中的参数都 括起来,还必须将整个字符串用括号括起来。即将上述程序修改为:
#include<stdio.h>
#define F(x) ((x)*(x)*(x)+(x)*(x)) main()
{ double f,x,y;
printf("input x,y:");
scanf("%lf,%lf",&x,&y);/*输入的两个数之间用逗号做分隔符*/
f=F(x)*F(y+1);
printf("f=%f\n",f);
}
此时上面的程序等价于下面的程序:
#include<stdio.h>
main()
{ double f,x,y;
printf("input x,y:");
scanf("%lf,%lf",&x,&y);/*输入的两个数之间用逗号做分隔符*/
f=(x 3 +x 2 )*((y+1) 3 +(y+1) 2 );
printf("f=%f\n",f);
}
有上面的分析可以看出,在使用带参数的宏定义时,一般应将宏定义字符串中的参数都 用括号括起来,并且整个字符串部分也要用括号括起来,这样才能保证在任何替代情况下,把 宏定义作为一个整体来看待, 从而得到一个合理的计算顺序, 否则经过宏展开后可能出现意想 不到的错误。
在 C 程序中,可以利用带参数的宏定义来表示一些比较简单的函数表达式。
(5)带参的宏和带参函数很相似,但有本质上的不同,除上面已讲到的几点外,把同一 表达式用函数处理与用宏处理两者的结果有可能是不同的。
【例 6.21】 用用户自定义函数求 n 2 。
#include"stdio.h"
void main() {
int i=1;
while (i<=5)
printf("%d\n",sq(i++));
}
int sq(int y) {
return((y)*(y));
}
运行结果如下:
1 4 9 16 25
【例 6.22】 用宏定义函数求 n 2 。
#include"stdio.h"
#define SQ(y) ((y)*(y)) void main()
{
int i=1;
while (i<=5)
printf("%d\n",SQ(i++));
}
运行结果如下:
2 12 30
在例 6.18 中函数名为 sq,形参为 y,函数体表达式为((y)*(y))。在例 6.19 中宏名为 SQ,
形参也为 y,字符串表达式为((y)*(y))。例 6.18 的函数调用为 sq(i++),例 6.19 的宏调用为 SQ(i++),实参也是相同的。从输出结果来看,却大不相同。
分析:在例 6.18 中,函数调用是把实参 i 值传给形参 y 后自增 1,然后输出函数值。因而 要循环 5 次,输出 1~5 的平方值。而在例 6.19 中宏调用时,只作代换。SQ(i++)被代换为 ((i++)*(i++))。在第一次循环时,由于 i 等于 1,其计算过程为:表达式中前一个 i 初值为 1,
然后 i 自增 1 变为 2,因此表达式中第 2 个 i 初值为 2,相乘的结果也为 2,然后 i 值再自增 1
然后 i 自增 1 变为 2,因此表达式中第 2 个 i 初值为 2,相乘的结果也为 2,然后 i 值再自增 1