C/C++预处理命令
1.预处理概述和文件包含命令
前面各章中,已经多次使用过#include
命令。使用库函数之前,应该用#include
引入对应的头文件。这种以#
号开头的命令称为预处理命令。
c语言源文件要经过编译、链接才能生成可执行程序:
1) 编译(compile)会将源文件(.c文件)转换为目标文件。对于vc/vs,目标文件后缀为 .obj;对于gcc,目标文件后缀为 .o。
编译是针对单个源文件的,一次编译操作只能编译一个源文件,如果程序中有多个源文件,就需要多次编译操作。2) 链接(link)是针对多个文件的,它会将编译生成的多个目标文件以及中的库、等合并成一个可执行程序。
不过,在编译之前有时候还需要对源文件进行简单的处理。例如,我们希望自己的程序在windows和linux下都能够运行,那么就要在windows下使用vs编译一遍,然后在linux下使用gcc编译一遍。但是现在有个问题,程序中要实现的某个功能在vs和gcc下使用的函数不同(假设vs下使用 a(),gcc下使用 b()),vs下的函数在gcc下不能编译通过,gcc下的函数在vs下也不能编译通过,怎么办呢?
这就需要在编译之前先对源文件进行处理:如果检测到是vs,那么就保留 a() 删除 b();如果检测到是gcc,那么就保留 b() 删除 a()。
这些在编译之前对源文件进行的简单处理,就称为预处理(即预先处理、提前处理)。
预处理主要是处理以
#
开头的命令,例如#include
等。预处理命令要放在所有函数之外,而且一般都放在源文件的前面。预处理是c语言的一个重要功能,由预处理程序完成。当对一个源文件进行编译时,系统将自动调用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。
编译器会将预处理的结果保存到和源文件同名的
.i
文件中,例如
main.c 的预处理结果在 main.i 中。和.c
一样,.i
也是文本文件,可以用编辑器打开直接查看内容。c语言提供了多种预处理功能,如宏定义、文件包含、条件编译等,合理地使用它们会使编写的程序便于、修改、移植和调试,也有利于模块化程序设计。
#include命令
#include
是文件包含命令,主要用来引入对应的头文件。#include
的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。#include有两种使用方式:
#include #include "myheader.h"使用尖括号
和双引号"
"
的区别在于头文件的搜索路径不同,我们将在《c语言头文件的路径》一节中深入探讨,请大家先记住:包含标准库的头文件一般用尖括号,包含自定义的头文件一般用双引号。说明:
-
一个#include命令只能包含一个头文件,多个头文件需要多个#include命令。文件包含允许嵌套,也就是说在一个被包含的文件中又可以包含另一个文件。
2.c语言宏定义
宏定义是预处理命令的一种,它允许用一个标识符来表示一个字符串。先看一个例子:
#include #define n 100int main(){ int sum = 20 + n; printf("%d\n", sum); return 0;}
运行结果:
120
该示例中的语句int
sum = 20 + n;
,n
被100
代替了。#define
n 100
就是宏定义,n
为宏名,100
是宏的内容。在编译预处理时,对程序中所有出现的“宏名”,都用宏定义中的字符串去代换,这称为“宏代换”或“宏展开”。
宏定义是由源程序中的宏定义命令#define
完成的,宏代换是由预处理程序完成的。
宏定义的一般形式为:
#define 宏名 字符串
#
表示这是一条预处理命令,所有的预处理命令都以#开头。define
是预处理命令。宏名
是标识符的一种,命名规则和标识符相同。字符串
可以是常数、表达式等。
这里所说的字符串是一般意义上的字符序列,不要和c语言中的字符串等同,它不需要双引号。程序中反复使用的表达式就可以使用宏定义,例如:
#define m (n*n+3*n)它的作用是指定标识符
m
来代替表达式(y*y+3*y)
。在编写源程序时,所有的(y*y+3*y)都可由m代替,而对源程序编译时,将先由预处理程序进行宏代换,即用(y*y+3*y)表达式去替换所有的宏名m,然后再进行编译。将上面的例子补充完整:
#include #define m (n*n+3*n)int main(){ int sum, n; printf("input a number: "); scanf("%d", &n); sum = 3*m+4*m+5*m; printf("sum=%d\n", n); return 0;}
运行结果:
input a number: 10↙
sum=1560
上面的程序中首先进行宏定义,定义m来替代表达式(n*n+3*n),在sum=3*m+4*m+5*m
中作了宏调用。在预处理时经宏展开后该语句变为:
sum=3*(n*n+3*n)+4*(n*n+3*n)+5*(n*n+3*n);需要注意的是,在宏定义中表达式
(n*n+3*n)
两边的括号不能少,否则会发生错误。如当作以下定义后:
#difine m n*n+3*n在宏展开时将得到下述语句:
s=3*n*n+3*n+4*n*n+3*n+5*n*n+3*n;这相当于: 3n2+3n+4n2+3n+5n2+3n 这显然是不正确的。所以进行宏定义时要注意,应该保证在宏代换之后不发生错误。
对宏定义的几点说明
1) 宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单的替换。字符串中可以含任何字符,可以是常数,也可以是表达式,预处理程序对它不作任何检查,如有错误,只能在编译已被宏展开后的源程序时发现。2) 宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换。
3) 宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用
#undef
命令。例如:
#define pi 3.14159int main(){ // code return 0;}#undef pivoid func(){ // code}
表示pi只在main函数中有效,在func中无效。
4) 宏名在源程序中若用引号括起来,则预处理程序不对其作宏代换,例如:
#include #define ok 100int main(){ printf("ok\n"); return 0;}
运行结果:
ok
该例中定义宏名ok表示100,但在 printf 语句中 ok 被引号括起来,因此不作宏代换,而作为字符串处理。
5) 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换。例如:
#define pi 3.1415926 #define s pi*y*y /* pi是已定义的宏名*/对语句:
printf("%f", s);在宏代换后变为:
printf("%f", 3.1415926*y*y);
6) 习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。
7) 可用宏定义表示数据类型,使书写方便。例如:
#define uint unsigned int在程序中可用uint作变量说明:
uint a, b;应注意用宏定义表示数据类型和用typedef定义数据说明符的区别。宏定义只是简单的字符串代换,是在预处理完成的,而typedef是在编译时处理的,它不是作简单的代换,而是对类型说明符重新命名。被命名的标识符具有类型定义说明的功能。
请看下面的例子:
#define pin1 int * typedef (int *) pin2;从形式上看这两者相似, 但在实际使用中却不相同。
下面用pin1,pin2说明变量时就可以看出它们的区别:
pin1 a,b;在宏代换后变成:
int *a,b;表示a是指向整型的指针变量,而b是整型变量。然而:
pin2 a,b;表示a、b都是指向整型的指针变量。因为pin2是一个类型说明符。由这个例子可见,宏定义虽然也可表示数据类型, 但毕竟是作字符代换。在使用时要分外小心,以避出错。
3.c语言带参数宏定义
c语言允许宏带有参数。在宏定义中的参数称为形式参数,在宏调用中的参数称为实际参数。
对带参数的宏,在调用中,不仅要宏展开,而且要用实参去代换形参。
带参宏定义的一般形式为:
#define 宏名(形参列表) 字符串
带参宏调用的一般形式为: 宏名(实参列表); 例如:
#define m(y) y*y+3*y //宏定义 // code k=m(5); //宏调用在宏调用时,用实参5去代替形参y,经预处理宏展开后的语句为
k=5*5+3*5
。【示例】输出两个数中较大的数。
#include #define max(a,b) (a>b) ? a : bint main(){ int x , y, max; printf("input two numbers: "); scanf("%d %d", &x, &y); max = max(x, y); printf("max=%d\n", max); return 0;}
运行结果:
input two numbers: 10 20
max=20
程序第2行进行了带参宏定义,用宏名
max
表示条件表达式(a>b)
? a : b
,形参a、b均出现在条件表达式中。程序第7行max=max(x,
y)
为宏调用,实参x、y,将代换形参a、b。宏展开后该语句为:
max=(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
。宏展开时,宏调用语句:
max=max(x,y);将变为:
max=(a,b)(a>b)?a:b(x,y);这显然是错误的。
2) 在带参宏定义中,形式参数不分配内存单元,因此不必作类型说明。而宏调用中的实参有具体的值,要用它们去代换形参,因此必须作类型说明。这是与函数中的情况不同的。在函数中,形参和实参是两个不同的量,各有自己的作用域,调用时要把实参值赋予形参,进行“值传递”。而在带参宏中,只是符号代换,不存在值传递的问题。
3) 在宏定义中的形参是标识符,而宏调用中的实参可以是表达式。
【示例】输入 n,输出 (n+1)^2 的值。
#include #define sq(y) (y)*(y)int main(){ int a, sq; printf("input a number: "); scanf("%d", &a); sq = sq(a+1); printf("sq=%d\n", sq); return 0;}
运行结果:
input a number: 9
sq=100
第2行为宏定义,形参为y。程序第7行宏调用中实参为a+1,是一个表达式,在宏展开时,用a+1代换y,再用(y)*(y) 代换sq,得到如下语句:
sq=(a+1)*(a+1);这与函数的调用是不同的,函数调用时要把实参表达式的值求出来再赋予形参,而宏代换中对实参表达式不作计算直接地照原样代换。
4) 在宏定义中,字符串内的形参通常要用括号括起来以避免出错。在上例中的宏定义中(y)*(y)表达式的y都用括号括起来,因此结果是正确的。如果去掉括号,把程序改为以下形式:
#include #define sq(y) y*yint main(){ int a, sq; printf("input a number: "); scanf("%d", &a); sq = sq(a+1); printf("sq=%d\n", sq); return 0;}
运行结果为:
input a number: 9
sq=19
同样输入9,但结果却是不一样的。问题在哪里呢?这是由于替换只作符号替换而不作其它处理而造成的。宏替换后将得到以下语句:
sq=a+1*a+1;由于a为9故sq的值为19。这显然与题意相违,因此参数两边的括号是不能少的。即使在参数两边加括号还是不够的,请看下面程序:
#include #define sq(y) (y)*(y)int main(){ int a,sq; printf("input a number: "); scanf("%d", &a); sq = 200 / sq(a+1); printf("sq=%d\n", sq); return 0;}
本程序与前例相比,只把宏调用语句改为:
sq=160/sq(a+1);运行本程序如输入值仍为9时,希望结果为2。但实际运行的结果如下:
input a number: 9
sq=200
为什么会得这样的结果呢?分析宏调用语句,在宏代换之后变为:
sq=200/(a+1)*(a+1);a为9时,由于“/”和“*”运算符优先级和结合性相同,则先作200/(9+1)得20,再作20*(9+1)最后得200。为了得到正确答案应在宏定义中的整个字符串外加括号,程序修改如下:
#include #define sq(y) ((y)*(y))int main(){ int a,sq; printf("input a number: "); scanf("%d", &a); sq = 200 / sq(a+1); printf("sq=%d\n", sq); return 0;}
由此可见:对于宏定义不仅应在参数两侧加括号,也应在整个字符串外加括号。
4.c语言带参宏定义和函数的区别
带参的宏和带参函数很相似,但有本质上的不同,把同一表达式用函数处理与用宏处理的结果有可能是不同的。
【示例①】用函数计算平方值。
#include int sq(int y){ return ((y)*(y));}int main(){ int i=1; while(i printf("%d^2 = %d\n", (i-1), sq(i++)); } return 0;}运行结果:
1^2 = 1
2^2 = 4
3^2 = 9
4^2 = 16
5^2 = 25
【示例②】用宏计算平方值。
#include #define sq(y) ((y)*(y))int main(){ int i=1; while(i printf("%d^2 = %d\n", i, sq(i++)); } return 0;}
vc 6.0下运行结果:
1^2 = 1
3^2 = 9
5^2 = 25
c-free(mingw)下运行结果:
3^2 = 1
5^2 = 9
7^2 = 25
之所以出现不同的结果,与 printf() 参数列表中表达式的计算顺序和优先级有关,这里不再深究。分析如下:在示例①中,函数调用是把实参 i 值传给形参 y 后自增 1,然后输出函数值,所以要循环5次,输出1~5的平方值。而在示例②中宏调用时只作代换,sq(i++) 被代换为 ((i++)*(i++))。第一次循环,i 的值为1,(i++)*(i++)=1;第二次循环 i 的值为 3,(i++)*(i++)=9;第三次循环 i 的值为 5,(i++)*(i++)=25;第四次循环,i 的值为7,终止循环。
从以上分析可以看出函数调用和宏调用二者在形式上相似,在本质上是完全不同的。
宏定义也可用来定义多个语句,在宏调用时,把这些语句又代换到源程序内。看下面的例子。
#include #define sssv(s1, s2, s3, v) s1=l*w; s2=l*h; s3=w*h; v=w*l*h;int main(){ int l=3, w=4, h=5, sa, sb, sc, vv; sssv(sa, sb, sc, vv); printf("sa=%d\nsb=%d\nsc=%d\nvv=%d\n", sa, sb, sc, vv); return 0;}
运行结果:
sa=12
sb=15
sc=20
vv=60
5.c语言宏参数的字符串化和宏参数的连接
在宏定义中,有时还会用到#
和##
两个符号,它们能够对宏参数进行操作。
# 的用法
#
用来将宏参数转换为字符串,也就是在宏参数的开头和末尾添加引号。例如有如下宏定义:
#define str(s)那么:
printf("%s", str(c.biancheng.net)); printf("%s", str("c.biancheng.net"));分别被展开为:
printf("%s", "c.biancheng.net"); printf("%s", "\"c.biancheng.net\"");可以发现,即使给宏参数“传递”的数据中包含引号,使用
#
仍然会在两头添加新的引号,而原来的引号会被转义。将上面的例子补充完整:
#include #define str(s) #sint main() { printf("%s\n", str(c.biancheng.net)); printf("%s\n", str("c.biancheng.net")); return 0;}运行结果:
c.biancheng.net
"c.biancheng.net"
##的用法
##
称为连接符,用来将宏参数或其他的串连接起来。例如有如下的宏定义:
#define con1(a, b) a##e##b #define con2(a, b) a##b##00那么:
printf("%f\n", con1(8.5, 2)); printf("%d\n", con2(12, 34));将被展开为:
printf("%f\n", 8.5e2); printf("%d\n", 123400);将上面的例子补充完整:
#include #define con1(a, b) a##e##b#define con2(a, b) a##b##00int main() { printf("%f\n", con1(8.5, 2)); printf("%d\n", con2(12, 34)); return 0;}
运行结果:
850.000000
123400
6.c语言中几个预定义宏
顾名思义,预定义宏就是已经预先定义好的宏,我们可以直接使用,无需再重新定义。
ansi c 规定了以下几个预定义宏,它们在各个编译器下都可以使用:
-
__line__:表示当前源代码的行号;__file__:表示当前源文件的名称;__date__:表示当前的编译日期;__time__:表示当前的编译时间;__stdc__:当要求程序严格遵循ansi c标准时该标识被赋值为1;__cplusplus:当编写c++程序时该标识符被定义。
预定义宏演示:
#include #include int main() { printf("date : %s\n", __date__); printf("time : %s\n", __time__); printf("file : %s\n", __file__); printf("line : %d\n", __line__); system("pause"); return 0;}
vs下的输出结果:
date : mar 6 2016
time : 11:47:15
file : main.c
line : 8
c-free 5.0 下的输出结果:
date : mar 6 2016
time : 12:12:59
file : c:\users\mozhiyan\desktop\demo.c
line : 8
7.c语言条件编译
预处理程序提供了条件编译的功能,可以按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件。这对于程序的移植和调试是很有用的。条件编译有三种形式,下面分别介绍。
第一种形式
第一种形式的格式为:#ifdef 标识符
程序段1
#else
程序段2
#endif
它的功能是,如果标识符已被 #define 命令定义过则对程序段1进行编译;否则对程序段2进行编译。如果没有程序段2(它为空),本格式中的#else可以没有,即可以写为:
#ifdef 标识符
程序段
#endif
请看下面的例子:
#include #define win16 trueint main(void){ #ifdef win16 printf("the value of sizeof(int) is 2.\n"); #else printf("the value of sizeof(int) is 4.\n"); #endif return 0;}运行结果:
the value of sizeof(int) is 2.
第4行插入了条件编译预处理命令,要根据 win16 是否被定义过来决定编译哪一个 printf 语句。而在程序的第2行已对 win16 作过宏定义,所以应对第一个 printf 语句进行编译。
程序第2行宏定义中,定义 win16 表示字符串 true,其实也可以为任何字符串,甚至不给出任何字符串,写为:
#define win16也具有同样的意义。只有取消程序的第2行才会去编译第二个 printf 语句。
第二种形式
第二种形式的格式为:#ifndef 标识符
程序段1
#else
程序段2
#endif
与第一种形式的区别是将
ifdef
改为ifndef
。它的功能是,如果标识符未被#define命令定义过则对程序段1进行编译,否则对程序段2进行编译。这与第一种形式的功能正相反。第三种形式
第三种形式的格式为:#if 常量表达式
程序段1
#else
程序段2
#endif
它的功能是,如常量表达式的值为真(非0),则对程序段1 进行编译,否则对程序段2进行编译。因此可以使程序在不同条件下,完成不同的功能。
请看下面的例子:
#include #define r 1int main(){ float len, area_round, area_square; printf ("input a number: "); scanf("%f", &len); #if r area_round = 3.14159*len*len; printf("area of round is: %f\n", area_round); #else area_square = len*len; printf("area of square is: %f\n", area_square); #endif return 0;}
运行结果:
input a number: 4
area of round is: 50.265442
第2行宏定义中,定义r为1,因此在条件编译时,常量表达式的值为真,所以计算并输出圆面积。
上面介绍的条件编译当然也可以用条件语句 if-else 来实现。 但是用条件语句将会对整个源程序进行编译,生成的目标代码程序较长,而采用条件编译,则根据条件只编译其中的程序段1或程序段2,生成的目标程序较短。如果条件选择的程序段很长,采用条件编译的方法是十分必要的。
8.c语言#error命令,阻止程序编译
#error 指令用于在编译期间产生错误信息,并阻止程序的编译,其形式如下:
#error error_message例如,我们的程序针对linux编写,不保证兼容windows,那么可以这样做:
#ifdef win32#error this programme cannot compile at windows platform#endifwin32 是windows下的预定义宏。当用户在windows下编译该程序时,由于定义了win32这个宏,所以会执行
#error
命令,提示用户发生了编译错误,错误信息是:
this programme cannot compile at windows platform
这和发生语法错误的效果是一样的,程序编译失败。请看下面的截图:
vs2010 下的错误信息
c-free 5.0 下的错误信息
需要注意的是:报错信息不需要加引号
"
"
,如果加上,引号会被一起输出。例如将上面的#error命令改为:
#error "this programme cannot compile at windows platform"那么错误信息如下:
再如,当我们希望以c++的方式来编译程序时,可以这样做:
复制纯文本新窗口
#ifndef __cplusplus#error 当前程序必须以c++方式编译#endif
9.c语言预处理指令总结
预处理指令是以#
号开头的代码行,#号必须是该行除了任何空白字符外的第一个字符。#后是指令关键字,在关键字和#号之间允许存在任意个数的空白字符,整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。
下面是本章涉及到的部分预处理指令:
指令 | 说明 |
---|---|
# | 空指令,无任何效果 |
#include | 包含一个源代码文件 |
#define | 定义宏 |
#undef | 取消已定义的宏 |
#if | 如果给定条件为真,则编译下面代码 |
#ifdef | 如果宏已经定义,则编译下面代码 |
#ifndef | 如果宏没有定义,则编译下面代码 |
#elif | 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个#if……#else条件编译块 |
预处理功能是c语言特有的功能,它是在对源程序正式编译前由预处理程序完成的,程序员在程序中用预处理命令来调用这些功能。
宏定义可以带有参数,宏调用时是以实参代换形参,而不是“值传送”。
为了避免宏代换时发生错误,宏定义中的字符串应加括号,字符串中出现的形式参数两边也应加括号。
文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。
条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。
使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。
上一篇: [C++]高效使用容器的一些建议
下一篇: C#根据屏幕分辨率改变图片尺寸