预处理详解

avatar
作者
筋斗云
阅读量:0

目录

1.预定义符号

2.#define定义常量

3.#define定义宏 

4.带有副作用的宏参数

5.宏替换的规则

6.宏和函数的对比

7.#和##

7.1 #运算符

7.2 ##运算符

8.命名约定

9. #undef

10.命令行定义

11.条件编译

12.头文件的包含

12.1 本地文件包含

12.2 库文件包含 

12.3 嵌套文件包含 


1.预定义符号

C语言设置了一些预定义符号,可以直接用,预定义符号也是在预处理期间处理的。

__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义

#include <stdio.h> int main() { 	printf("%s\n", __FILE__); 	printf("%s\n", __DATE__); 	printf("%s\n", __TIME__); 	printf("%d\n", __LINE__); 	return 0; }

 运行结果:

至于预定义符号的处理过程可以使用gcc编译器测试观察。


2.#define定义常量

基本语法:

#define name stuff
#define MAX 1000 #define reg register //为 register这个关键字,创建⼀个简短的名字 #define do_forever for(;;) //用更形象的符号来替换⼀种实现 #define CASE break;case //在写case语句的时候⾃动把 break写上。 // 如果定义的 stuff过⻓,可以分成⼏⾏写,除了最后⼀⾏外,每⾏的后⾯都加⼀个反斜杠(续行符)。 #define DEBUG_PRINT printf("file:%s\tline:%d\t \  date:%s\ttime:%s\n" ,\  __FILE__,__LINE__ , \  __DATE__,__TIME__ )

for(; ;)初始化部分,调整部分都可以省略掉,但是判断部分如果省略不写,就意味着判断条件恒为真,造成死循环。


在define定义标识符的时候,要不要在最后加上 ‘?’ 。

#define MAX 1000; //error! #define MAX 1000

不要加 ,这样容易导致问题。

比如下面的场景:

#include <stdio.h> #define MAX 100; int main() { 	int n = 0; 	if (1) 		n = MAX; 	else 		n = -1; 	return 0; }

如果是加了分号的情况,等替换后,if和else之间就是2条语句,而没有大括号的时候,if后边只能有一条语句。所以这里会出现语法错误。


3.#define定义宏 

#define 机制包括了一个规定,允许把参数替换到文本中,这种实现通常称为宏(macro)或定义宏 (define macro)。

下面是宏的申明方式:

#define name( parament-list ) stuff

其中的 parament-list 是一个由逗号隔开的符号表,它们可能出现在stuff中。

注意参数列表的左括号必须与name紧邻,如果两者之间有任何空白存在,参数列表就会被解释为stuff的一部分。

举例: 

#define SQUARE( x ) x * x
#include<stdio.h> #define SQUARE(x) x*x int main() { 	int a = 5; 	int ret = SQUARE(a); 	printf("%d", ret); 	return 0; }

运行结果:

这个宏接收一个参数 x ,如果在上述声明之后,把 SQUARE( 5 )置于程序中,预处理器处理变成:int ret = 5*5;

警告:

这个宏存在问题,例如下面代码:

#include<stdio.h> #define SQUARE(x) x*x int main() { 	int a = 5; 	int ret = SQUARE(a+1); 	printf("%d", ret); 	return 0; }

运行结果:

为什么输出结果不是36呢?

替换文本时,代码实际上变成了:int ret=a+1*a+1;

按照操作符的优先级算得结果为11。

解决方法:在宏定义上加上两个括号。

 #define SQUARE(x) (x) * (x)

这里还有一种宏定义:

#include<stdio.h> #define DOUBLE(x) (x) + (x) int main() { 	int a = 5; 	printf("%d\n", 2 * DOUBLE(a + 1)); 	return 0; }

运行结果:

定义中我们使用了括号,想避免之前的问题,但是这个宏又出现了新的错误,问什么输出结果不是24呢?

替换文本时,代码实际上变成了:printf("%d\n", 2 * (a+1)+(a+1));

按照操作符的优先级算得结果为18。

解决方法在宏定义表达式两边加上一对括号。

#define DOUBLE(x) ((x) + (x))

提示用于对数值表达式进行求值的宏定义都应该用以上方式加上括号,避免在使用宏时由于参数中的操作符或邻近操作符之间不可预料的相互作用。


4.带有副作用的宏参数

例如:

x+1;//不带副作⽤ x++;//带有副作⽤ 

当宏参数在宏的定义中出现超过一次的时候,如果参数带有副作用,那么你在使用这个宏的时候就可能出现危险,导致不可预测的后果。副用就是表达式求值的时候出现的永久性效果。

MAX宏可以证明具有副作用的参数所引起的问题: 

#include<stdio.h> #define MAX(X,Y) ((X)>(Y)?(X):(Y)) int main() { 	int a = 3; 	int b = 5; 	int m = MAX(a++, b++); 	printf("m=%d\n", m); 	printf("a=%d\n", a); 	printf("b=%d\n", b); 	return 0; }

运行结果:

预处理器处理之后的结果是:int m=((a++)>(b++)?(a++):(b++))


5.宏替换的规则

在程序中扩展#define定义符号和宏时,需要涉及几个步骤:

  •  在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它首先被替换。
  •  替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
  •  最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
#define MAX(X,Y) ((X)>(Y)?(X):(Y)) #define M 10 int main() { 	int a = 3; 	int m = MAX(a, M);//参数中有#define定义的符号M,它首先被替换 	                  //再次扫描,有#define定义的符号MAX,接着被替换     return 0;  }

注意:

  1. 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
  2. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索

6.宏和函数的对比

宏通常被应用于执行简单的运算。 比如在两个数中找出较大的一个时,写成下面的宏,更有优势一些。

#define MAX(X,Y) ((X)>(Y)?(X):(Y))

那为什么不用函数来完成这个任务?

原因有二:

  1.  用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹
  2. 更为重要的是函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用,但是宏可以适用于整形、长整型、浮点型等可以用于比较的类型。宏的参数是类型无关的。

和函数相比宏的劣势:

  1. 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
  2. 宏是没法调试的。
  3. 宏由于类型无关,也就不够严谨。
  4. 宏可能会带来运算符优先级的问题,导致程容易出现错。

宏有时候可以做函数做不到的事情。比如:宏的参数可以出现类型,但是函数做不到。

例如:

#define Malloc(n,type) (type*)malloc(n*sizeof(type)) int main() { 	int* ptr = Malloc(10, int); 	//预处理替换后 int* ptr=(int*)malloc(10*sizeof(int)); 	return 0; }

宏和函数的对比

属性#define定义宏函数
代码长度每次使用时,宏代码都会被插入到程序中。
除了非常小的宏之外,程序的长度会大幅度
增长
函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码
执行速度更快存在函数的调用和返回的额外开销,所以相对慢一些
操作符优先级宏参数的求值是在所有周围表达式的上下文
环境里,除非加上括号,否则邻近操作符的
优先级可能会产生不可预料的后果,所以建
议宏在书写的时候多写括号
函数参数只在函数调用的时候求值一次,它的结果值传递给函数。表达式的求值结果更容易预测
带有副作用的参数参数可能被替换到宏体中的多个位置,如果
宏的参数被多次计算,带有副作用的参数求
值可能会产生不可预料的结果
函数参数只在传参的时候求值一次,结果更容易控制
参数类型宏的参数与类型无关,只要对参数的操作是
合法的,它就可以使用于任何参数类型。
 
函数的参数是与类型有关的,如果参数的类型不同,就需要不同的函数,即使它们执行的任务是相同的。
调试宏是不方便调试的函数是可以逐语句调试的
递归宏是不能递归的函数是可以递归的

7.#和##

7.1 #运算符

#的功能是将其后面的宏参数进行字符串化操作(Stringfication),简单说就是在对它所引用的宏变量通过替换后在其左右各加上一个双引号

当我们有一个变量 int a = 10; 的时候,我们想打印出: the value of a is 10 . 就会写出这样的代码:

#include<stdio.h> #define PRINT(n) printf("the value of n is %d\n", n); int main() { 	int a = 10; 	PRINT(a); 	return 0; }

我们发现:上面字符串中的字符n被当作普通文本来处理,而不是被当作一个可以被替换的语言符号。而我们希望在输出的字符串中包含宏参数,那我们就可以使用“#”,它就可以不受字符串的约束,字符串里面的n也可以被当做宏参数替换。

#include<stdio.h> #define PRINT(n) printf("the value of "#n" is %d\n", n); int main() { 	int a = 10; 	PRINT(a); 	return 0; }

当我们把a替换到宏的体内时,就出现了#a,而#a就是转换为“a”,预处理为:

printf("the value of ""a" " is %d\n", a);

运行代码就能在屏幕上打印:


7.2 ##运算符

## 可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。 ## 被称为记号粘合

这里我们想想,写一个函数求2个数的较大值的时候,不同的数据类型就得写不同的函数。例如:

int int_max(int x, int y) { 	return x > y ? x : y; } float float_max(float x, float y) { 	return x > y ? x : y; }

但是这样写起来太繁琐了,我们可以这样写:

#include<stdio.h> #define GENERIC_MAX(type) \ type type##_max(type x,type y)\ {   \     return x > y ? x : y;\ } //定义函数 GENERIC_MAX(int);//int_max GENERIC_MAX(float);//float max

也可以理解为:不加##,他会认为type_max函数名是一个符号,就不进行替换了,而用##给它隔开,type##_max就会被拆解替换成int_max或float_max来形成不同的函数名。实际上##起到一个记号粘合的效果。 

使用:

#include<stdio.h> #define GENERIC_MAX(type) \ type type##_max(type x,type y)\ {   \     return x > y ? x : y;\ } //定义函数 GENERIC_MAX(int);  //替换到宏体内后int##_max ⽣成了新的符号 int_max做函数名 GENERIC_MAX(float);//替换到宏体内后float##_max ⽣成了新的符号 float_max做函数名 int main() { 	int r1 = int_max(3, 5); 	printf("%d\n", r1); 	float r2 = float_max(3.2f, 4.6f); 	printf("%f\n", r2); 	return 0; }

运行结果:


8.命名约定

一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。 那我们平时的书写习惯是:

把宏名全部大写

函数名不全部大写


9. #undef

这条指令用于移除一个宏定义。

#undef NAME //如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。

例如:


10.命令行定义

许多C 的编译器提供了一种能力,允许在命令行中定义符号。用于启动编译过程。

例如:当我们根据同一个源文件要编译出一个程序的不同版本的时候,这个特性有点用处。(假定某个程序中声明了一个某个长度的数组,如果机器内存有限,我们需要一个很小的数组,但是另外一个机器内存大些,我们需要一个较大的数组。)

例如:在gcc编译器下测试下面代码

#include <stdio.h> int main() { 	int array[SZ]; //SZ未定义 	int i = 0; 	for (i = 0; i < SZ; i++) 	{ 		array[i] = i+1; 	} 	for (i = 0; i < SZ; i++) 	{ 		printf("%d ", array[i]); 	} 	return 0; }

命令行中输入编译指令:

gcc -D SZ=10 programe.c

在编译的过程中,通过命令行的方式指定代码中未定义的符号。


11.条件编译

在编译一个程序的时候我们如果要将一条语句(一组语句)编译或者放弃是很方便的。因为我们有条件编译指令。

比如:调试性的代码,删除可惜,保留又碍事,我们可以选择性的编译。 

#include <stdio.h> #define __DEBUG__  //如果不想让11行的代码编译就屏蔽掉这段代码 int main() { 	int i = 0; 	int arr[10] = { 0 }; 	for (i = 0; i < 10; i++) 	{ 		arr[i] = i;       #ifdef __DEBUG__ 		printf("%d\n", arr[i]);//为了观察数组是否赋值成功。        #endif  	} 	return 0; }

常见的条件编译指令:

1.判断条件真假 #if 常量表达式 //... #endif  2.多个分⽀的条件编译 #if 常量表达式 //... #elif 常量表达式 //... #else //... #endif  3.判断是否被定义 #if defined(symbol) //...                         //符号被定义 #endif  #ifdef symbol //...                         //符号被定义 #endif  #if !defined(symbol) //...                         //符号不被定义 #endif  #ifndef symbol //...                         //符号不被定义 #endif  4.嵌套指令 #if defined(OS_UNIX)      #ifdef OPTION1              unix_version_option1();      #endif      #ifdef OPTION2              unix_version_option2();      #endif #elif defined(OS_MSDOS)      #ifdef OPTION2              msdos_version_option2();      #endif #endif

12.头文件的包含

头文件被包含的方式:

12.1 本地文件包含

#include "filename"

查找策略先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头文件一样在标准位置查找头文件。 如果找不到就提示编译错误。

Linux环境的标准头文件的路径:

 /usr/include 

VS环境的标准头文件的路径:

C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\include //这是VS2013的默认路径

12.2 库文件包含 

#include <filename.h>

查找策略查找头文件直接去标准路径下去查找,如果找不到就提示编译错误。

那么对于库文件是否也可以使用 “ ” 的形式包含?

可以但是这样做查找的效率较低,并且这样也不容易区分是库文件还是本地文件了


12.3 嵌套文件包含 

我们已经知道, #include 指令可以使另外一个文件被编译。就像它实际出现于 #include 指令的地方一样。 这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。 一个头文件被包含5次,那就实际被编译5次,如果重复包含,对编译的压力就比较大。

test.c 

#include "test.h" #include "test.h" #include "test.h" #include "test.h" #include "test.h" int main() {  	return 0; }

test.h

void test(); struct Stu { 	int age; 	char name[20]; };

如果直接这样写,test.c文件中将test.h包含5次,那么test.h文件的内容将会被拷贝5份在test.c中。如果test.h 文件比较大,这样预处理后代码量会剧增。如果工程比较大,有公共使用的头文件,被大家都能使用,又不做任何的处理,那么后果真的不堪设想。

那么使用条件编译可以很好的解决文件被重复引入的问题

test.h文件中增加几句代码变成:

#ifndef __TEST_H__ #define __TEST_H__  void test(); struct Stu { 	int id; 	char name[20]; };  #endif 

或者

#pragma once  void test(); struct Stu { 	int id; 	char name[20]; };

这样经过预处理后的test.h文件的内容最终只会拷贝一份在test.c文件中,这样就避免了头文件被重复多次引用

广告一刻

为您即时展示最新活动产品广告消息,让您随时掌握产品活动新动态!