C/C++预处理命令

来源:互联网 发布:任我游电子狗升级数据 编辑:程序博客网 时间:2024/06/05 06:16

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 <stdio.h>等。预处理命令要放在所有函数之外,而且一般都放在源文件的前面。

预处理是C语言的一个重要功能,由预处理程序完成。当对一个源文件进行编译时,系统将自动调用预处理程序对源程序中的预处理部分作处理,处理完毕自动进入对源程序的编译。

编译器会将预处理的结果保存到和源文件同名的.i文件中,例如 main.c 的预处理结果在 main.i 中。和.c一样,.i也是文本文件,可以用编辑器打开直接查看内容。

C语言提供了多种预处理功能,如宏定义、文件包含、条件编译等,合理地使用它们会使编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。

#include命令

#include是文件包含命令,主要用来引入对应的头文件。#include的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。

#include有两种使用方式:
#include <stdio.h>#include "myHeader.h"
使用尖括号< >和双引号" "的区别在于头文件的搜索路径不同,我们将在《C语言头文件的路径》一节中深入探讨,请大家先记住:包含标准库的头文件一般用尖括号,包含自定义的头文件一般用双引号。

说明:
  • 一个#include命令只能包含一个头文件,多个头文件需要多个#include命令。
  • 文件包含允许嵌套,也就是说在一个被包含的文件中又可以包含另一个文件。

2.C语言宏定义
宏定义是预处理命令的一种,它允许用一个标识符来表示一个字符串。先看一个例子:
  1. #include <stdio.h>
  2. #define N 100
  3. int main(){
  4. int sum = 20 + N;
  5. printf("%d\n", sum);
  6. return 0;
  7. }
运行结果:
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,然后再进行编译。

将上面的例子补充完整:
  1. #include <stdio.h>
  2. #define M (n*n+3*n)
  3. int main(){
  4. int sum, n;
  5. printf("Input a number: ");
  6. scanf("%d", &n);
  7. sum = 3*M+4*M+5*M;
  8. printf("sum=%d\n", n);
  9. return 0;
  10. }
运行结果:
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命令。例如:
  1. #define PI 3.14159
  2. int main(){
  3. // Code
  4. return 0;
  5. }
  6. #undef PI
  7. void func(){
  8. // Code
  9. }
表示PI只在main函数中有效,在func中无效。

4) 宏名在源程序中若用引号括起来,则预处理程序不对其作宏代换,例如:
  1. #include <stdio.h>
  2. #define OK 100
  3. int main(){
  4. printf("OK\n");
  5. return 0;
  6. }
运行结果:
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  //宏定义// Codek=M(5);  //宏调用
在宏调用时,用实参5去代替形参y,经预处理宏展开后的语句为k=5*5+3*5

【示例】输出两个数中较大的数。
  1. #include <stdio.h>
  2. #define MAX(a,b) (a>b) ? a : b
  3. int main(){
  4. int x , y, max;
  5. printf("input two numbers: ");
  6. scanf("%d %d", &x, &y);
  7. max = MAX(x, y);
  8. printf("max=%d\n", max);
  9. return 0;
  10. }
运行结果:
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 的值。
  1. #include <stdio.h>
  2. #define SQ(y) (y)*(y)
  3. int main(){
  4. int a, sq;
  5. printf("input a number: ");
  6. scanf("%d", &a);
  7. sq = SQ(a+1);
  8. printf("sq=%d\n", sq);
  9. return 0;
  10. }
运行结果:
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都用括号括起来,因此结果是正确的。如果去掉括号,把程序改为以下形式:
  1. #include <stdio.h>
  2. #define SQ(y) y*y
  3. int main(){
  4. int a, sq;
  5. printf("input a number: ");
  6. scanf("%d", &a);
  7. sq = SQ(a+1);
  8. printf("sq=%d\n", sq);
  9. return 0;
  10. }
运行结果为:
input a number: 9
sq=19
同样输入9,但结果却是不一样的。问题在哪里呢?这是由于替换只作符号替换而不作其它处理而造成的。宏替换后将得到以下语句:
sq=a+1*a+1;
由于a为9故sq的值为19。这显然与题意相违,因此参数两边的括号是不能少的。即使在参数两边加括号还是不够的,请看下面程序:
  1. #include <stdio.h>
  2. #define SQ(y) (y)*(y)
  3. int main(){
  4. int a,sq;
  5. printf("input a number: ");
  6. scanf("%d", &a);
  7. sq = 200 / SQ(a+1);
  8. printf("sq=%d\n", sq);
  9. return 0;
  10. }
本程序与前例相比,只把宏调用语句改为:
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。为了得到正确答案应在宏定义中的整个字符串外加括号,程序修改如下:
  1. #include <stdio.h>
  2. #define SQ(y) ((y)*(y))
  3. int main(){
  4. int a,sq;
  5. printf("input a number: ");
  6. scanf("%d", &a);
  7. sq = 200 / SQ(a+1);
  8. printf("sq=%d\n", sq);
  9. return 0;
  10. }
由此可见:对于宏定义不仅应在参数两侧加括号,也应在整个字符串外加括号。

4.C语言带参宏定义和函数的区别
带参的宏和带参函数很相似,但有本质上的不同,把同一表达式用函数处理与用宏处理的结果有可能是不同的。

【示例①】用函数计算平方值。
  1. #include <stdio.h>
  2. int SQ(int y){
  3. return ((y)*(y));
  4. }
  5. int main(){
  6. int i=1;
  7. while(i<=5){
  8. printf("%d^2 = %d\n", (i-1), SQ(i++));
  9. }
  10. return 0;
  11. }
运行结果:
1^2 = 1
2^2 = 4
3^2 = 9
4^2 = 16
5^2 = 25

【示例②】用宏计算平方值。
  1. #include <stdio.h>
  2. #define SQ(y) ((y)*(y))
  3. int main(){
  4. int i=1;
  5. while(i<=5){
  6. printf("%d^2 = %d\n", i, SQ(i++));
  7. }
  8. return 0;
  9. }
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,终止循环。

从以上分析可以看出函数调用和宏调用二者在形式上相似,在本质上是完全不同的。

宏定义也可用来定义多个语句,在宏调用时,把这些语句又代换到源程序内。看下面的例子。
  1. #include <stdio.h>
  2. #define SSSV(s1, s2, s3, v) s1=l*w; s2=l*h; s3=w*h; v=w*l*h;
  3. int main(){
  4. int l=3, w=4, h=5, sa, sb, sc, vv;
  5. SSSV(sa, sb, sc, vv);
  6. printf("sa=%d\nsb=%d\nsc=%d\nvv=%d\n", sa, sb, sc, vv);
  7. return 0;
  8. }
运行结果:
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\"");
可以发现,即使给宏参数“传递”的数据中包含引号,使用#仍然会在两头添加新的引号,而原来的引号会被转义。

将上面的例子补充完整:
  1. #include <stdio.h>
  2. #define STR(s) #s
  3. int main() {
  4. printf("%s\n", STR(c.biancheng.net));
  5. printf("%s\n", STR("c.biancheng.net"));
  6. return 0;
  7. }
运行结果:
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);
将上面的例子补充完整:
  1. #include <stdio.h>
  2. #define CON1(a, b) a##e##b
  3. #define CON2(a, b) a##b##00
  4. int main() {
  5. printf("%f\n", CON1(8.5, 2));
  6. printf("%d\n", CON2(12, 34));
  7. return 0;
  8. }
运行结果:
850.000000
123400

6.C语言中几个预定义宏
顾名思义,预定义宏就是已经预先定义好的宏,我们可以直接使用,无需再重新定义。

ANSI C 规定了以下几个预定义宏,它们在各个编译器下都可以使用:
  • __LINE__:表示当前源代码的行号;
  • __FILE__:表示当前源文件的名称;
  • __DATE__:表示当前的编译日期;
  • __TIME__:表示当前的编译时间;
  • __STDC__:当要求程序严格遵循ANSI C标准时该标识被赋值为1;
  • __cplusplus:当编写C++程序时该标识符被定义。

预定义宏演示:
  1. #include <stdio.h>
  2. #include <stdlib.h>
  3. int main() {
  4. printf("Date : %s\n", __DATE__);
  5. printf("Time : %s\n", __TIME__);
  6. printf("File : %s\n", __FILE__);
  7. printf("Line : %d\n", __LINE__);
  8. system("pause");
  9. return 0;
  10. }
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

请看下面的例子:
  1. #include <stdio.h>
  2. #define WIN16 true
  3. int main(void){
  4. #ifdef WIN16
  5. printf("The value of sizeof(int) is 2.\n");
  6. #else
  7. printf("The value of sizeof(int) is 4.\n");
  8. #endif
  9. return 0;
  10. }
运行结果:
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进行编译。因此可以使程序在不同条件下,完成不同的功能。

请看下面的例子:
  1. #include <stdio.h>
  2. #define R 1
  3. int main(){
  4. float len, area_round, area_square;
  5. printf ("input a number: ");
  6. scanf("%f", &len);
  7. #if R
  8. area_round = 3.14159*len*len;
  9. printf("Area of round is: %f\n", area_round);
  10. #else
  11. area_square = len*len;
  12. printf("Area of square is: %f\n", area_square);
  13. #endif
  14. return 0;
  15. }
运行结果:
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,那么可以这样做:
  1. #ifdef WIN32
  2. #error This programme cannot compile at Windows Platform
  3. #endif
WIN32 是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++的方式来编译程序时,可以这样做:
复制纯文本新窗口
  1. #ifndef __cplusplus
  2. #error 当前程序必须以C++方式编译
  3. #endif

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

下面是本章涉及到的部分预处理指令:
指令说明#空指令,无任何效果#include包含一个源代码文件#define定义宏#undef取消已定义的宏#if如果给定条件为真,则编译下面代码#ifdef如果宏已经定义,则编译下面代码#ifndef如果宏没有定义,则编译下面代码#elif如果前面的#if给定条件不为真,当前条件为真,则编译下面代码#endif结束一个#if……#else条件编译块
预处理功能是C语言特有的功能,它是在对源程序正式编译前由预处理程序完成的,程序员在程序中用预处理命令来调用这些功能。

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

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

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

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

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

0 0
原创粉丝点击