一. 预处理
-
什么是预处理:
ANSI C标准规定可以在C源程序中加入一些“预处理命令”,以改进程序设计环境,提高编程效率。
对程序进行通常的编译之前,先对程序中特殊的命令进行“预处理”。
这些预处理命令是由ANSI C统一规定的,但是它不是C语言本身的组成部分,不能直接对它们进行编译 -
预处理命令
C提供的预处理命令主要包含以下三种:
1.宏定义
2.文件包含
3.条件编译
为了与一般C语句相区别,这些命令以符号“#”开头。
例如:#define
,#include
二. 宏定义
标志:#define
作用:定义符号常量和带参的宏
分类:
- 不带参数的宏
- 带参数的宏
-
不带参数的宏定义:
一般形式为:
#define 标识符(宏名) 字符串(宏体)
示例:#define PI 3.1415926
定义时作用:用指定的标识符宏名PI来代替宏体“3.1415926” 。
宏替换:在程序编译前会用宏体替换程序中出现的所有宏名。
例如:#include <stdio.h> #define PI 3.1415926 /*不带参的宏定义*/ int main() { float l,s,r,v; printf("input radius:"); scanf("%f",&r); /*此处的PI将会用3.1415926替换*/ l = 2.0*PI*r; s = PI*r*r; v = 4.0/3*PI*r*r*r; printf("l=%10.4df\ns=%10.4f\nv=%10.4f\n",l,s,v); return 0; }
说明:
(1).宏名一般习惯用大写字母表示,以便与变量名相区别,也可用小写字母。宏名不分配存储空间,变量分配存储空间。
(2).宏定义不是C语句,不必在行末加分号。如果加了分号则会连分号一起进行置换。
(3).#define
命令写在文件开头,作用范围是从定义位置开始到最后一行均有效。但可以用#undef
命令终止宏定义的作用域。
示例1:#include <stdio.h> #define PI 3.1415926 int main() { … s=PI*r*r; } #undef PI // 此行语句之后,PI不再代表3.1415926。 void f1(int r) { l=2*PI*r; … }
示例2:在宏定义中可引用已定义的宏名。
#include <stdio.h> #define R 4.0 #define PI 3.1415926 #define L 2*PI*R /*宏体中引用已经定义过的宏名PI和R*/ #define S PI*R*R /*宏体中引用已经定义过的宏名PI和R*/ int main() { printf("L=%10.4f\nS=%10.4f\n",L,S); return 0; }
其中,L被替换为23.14159264.0,S被替换为3.14159264.04.0
-
带参数的宏定义:
(1)一般形式为:
#define 宏名(参数表)字符串
字符串中包含了括弧中所指定的参数
作用:不光进行字符串替换,还要进行参数替换。
示例:#define S(a,b) a*b area=S(3,2); /*宏替换预处理 area=3*2 */
(2)带实参的宏替换过程描述
按#define
命令行中指定的字符串从左到右进行置换。
若串中包含宏中的形参(如a、b),则将程序中相应的实参(可以是常量、变量或表达式)代替形参。
若宏体中的字符不是参数的字符(如ab中的号),则保留。
观察以下三例:
例1:#include <stdio.h> #define S(a) 2*a*a /*定义了带参的宏*/ int main() { int m; m=S(5); /* m=2*5*5 */ printf("m=%d\n",m); return 0; } 结果:m=50
例2:
#include <stdio.h> #define S(a) 2*a*a int main() { int m; m=S(2+3); /* m=2*(2+3)*(2+3) */ printf("m=%d\n",m); return 0; } 结果:m=13
思考:结果为什么不一样?
因为只是用实参替换形参a,所以用2加3替换a,而不是用2加3的和5替换例3:
#include <stdio.h> #define S(a) 2*(a)*(a) int main() { int m; m=S(2+3); printf("m=%d\n",m); return 0; } 结果:m=50
在宏定义的宏体中参数a的左右加上括号,表示在替换时括号会照原样保留,所以先计算小括号内的,这样就可以了
一定要注意,预处理宏命令时,只是用实参替换形参,而不计算。同时替换时,宏体中除了参数以外的字符都照原样保留。
(3)带参数的宏和函数的区别:
->函数调用要进行参数的传递及函数体内语句的执行;而带参的宏只是进行简单的字符替换。
->函数调用是在程序运行时处理的,会为形参分配临时的存储单元;而宏替换是在编译前进行,在展开时并不分配内存单元,不进行值的传递处理,也没有“返回值”的概念。
->函数要求实参和形参类型一致;而宏名无类型,参数也无类型,只是一个符号代表,替换时代入指定的字符串即可。宏定义时,字符串可以是任何类型的数据。
->调用函数只可得到一个返回值,而用宏可以得到几个结果。(4)带参宏的应用:
#include <stdio.h> #define PI 3.1415926 #define CIRCLE(R,L,S,V) L=2*PI*R;S=PI*R*R;V=4.0/3.0*PI*R*R*R int main() { float r,l,s,v; scanf("%f",&r); CIRCLE(r,l,s,v); printf("r=%6.2f,l=%6.2f,s=%6.2f,v=%6.2f\n",r,l,s,v); return 0; }
在上例中定义了带参的宏,简化了程序中相应的语句。在预处理时分别用实参替换宏体中所有的形参。
相应的替换后的结果:
l=23.1415926r;
s=3.1515926rr;
v=4.0/3/03.1415926rrr;
三. 文件包含
-
格式:
#include <文件名>
----->在包含文件目录中去查找该文件
#include “文件名”
----->在当前源文件目录中查找该文件,若未找到,则到包含文件目录中去查找
作用:一个源文件可以将另外一个源文件的全部内容包含进来。 -
案例:利用文件包含预处理命令,按照规定格式,输出多个指定变量或表达式的值。
分析:
源文件中包含的文件中应有输出函数;
多个变量,类型、输出格式均有可能不相同,故利用带参宏定义命令做成一个通用格式。–> 由于需要多次执行输出,所以可将输出函数做成格式宏。
(1) 利用宏命令将输出函数语句作为宏体,
参数w—输出精度 t—输出格式控制符 v—输出值#define PRINT(w,t,v) printf("\n%"#w#t,(v))
其中中出现的井号表示可以把跟在其后的参数转换成一个字符串并和前面内容连接
–> 将格式宏保存在一个文件中,并存为头文件FORM.h,它将是源文件中包含的文件。
(2)保存成头文件FORM.h#include <stdio.h> #define PRINT(w,t,v) printf("\n%"#w#t,(v))
–> 完成源文件s.c的编写,其中要包含文件FORM.h。
(3)主文件s.c#include <stdio.h> #include "FORM.h" int main() { int a=1; char string[]="ABCDEF"; PRINT(5.1,f,3.25); PRINT(3,d,a); PRINT(10,s,string); PRINT(,d,8+2); return 0; }
在预处理时依次将主函数中的四个宏进行参数替换,编译运行输出结果。主函数中的第一个宏展开后表示将3.25以百分号5.1f的格式输出,故输出3.3;第二个宏将a的值以百分号3d的格式输出,故输出空格空格1;第三第四个宏类似。
注意: 在案例1中有两个文件,但并不是各自编译,而是在编译预处理后将头文件FORM.h包含到s.c文件中,得到一个新的源程序,然后对新文件进行编译,得到一个新的目标(.obj)文件。
上例s.c文件等同于:#include <stdio.h> int main() { int a=1; char string[]="ABCDEF"; printf("\n%5.1f",(3.25)); printf("\n%3d",(a)); printf("\n%10s",(string)); printf("\n%d",(8+2)); return 0; }
-
使用文件包含时注意的问题:
–> 一个#include命令只能指定一个被包含文件,如果要包含n个文件,要用n个#include命令。
–> 如果文件1包含文件2,而在文件2中要用到文件3的内容,则可在文件1中用两个include命令分别包含文件2和文件3,而且文件3应出现在文件2之前.
即在file1.c中定义#define "file3.h"和#define “file2.h”;而file2.h和file3.h不包含#define命令。
如图:
–> 在一个被包含文件中又可以包含另一个被包含文件,即文件包含是可以嵌套的。
如图:
四. 条件编译
-
作用:对部分内容指定编译条件,使其只在满足一定条件才进行编译。
标志:
#ifdef
#ifndef
#if -
形式1:
#ifdef 标识符 程序段1 #else 程序段2 #endif
以井号ifdef开头的条件编译,后面跟标识符,表示该标识符若被定义为宏名(被#define命令定义过),则编译程序段1,没有被定义则编译程序段2
形式2:
#ifndef 标识符 程序段1 #else 程序段2 #endif
以井号ifndef开头的条件编译则与前一个相反,即标识符如果没有被定义为宏,则编译程序段1,被定义过则编译程序段2。
形式3:
#if 表达式 程序段1 #else 程序段2 #endif
以井号if开头的条件编译,则是根据其后的表达式判断,若表达式为真,则编译程序段1,为假,则编译程序段2。
不论哪种形式的条件编译都意味着程序中一部分语句不会被编译执行,目的是便于查错,便于移植到其它程序中。
-
案例:输入一行字母字符,根据需要设置条件编译,使之能将字母全改为大写输出,或全改为小写字母输出。
分析:根据条件判断,
若为大写字母,则ASCII码加32 ,变为小写;
若为小写字母,则ASCII码减32,变为大写。
代码实现:#include <stdio.h> #define LETTER 1 int main() { char str[]="ABdeFgh",c; int i; i=0; while((c=str[i])!='\0') { i++; #if LETTER if(c>='a' && c<='z') c=c-32; #else if(c>='A' && c<='Z') c=c+32; #endif printf("%c",c); } return 0; }
若表达式LETTER值为真,则将小写字母减32转换为对应的大写字母输出;若值为假,则将大写字母加32转换为对应的小写字母输出。
预处理中先进行宏替换即条件编译中出现宏名的地方用1代替,再条件编译,1表示表达式值为真,故输出结果为str数组中所有字符的大写字母。若将宏体改为0,则输出str数组中所有字符的小写字母。