【C语言学习】预处理命令
什么是预处理命令
前面各章中,已经多次使用过#include
命令。使用库函数之前,应该用#include
引入对应的头文件。这种以#
号开头的命令称为预处理命令。
C语言源文件要经过编译、链接才能生成可执行程序:
- 编译(Compile)会将源文件(
.c
文件)转换为目标文件。对于 VC/VS,目标文件后缀为.obj
;对于GCC,目标文件后缀为.o
。
编译是针对单个源文件的,一次编译操作只能编译一个源文件,如果程序中有多个源文件,就需要多次编译操作。
- 链接(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语言提供了多种预处理功能,如宏定义、文件包含、条件编译等,合理地使用它们会使编写的程序便于阅读、修改、移植和调试,也有利于模块化程序设计。
实例
下面我们举个例子来说明预处理命令的实际用途。假如现在要开发一个C语言程序,让它暂停 5 秒以后再输出内容,并且要求跨平台,在 Windows 和 Linux 下都能运行,怎么办呢?
这个程序的难点在于,不同平台下的暂停函数和头文件都不一样:
-
Windows 平台下的暂停函数的原型是
void Sleep(DWORD dwMilliseconds)
(注意 S 是大写的),参数的单位是“毫秒”,位于 <windows.h> 头文件。 -
Linux 平台下暂停函数的原型是
unsigned int sleep (unsigned int seconds)
,参数的单位是“秒”,位于 <unistd.h> 头文件。
不同的平台下必须调用不同的函数,并引入不同的头文件,否则就会导致编译错误,因为 Windows 平台下没有 sleep() 函数,也没有 <unistd.h> 头文件,反之亦然。这就要求我们在编译之前,也就是预处理阶段来解决这个问题:
#include <stdio.h>
//不同的平台下引入不同的头文件
#if _WIN32 //识别windows平台
#include <windows.h>
#elif __linux__ //识别linux平台
#include <unistd.h>
#endif
int main() {
//不同的平台下调用不同的函数
#if _WIN32 //识别windows平台
Sleep(5000);
#elif __linux__ //识别linux平台
sleep(5);
#endif
puts("http://c.biancheng.net/");
return 0;
}
#if、#elif、#endif 就是预处理命令,它们都是在编译之前由预处理程序来执行的。
对于 Windows 平台,预处理以后的代码变成:
#include <stdio.h>
#include <windows.h>
int main() {
Sleep(5000);
puts("http://c.biancheng.net/");
return 0;
}
对于 Linux 平台,预处理以后的代码变成:
#include <stdio.h>
#include <unistd.h>
int main() {
sleep(5);
puts("http://c.biancheng.net/");
return 0;
}
在不同的平台下,编译之前(预处理之后)的源代码都是不一样的。这就是预处理阶段的工作,它把代码当成普通文本,根据设定的条件进行一些简单的文本替换,将替换以后的结果再交给编译器处理。
#include的用法
#include
叫做文件包含命令,用来引入对应的头文件(.h
文件)。#include 也是C语言预处理命令的一种。
#include 的处理过程很简单,就是将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,这与复制粘贴的效果相同。
#include 的用法有两种:
#include <stdHeader.h>
#include "myHeader.h"
使用尖括号< >
和双引号" "
的区别在于头文件的搜索路径不同:
-
使用尖括号
< >
,编译器会到系统路径下查找头文件; -
而使用双引号
" "
,编译器首先在当前目录下查找头文件,如果没有找到,再到系统路径下查找。
也就是说,使用双引号比使用尖括号多了一个查找路径,它的功能更为强大。
以下两种方法引入方法均可:
#include "stdio.h"
#include "stdlib.h"
stdio.h 和 stdlib.h 都是标准头文件,它们存放于系统路径下,所以使用尖括号和双引号都能够成功引入;而自己编写的头文件,一般存放于当前项目的路径下,所以不能使用尖括号,只能使用双引号。
推荐使用尖括号来引入标准头文件,使用双引号来引入自定义头文件(自己编写的头文件),这样一眼就能看出头文件的区别。
注意事项:
-
一个 #include 命令只能包含一个头文件,多个头文件需要多个 #include 命令。
-
同一个头文件可以被多次引入,多次引入的效果和一次引入的效果相同,因为头文件在代码层面有防止重复引入的机制。
-
文件包含允许嵌套,也就是说在一个被包含的文件中又可以包含另一个文件。
#include 用法举例
本例中需要创建三个文件,分别是 main.c、my.c 和 my.h
my.c 所包含的代码:
//计算从m加到n的和
int sum(int m, int n) {
int i, sum = 0;
for (i = m; i <= n; i++) {
sum += i;
}
return sum;
}
my.h 所包含的代码:
//声明函数
int sum(int m, int n);
main.c 所包含的代码:
#include <stdio.h>
#include "my.h"
int main() {
printf("%d\n", sum(1, 100));
return 0;
}
不管是标准头文件,还是自定义头文件,都只能包含变量和函数的声明,不能包含定义,否则在多次引入时会引起重复定义错误。
宏定义
#define 叫做宏定义命令,它也是C语言预处理命令的一种。所谓宏定义,就是用一个标识符来表示一个字符串,如果在后面的代码中出现了该标识符,那么就全部替换成指定的字符串。
先通过一个例子来看一下 #define 的用法:
#include <stdio.h>
#define N 100
int main(){
int sum = 20 + N;
printf("%d\n", sum);
return 0;
}
// 120
注意第 6 行代码int sum = 20 + N
,N
被100
代替了。
#define N 100
就是宏定义,N
为宏名,100
是宏的内容(宏所表示的字符串)。在预处理阶段,对程序中所有出现的“宏名”,预处理器都会用宏定义中的字符串去代换,这称为“宏替换”或“宏展开”。
宏定义是由源程序中的宏定义命令#define
完成的,宏替换是由预处理程序完成的。
宏定义的一般形式为:
#define 宏名 字符串
#
表示这是一条预处理命令,所有的预处理命令都以 # 开头。宏名
是标识符的一种,命名规则和变量相同。字符串
可以是数字、表达式、if 语句、函数等。
这里所说的字符串是一般意义上的字符序列,不要和C语言中的字符串等同,它不需要双引号。
程序中反复使用的表达式就可以使用宏定义,例如:
#define M (y*y+3*y)
它的作用是指定标识符M
来表示(y*y+3*y)
这个表达式。在编写代码时,所有出现 (yy+3y) 的地方都可以用 M 来表示,而对源程序编译时,将先由预处理程序进行宏代替,即用 (yy+3y) 去替换所有的宏名 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;
}
/*
Input a number: 10↙
sum=1560
*/
程序的开头首先定义了一个宏 M,它表示 (nn+3n) 这个表达式。在 9 行代码中使用了宏 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;
这显然是不正确的。所以进行宏定义时要注意,应该保证在宏替换之后不发生歧义。
对 #define 用法的几点说明
-
宏定义是用宏名来表示一个字符串,在宏展开时又以该字符串取代宏名,这只是一种简单粗暴的替换。字符串中可以含任何字符,它可以是常数、表达式、if 语句、函数等,预处理程序对它不作任何检查,如有错误,只能在编译已被宏展开后的源程序时发现。
-
宏定义不是说明或语句,在行末不必加分号,如加上分号则连分号也一起替换。
-
宏定义必须写在函数之外,其作用域为宏定义命令起到源程序结束。如要终止其作用域可使用
#undef
命令。例如:
#define PI 3.14159
int main(){
// Code
return 0;
}
#undef PI
void func(){
// Code
}
表示 PI 只在 main() 函数中有效,在 func() 中无效。
- 代码中的宏名如果被引号包围,那么预处理程序不对其作宏代替,例如:
#include <stdio.h>
#define OK 100
int main(){
printf("OK\n");
return 0;
}
// OK
该例中定义宏名 OK 表示 100,但在 printf 语句中 OK 被引号括起来,因此不作宏替换,而作为字符串处理。
- 宏定义允许嵌套,在宏定义的字符串中可以使用已经定义的宏名,在宏展开时由预处理程序层层代换。例如:
#define PI 3.1415926
#define S PI*y*y /* PI是已定义的宏名*/
对语句:
printf("%f", S);
在宏代换后变为:
printf("%f", 3.1415926**y**y);
-
习惯上宏名用大写字母表示,以便于与变量区别。但也允许用小写字母。
-
可用宏定义表示数据类型,使书写方便。例如:
#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语言允许宏带有参数。在宏定义中的参数称为“形式参数”,在宏调用中的参数称为“实际参数”,这点和函数有些类似。
对带参数的宏,在展开过程中不仅要进行字符串替换,还要用实参去替换形参。
带参宏定义的一般形式为:
#define 宏名(形参列表) 字符串
在字符串中可以含有各个形参。
带参宏调用的一般形式为:
宏名(实参列表);
例如:
#define M(y) y*y+3*y //宏定义
// TODO:
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 均出现在条件表达式中。程序第 7 行max = MAX(x, y)
为宏调用,实参 x、y 将用来代替形参 a、b。宏展开后该语句为:
max=(x>y) ? x : y;
对带参宏定义的说明
- 带参宏定义中,形参之间可以出现空格,但是宏名和形参列表之间不能有空格出现。例如把:
#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);
这显然是错误的。
- 在带参宏定义中,不会为形式参数分配内存,因此不必指明数据类型。而在宏调用中,实参包含了具体的数据,要用它们去替换形参,因此实参必须要指明数据类型。
这一点和函数是不同的:在函数中,形参和实参是两个不同的变量,都有自己的作用域,调用时要把实参的值传递给形参;而在带参数的宏中,只是符号的替换,不存在值传递的问题。
输入 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);
这与函数的调用是不同的,函数调用时要把实参表达式的值求出来再传递给形参,而宏展开中对实参表达式不作计算,直接按照原样替换。
- 在宏定义中,字符串内的形参通常要用括号括起来以避免出错。例如上面的宏定义中 (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
*/
由于宏展开只是简单的符号替换的过程,没有任何其它的处理。宏替换后将得到以下语句:
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;
}
由此可见,对于带参宏定义不仅要在参数两侧加括号,还应该在整个字符串外加括号。
带参宏定义和函数的区别
带参数的宏和函数很相似,但有本质上的区别:宏展开仅仅是字符串的替换,不会对表达式进行计算;宏在编译之前就被处理掉了,它没有机会参与编译,也不会占用内存。而函数是**一段可以重复使用的代码,会被编译,会给它分配内存,**每次调用函数,就是执行这块内存中的代码。
【示例①】用函数计算平方值:
#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;
}
/*
3^2 = 1
5^2 = 9
7^2 = 25
*/
在示例①中,先把实参 i 传递给形参 y,然后再自增 1,这样每循环一次 i 的值增加 1,所以最终要循环 5 次。
在示例②中,宏调用只是简单的字符串替换,SQ(i++) 会被替换为 ((i++)*(i++)),这样每循环一次 i 的值增加 2,所以最终只循环 3 次。
由此可见,宏和函数只是在形式上相似,本质上是完全不同的。
带参数的宏也可以用来定义多个语句,在宏调用时,把这些语句又替换到源程序中:
#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
*/
条件编译
假如现在要开发一个C语言程序,让它输出红色的文字,并且要求跨平台,在 Windows 和 Linux 下都能运行,怎么办呢?
这个程序的难点在于,不同平台下控制文字颜色的代码不一样,我们必须要能够识别出不同的平台。
Windows 有专有的宏_WIN32
,Linux 有专有的宏__linux__
,以现有的知识,我们很容易就想到了 if else:
#include <stdio.h>
int main(){
if(_WIN32){
system("color 0c");
printf("http://c.biancheng.net\n");
}else if(__linux__){
printf("\033[22;31mhttp://c.biancheng.net\n\033[22;30m");
}else{
printf("http://c.biancheng.net\n");
}
return 0;
}
但这段代码是错误的,在 Windows 下提示 linux 是未定义的标识符,在 Linux 下提示 _Win32 是未定义的标识符。对代码进行改进:
#include <stdio.h>
int main(){
#if _WIN32
system("color 0c");
printf("http://c.biancheng.net\n");
#elif __linux__
printf("\033[22;31mhttp://c.biancheng.net\n\033[22;30m");
#else
printf("http://c.biancheng.net\n");
#endif
return 0;
}
#if、#elif、#else 和 #endif 都是预处理命令,整段代码的意思是:如果宏 _WIN32 的值为真,就保留第 4、5 行代码,删除第 7、9 行代码;如果宏 linux 的值为真,就保留第 7 行代码;如果所有的宏都为假,就保留第 9 行代码。
这些操作都是在预处理阶段完成的,多余的代码以及所有的宏都不会参与编译,不仅保证了代码的正确性,还减小了编译后文件的体积。
这种能够根据不同情况编译不同代码、产生不同目标文件的机制,称为条件编译。条件编译是预处理程序的功能,不是编译器的功能。
#if 的用法
#if 用法的一般格式为:
#if 整型常量表达式1
程序段1
#elif 整型常量表达式2
程序段2
#elif 整型常量表达式3
程序段3
#else
程序段4
#endif
它的意思是:如常“表达式1”的值为真(非0),就对“程序段1”进行编译,否则就计算“表达式2”,结果为真的话就对“程序段2”进行编译,为假的话就继续往下匹配,直到遇到值为真的表达式,或者遇到 #else。这一点和 if else 非常类似。
需要注意的是,#if 命令要求判断条件为“整型常量表达式”,也就是说,表达式中不能包含变量,而且结果必须是整数;而 if 后面的表达式没有限制,只要符合语法就行。这是 #if 和 if 的一个重要区别。
#elif 和 #else 也可以省略,如下所示:
#include <stdio.h>
int main(){
#if _WIN32
printf("This is Windows!\n");
#else
printf("Unknown platform!\n");
#endif
#if __linux__
printf("This is Linux!\n");
#endif
return 0;
}
#ifdef 的用法
#ifdef 用法的一般格式为:
#ifdef 宏名
程序段1
#else
程序段2
#endif
它的意思是,如果当前的宏已被定义过,则对“程序段1”进行编译,否则对“程序段2”进行编译。
也可以省略 #else:
#ifdef 宏名
程序段
#endif
为了能够清楚地看到当前程序的编译模式,我们不妨在程序中增加提示,请看下面的代码:
#include <stdio.h>
#include <stdlib.h>
int main(){
#ifdef _DEBUG
printf("正在使用 Debug 模式编译程序...\n");
#else
printf("正在使用 Release 模式编译程序...\n");
#endif
system("pause");
return 0;
}
当以 Debug 模式编译程序时,宏 _DEBUG 会被定义,预处器会保留第 5 行代码,删除第 7 行代码。反之会删除第 5 行,保留第 7 行。
#ifndef 的用法
#ifndef 用法的一般格式为:
#ifndef 宏名
程序段1
#else
程序段2
#endif
与 #ifdef 相比,仅仅是将 #ifdef 改为了 #ifndef。它的意思是,如果当前的宏未被定义,则对“程序段1”进行编译,否则对“程序段2”进行编译,这与 #ifdef 的功能正好相反。
三者之间的区别
最后需要注意的是,#if 后面跟的是“整型常量表达式”,而 #ifdef 和 #ifndef 后面跟的只能是一个宏名,不能是其他的。
例如,下面的形式只能用于 #if:
#include <stdio.h>
#define NUM 10
int main(){
#if NUM == 10 || NUM == 20
printf("NUM: %d\n", NUM);
#else
printf("NUM Error\n");
#endif
return 0;
}
// NUM: 10
再如,两个宏都存在时编译代码A,否则编译代码B:
#include <stdio.h>
#define NUM1 10
#define NUM2 20
int main(){
#if (defined NUM1 && defined NUM2)
//代码A
printf("NUM1: %d, NUM2: %d\n", NUM1, NUM2);
#else
//代码B
printf("Error\n");
#endif
return 0;
}
// NUM1: 10, NUM2: 20
#ifdef 可以认为是 #if defined 的缩写,#ifndef可以认为是#if not defined 的缩写。
总结
预处理指令是以#
号开头的代码行,# 号必须是该行除了任何空白字符外的第一个字符。# 后是指令关键字,在关键字和 # 号之间允许存在任意个数的空白字符,整行语句构成了一条预处理指令,该指令将在编译器进行编译之前对源代码做某些转换。
部分预处理指令:
指令 | 说明 |
---|---|
# | 空指令,无任何效果 |
#include | 包含一个源代码文件 |
#define | 定义宏 |
#undef | 取消已定义的宏 |
#if | 如果给定条件为真,则编译下面代码 |
#ifdef | 如果宏已经定义,则编译下面代码 |
#ifndef | 如果宏没有定义,则编译下面代码 |
#elif | 如果前面的#if给定条件不为真,当前条件为真,则编译下面代码 |
#endif | 结束一个#if……#else条件编译块 |
预处理功能是C语言特有的功能,它是在对源程序正式编译前由预处理程序完成的,程序员在程序中用预处理命令来调用这些功能。
宏定义可以带有参数,宏调用时是以实参代换形参(纯字符串替换),而不是“值传送”。
为了避免宏代换时发生错误,宏定义中的字符串应加括号,字符串中出现的形式参数两边也应加括号。
文件包含是预处理的一个重要功能,它可用来把多个源文件连接成一个源文件进行编译,结果将生成一个目标文件。
条件编译允许只编译源程序中满足条件的程序段,使生成的目标程序较短,从而减少了内存的开销并提高了程序的效率。
使用预处理功能便于程序的修改、阅读、移植和调试,也便于实现模块化程序设计。