欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

C/C++预处理命令

程序员文章站 2022-04-08 11:52:40
1.预处理概述和文件包含命令 前面各章中,已经多次使用过#include命令。使用库函数之前,应该用#include引入对应的头文件。这种以#号开头的命令称为预处理命令。 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;n100代替了。

#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#endif
win32 是windows下的预定义宏。当用户在windows下编译该程序时,由于定义了win32这个宏,所以会执行#error命令,提示用户发生了编译错误,错误信息是:

this programme cannot compile at windows platform

这和发生语法错误的效果是一样的,程序编译失败。请看下面的截图:

C/C++预处理命令
vs2010 下的错误信息

C/C++预处理命令
c-free 5.0 下的错误信息


需要注意的是:报错信息不需要加引号" ",如果加上,引号会被一起输出。例如将上面的#error命令改为:
#error "this programme cannot compile at windows platform"
那么错误信息如下:
C/C++预处理命令

再如,当我们希望以c++的方式来编译程序时,可以这样做:
复制纯文本新窗口

#ifndef __cplusplus#error 当前程序必须以c++方式编译#endif


9.c语言预处理指令总结

预处理指令是以#号开头的代码行,#号必须是该行除了任何空白字符外的第一个字符。#后是指令关键字,在关键字和#号之间允许存在任意个数的空白字符,整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。

下面是本章涉及到的部分预处理指令:

指令 说明
# 空指令,无任何效果
#include 包含一个源代码文件
#define 定义宏
#undef 取消已定义的宏
#if 如果给定条件为真,则编译下面代码
#ifdef 如果宏已经定义,则编译下面代码
#ifndef 如果宏没有定义,则编译下面代码
#elif 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码
#endif 结束一个#if……#else条件编译块

预处理功能是c语言特有的功能,它是在对源程序正式编译前由预处理程序完成的,程序员在程序中用预处理命令来调用这些功能。

宏定义可以带有参数,宏调用时是以实参代换形参,而不是“值传送”。

为了避免宏代换时发生错误,宏定义中的字符串应加括号,字符串中出现的形式参数两边也应加括号。

文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。

条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。

使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。