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语言宏定义
宏定义是预处理命令的一种,它允许用一个标识符来表示一个字符串。先看一个例子:
- #include <stdio.h>
- #define N 100
- int main(){
- int sum = 20 + N;
- printf("%d\n", sum);
- return 0;
- }
运行结果:
120
该示例中的语句int
,
sum = 20 + N;N
被100
代替了。
#define
就是宏定义,
N 100N
为宏名,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 <stdio.h>
- #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.14159
- int main(){
- // Code
- return 0;
- }
- #undef PI
- void func(){
- // Code
- }
表示PI只在main函数中有效,在func中无效。
4) 宏名在源程序中若用引号括起来,则预处理程序不对其作宏代换,例如:
- #include <stdio.h>
- #define OK 100
- int 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 <stdio.h>
- #define MAX(a,b) (a>b) ? a : b
- int 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均出现在条件表达式中。程序第7行
? a : bmax=MAX(x,
为宏调用,实参x、y,将代换形参a、b。宏展开后该语句为:
y)
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 <stdio.h>
- #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 <stdio.h>
- #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=19
同样输入9,但结果却是不一样的。问题在哪里呢?这是由于替换只作符号替换而不作其它处理而造成的。宏替换后将得到以下语句:
sq=a+1*a+1;
由于a为9故sq的值为19。这显然与题意相违,因此参数两边的括号是不能少的。即使在参数两边加括号还是不够的,请看下面程序:
- #include <stdio.h>
- #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 <stdio.h>
- #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 <stdio.h>
- int SQ(int y){
- return ((y)*(y));
- }
- int main(){
- int i=1;
- while(i<=5){
- 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 <stdio.h>
- #define SQ(y) ((y)*(y))
- int main(){
- int i=1;
- while(i<=5){
- 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 <stdio.h>
- #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 <stdio.h>
- #define STR(s) #s
- int 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 <stdio.h>
- #define CON1(a, b) a##e##b
- #define CON2(a, b) a##b##00
- int 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 <stdio.h>
- #include <stdlib.h>
- 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 <stdio.h>
- #define WIN16 true
- int 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 <stdio.h>
- #define R 1
- int 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
这和发生语法错误的效果是一样的,程序编译失败。请看下面的截图:
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语言特有的功能,它是在对源程序正式编译前由预处理程序完成的,程序员在程序中用预处理命令来调用这些功能。
宏定义可以带有参数,宏调用时是以实参代换形参,而不是“值传送”。
为了避免宏代换时发生错误,宏定义中的字符串应加括号,字符串中出现的形式参数两边也应加括号。
文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。
条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。
使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。