前面各章中,已经多次使用过#include
命令。使用库函数之前,应该用#include
引入对应的头文件。这种以#
号开头的命令称为预处理命令。
C 预处理器不是编译器的组成部分,但是它是编译过程中一个单独的步骤。简言之,C 预处理器只不过是一个文本替换工具而已,它们会指示编译器在实际编译之前完成所需的预处理。我们将把 C 预处理器(C Preprocessor)简写为 CPP。
所有的预处理器命令都是以井号(#)开头。它必须是第一个非空字符,为了增强可读性,预处理器指令应从第一列开始。下面列出了所有重要的预处理器指令:
下面列出了所有重要的预处理器指令:
指令 | 描述 |
---|---|
#define | 定义宏 |
#include | 包含一个源代码文件 |
#undef | 取消已定义的宏 |
#ifdef | 如果宏已经定义,则返回真 |
#ifndef | 如果宏没有定义,则返回真 |
#if | 如果给定条件为真,则编译下面代码 |
#else | #if 的替代方案 |
#elif | 如果前面的 #if 给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个 #if……#else 条件编译块 |
#error | 当遇到标准错误时,输出错误消息 |
#pragma | 使用标准化方法,向编译器发布特殊的命令到编译器中 |
C语言代码要经过编译和链接才能生成可执行程序:
- 编译是针对单个源文件(.c 文件)的,有多少个源文件就生成多少个目标文件,并且在生成过程中不受其他源文件的影响。也就是说,每个源文件都是独立编译的。
- 链接器的作用就是将这些目标文件拼装成一个可执行程序,并为代码(函数)和数据(变量、字符串等)分配好虚拟地址,这和搭积木的过程有点类似。
编译的原理比较复杂,涉及到大量的算法和正则表达式,我们这门课不涉及C语言的编译。
在实际开发中,有时候在编译之前还需要对源文件进行简单的处理。例如,我们希望自己的程序在 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"
使用尖括号< >
和双引号" "
的区别在于头文件的搜索路径不同,包含标准库的头文件建议用尖括号,包含自定义的头文件建议用双引号。使用尖括号表示在包含文件目录中去查找(包含目录是
由用户在设置环境时设置的),而不在源文件目录去查找;使用双引号则表示首先在当前的源文件目录中查找,若未找到才到包含目录中去查找。用户编程时可根据自己文件所在的目录来选择某一种命令形式。
说明:
- 一个 #include 命令只能包含一个头文件,多个头文件需要多个 #include 命令。
- 文件包含允许嵌套,也就是说在一个被包含的文件中又可以包含另一个文件。
什么是宏 ?
宏(Macro)是预处理命令的一种,它允许用一个标识符来表示一个字符串。先看一个例子:
#include <stdio.h> #define N 100 int main() { int sum = 20 + N; printf("%d\n", sum); return 0; }
该示例中的语句int sum = 20 + N;
,N
被100
代替了。
#define N 100
就是宏定义,N
为宏名,100
是宏的内容。
在预处理阶段,对程序中所有出现的“宏名”,预处理器都会用宏定义中的字符串去代换,这称为“宏替换”或“宏展开”。
宏定义是由源程序中的宏定义命令#define
完成的,宏替换是由预处理程序完成的。
宏定义的一般形式为:
#define 宏名 字符串
#
表示这是一条预处理命令,所有的预处理命令都以 # 开头。define
是预处理命令。宏名
是标识符的一种,命名规则和标识符相同。字符串
可以是数字、表达式、if 语句、函数等。
这里所说的字符串是一般意义上的字符序列,不要和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", sum); return 0; }
程序的开头首先定义了一个宏 M,它表示 (n*n+3*n) 这个表达式。在 第8行代码中使用了宏 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;
这相当于:
这显然是不正确的。所以进行宏定义时要注意,应该保证在宏替换之后不发生歧义。
对宏定义的几点说明
1) 宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单粗暴的替换。字符串中可以含任何字符,它可以是常数、表达式、if 语句、函数等,预处理程序对它不作任何检查,如有错误,只能在编译已被宏展开后的源程序时发现。
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 表示 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; //也可以写作typedef int (*PIN2);
从形式上看这两者相似, 但在实际使用中却不相同。
下面用 PIN1,PIN2 说明变量时就可以看出它们的区别:
PIN1 a, b;
在宏代换后变成:
int * a, b;
表示 a 是指向整型的指针变量,而 b 是整型变量。然而:
PIN2 a,b;
表示 a、b 都是指向整型的指针变量。因为 PIN2 是一个新的、完整的数据类型。
由这个例子可见,宏定义虽然也可表示数据类型, 但毕竟只是简单的字符串替换。在使用时要格外小心,以避出错。
C语言带参数宏定义
C语言允许宏带有参数。在宏定义中的参数称为“形式参数”,在宏调用中的参数称为“实际参数”,这点和函数有些类似。
对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参。
带参宏定义的一般形式为:
在字符串中可以含有各个形参。
带参宏调用的一般形式为:
例如:
#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
,形参 a、b 均出现在条件表达式中。程序第 8 行max = MAX(x, y)
为宏调用,实参 x、y 将用来代替形参 a、b。宏展开后该语句为:
max=(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) 在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。而在宏调用中,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型。
这一点和函数是不同的:在函数中,形参和实参是两个不同的变量,都有自己的作用域,调用时要把实参的值传递给形参;而在带参数的宏中,只是符号的替换,不存在值传递的问题。
【示例】输入 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。第 8 行宏调用中实参为 a+1,是一个表达式,在宏展开时,用 a+1 代换 y,再用 (y)*(y) 代换 SQ,得到如下语句:
sq=(a+1)*(a+1);
这与函数的调用是不同的,函数调用时要把实参表达式的值求出来再传递给形参,而宏展开中对实参表达式不作计算,直接按照原样替换。
3) 在宏定义中,字符串内的形参通常要用括号括起来以避免出错。例如上面的宏定义中 (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 = 200/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; }
由此可见,对于带参宏定义不仅要在参数两侧加括号,还应该在整个字符串外加括号。
Y两边如果不用括号:
#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; }
input number: 9
sq结果: 10
为什么会得这样的结果呢?分析宏调用语句,在宏展开之后变为:
sq=200/(a+1*a+1) = 10 ( 取整的结果)
#include <stdio.h> #define SQ(y) (y*y) int main() { float a,sq; printf("input a number: "); scanf("%f", &a); sq = 200 / SQ(a+1); printf("sq=%f\n", sq); return 0; }
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; }
这样的结果是不对的,在不同的编译器上运行结果也不一样。在我们的CB编译器的结果是:
3^2 = 2
5^2 = 12
7^2 = 30
在示例①中,先把实参 i 传递给形参 y,然后再自增 1,这样每循环一次 i 的值增加 1,所以最终要循环 5 次。
在示例②中,宏调用只是简单的字符串替换,SQ(i++) 会被替换为 ((i++)*(i++)),这样每循环一次 i 的值增加 2,所以最终只循环 3 次。
在第一次循环时,由于 i 等于 1,其计算过程为: i 自增 两次变为 3,因此结果的第一个值就是3^2; 第一个Y是一,第二个Y是二,所以结果是2.
在第二次循环时,由于 i 等于 3,其计算过程为: i 自增 两次变为 5,因此结果的第一个值就是5^2; 第一个Y是3,第二个Y是4,所以结果是12.
在第三次循环时,由于 i 等于 5,其计算过程为: i 自增 两次变为 7,因此结果的第一个值就是7^2; 第一个Y是5,第二个Y是6,所以结果是30.
因为此时i已经等于7,不再满足循环条件,停止循环。
由此可见,宏和函数只是在形式上相似,本质上是完全不同的。
带参数的宏也可以用来定义多个语句,在宏调用时,把这些语句又替换到源程序中,请看下面的例子:
#include <stdio.h> #define SSSV(s1, s2, s3, v) s1 = length * width; s2 = length * height; s3 = width * height; v = width * length * height; int main() { int length = 3, width = 4, height = 5, sa, sb, sc, vv; SSSV(sa, sb, sc, vv); printf("sa=%d, sb=%d, sc=%d, vv=%d\n", sa, sb, sc, vv); return 0; }
运行结果:
sa=12, sb=15, sc=20, vv=60
预定义宏
ANSI C 定义了许多宏。在编程中您可以使用这些宏,但是不能直接修改这些预定义的宏。
宏 | 描述 |
---|---|
__DATE__ | 当前日期,一个以 “MMM DD YYYY” 格式表示的字符常量。 |
__TIME__ | 当前时间,一个以 “HH:MM:SS” 格式表示的字符常量。 |
__FILE__ | 这会包含当前文件名,一个字符串常量。 |
__LINE__ | 这会包含当前行号,一个十进制常量。 |
__STDC__ | 当编译器以 ANSI 标准编译时,则定义为 1。 |
让我们来尝试下面的实例:
#include <stdio.h> main() { printf("File :%s\n", __FILE__ ); printf("Date :%s\n", __DATE__ ); printf("Time :%s\n", __TIME__ ); printf("Line :%d\n", __LINE__ ); printf("ANSI :%d\n", __STDC__ ); }
预处理条件编译
预处理程序提供了条件编译的功能。可以按不同的条件去编译不同的程序部分,因而产生不同的目标代码文件。这对于程序的移植和调试是很有用的。
条件编译有三种形式,下面分别介绍:
1. 第一种形式:
#ifdef 标识符
程序段 1
#else
程序段 2
#endif
它的功能是,如果标识符已被 #define 命令定义过则对程序段 1 进行编译;否则对程序段 2 进行编译。如果没有程序段 2(它为空),本格式中的#else 可以没有,即可以写为:
#ifdef 标识符
程序段
#endif
例七:
#define NUM ok main(){ struct stu { int num; char *name; char sex; float score; } *ps; ps=(struct stu*)malloc(sizeof(struct stu)); ps->num=102; ps->name="Zhang ping"; ps->sex='M'; ps->score=62.5; #ifdef NUM printf("Number=%d\nScore=%f\n",ps->num,ps->score); #else printf("Name=%s\nSex=%c\n",ps->name,ps->sex); #endif free(ps); }
由于在程序的第 16 行插入了条件编译预处理命令,因此要根据 NUM 是否被定义过来决定
编译那一个 printf 语句。而在程序的第一行已对 NUM 作过宏定义,因此应对第一个 printf
语句作编译故运行结果是输出了学号和成绩。
在程序的第一行宏定义中,定义 NUM 表示字符串 OK,其实也可以为任何字符串,甚至不
给出任何字符串,写为:
#define NUM
也具有同样的意义。只有取消程序的第一行才会去编译第二个 printf 语句。读者可上机试作。
2. 第二种形式:
#ifndef 标识符
程序段 1
#else
程序段 2
#endif
与第一种形式的区别是将“ifdef”改为“ifndef”。它的功能是,如果标识符未被
#define 命令定义过则对程序段 1 进行编译,否则对程序段 2 进行编译。这与第一种形式的
功能正相反。
3. 第三种形式:
#if 常量表达式
程序段 1
#else
程序段 2
#endif
它的功能是,如常量表达式的值为真(非 0),则对程序段 1 进行编译,否则对程序段 2
进行编译。因此可以使程序在不同条件下,完成不同的功能。
【例 9.13】
#define R 1
main(){
float c,r,s;
printf (“input a number: “);
scanf(“%f”,&c);
#if R
r=3.14159*c*c;
printf(“area of round is: %f\n”,r);
#else
s=c*c;
printf(“area of square is: %f\n”,s);
#endif
}
本例中采用了第三种形式的条件编译。在程序第一行宏定义中,定义 R 为 1,因此在条
件编译时,常量表达式的值为真,故计算并输出圆面积。
上面介绍的条件编译当然也可以用条件语句来实现。 但是用条件语句将会对整个源程序
进行编译,生成的目标代码程序很长,而采用条件编译,则根据条件只编译其中的程序段 1
或程序段 2,生成的目标程序较短。如果条件选择的程序段很长,采用条件编译的方法是十
分必要的。