预处理详解
- 一 预处理阶段
- 1 知识背景:
- 2 预定义符号
- 3 #define 定义常量
- 当定义的标识符的值过长时:
- 注意,如果#define定义的标识符,其值的末尾有; 则说明; 是该标识符值的一部分
- 4 #define 定义宏
- 宏的声明方式:
- 当传入的参数是一个符号时:
- 当传入的参数是一个表达式时:
- 带有副作用的宏参数
- 宏替换的规则
- 5 宏与函数的对比
- (1)在执行小型运算时,宏更有优势:
- (2)宏对于函数的劣势
- (3)总的对比
- 6 #与##操作符
- 6.1 #操作符
- 6.2 ## 操作符
- 7 #undef
- 8 条件编译
- 举例:
- 常见的条件编译指令
- 9 头文件的包含
- 9.1 头文件包含的形式:
- 9.1.1 本地文件的包含:
- 9.1.2 库文件的包含
- 两种包含方式查找策略的不同:
- 9.2 嵌套文件包含
一 预处理阶段
1 知识背景:
一个c语言项目可能由多个.c文件与.h文件组成,那么它是如何转换成可执行程序的呢?
每一个.c文件单独通过编译器转换成目标文件
在windows系统中,目标文件的后缀为.obj,在linux系统中,目标文件的后缀为.o
通过链接器,将多个目标文件与链接库链接起来,生成可执行程序
链接库是指运行时的库(支持程序运行的基本函数的集合)或者第三方库(比如c语言标准库)
用高级编程语言所写的代码,要执行,需要在转换成操作系统可执行的二进制
代码
大体的转换流程是:编译-链接 转换成可执行程序
如果再细分,则是预处理-编译-汇编-链接 转换成可执行程序
如图:
本篇所写仅涉及与预处理阶段相关的内容【处理的代码方面】
2 预定义符号
c语言中定义了一些预定义符号,这些预定义符号可以直接使用,当然这些预定义符号也在预处理阶段处理!
//_ _FLIE_ _ //用来编译的源文件
//__LINE__ //在当前文件中被编译的代码行号
//__DATE__ //文件被编译的日期
//__TIME__ //文件被编译的时间
//__STDC__ 如果编译器完全实现 ANSI C 则值为1,否则未定义
// 至于这个是关于什么的信息,我不清楚
#include<stdio.h>
int main() {
printf("%s\n", __FILE__);
printf("%d\n", __LINE__);
printf("%s\n", __DATE__);
printf("%s\n", __TIME__);
// printf("%d\n", __STDC__);//在VS编译器中并没有STDC的规定!
return 0;
}
在预处理之后,生成的文件test.i 为:
3 #define 定义常量
在程序中#define 定义的符号,在经过预处理之后,直接被其值替换
#include<stdio.h>
#define MAX 100 // 定义符号 值为数值常量
#define Str "hello World"// 定义符号 值为字符串常量
#define MA 'a' // 定义符号 值为字符
int main() {
// #define 可以定义符号,其值为常量
printf("%d\n", MAX);
printf("%s\n", Str);
printf("%c\n", MA);
return 0;
}
当定义的标识符的值过长时:
#define Print printf("file:%s\tline:%d\tDATE:%s\tTIME:%s\t",__FILE__,__LINE__,__DATE__,__TIME__);
#include<stdio.h>
//在默认情况下#define定义标识符只能在一行中定义,但如果想换行的话,则需在每一行末尾加上 \,意为这一行的扩展
#define Print printf("file:%s\t \
line:%d\t \
DATE:%s\t \
TIME:%s\t", \
__FILE__,__LINE__,__DATE__,__TIME__);
int main() {
return 0;
}
注意,如果#define定义的标识符,其值的末尾有; 则说明; 是该标识符值的一部分
4 #define 定义宏
宏是带有参数的标识符,参数与标识符对应的值有关
宏的声明方式:
#define name(parament-Iist ) stuff
其中stuff是宏体,parament-list代表用,隔开的参数集
注意:name与()的左括号,中间不能有间隔,否则编译器会认为(parament-Iist)是宏体的一部分
当传入的参数是一个符号时:
#include<stdio.h>
#define SQUARE(x) x*x
int main() {
int c = SQUARE(5);
printf("%d\n", c);
return 0;
}
当传入的参数是一个表达式时:
#include<stdio.h>
#define SQUARE(x) x*x
int main() {
int c = SQUARE(5+1);
printf("%d\n", c);
//预期值是36 ,但结果是;
return 0;
}
这是因为,在经历预处理时,先将宏的形参变为实参变为:
// 5+1*5+1
再将转换后的文本替换到原来程序所在的文本的位置即:
#include<stdio.h>
#define SQUARE(x) x*x
int main() {
int c = 5+1*5+1;
//此时的结果为1*5 + 6 ==11
printf("%d\n", c);
return 0;
}
如何解决这个问题呢?只需要在宏的定义中,将每一个参数用()括起来,再将整个宏的体括起来
这是为了防止外在的程序直接与宏的部分体进行计算,例如:
#include<stdio.h>
#define ADD(x) x+x
int main() {
int a = 5;
//目标打印出50,但是结果为:
printf("%d\n", ADD(5) * a);
return 0;
}
解决方法:
#include<stdio.h>
#define ADD(x) ((x)+(x)) // 将参数括起来,再将整个宏体括起来
int main() {
int a = 5;
printf("%d\n", ADD(5) * a);
return 0;
}
带有副作用的宏参数
当带副作用的参数(所谓副作用即在计算结束后,自身的值发生变化)在宏体中,超过一次,
就会可能导致错误的结果
例:
#include<stdio.h>
#define MAX(x,y) ((x)>(y)?x:y)
int main() {
int a = 3;
int b = 5;
//如果是按照函数的思维,c 返回值应该是5,但是结果是:
int c = MAX(a++,b++);
printf("%d\n", a);
printf("%d\n", b);
printf("%d\n", c);
return 0;
}
所以尽量不要用这种带有副作用的形参
x++//有副作用
x+1//挺好
宏替换的规则
有以下几个步骤:
1 首先扫描宏的参数,如果参数中有#define定义的符号或宏,则替换成对应的值
2 随后将替换文本插入到程序中原文本处
3 最后重新扫描结果文本,将#define定义的宏或符号转换成宏体或值
举例:
#include<stdio.h>
#define MAX(x,y) ((x)>(y)?(x):(y))
#define M 10
int main() {
int a = 3;
int b = 5;
int c = MAX(M, a);
// 预处理时,1 首先扫描宏的参数,发现#define定义的M,将其转换成其值 10:
// 2 int c = MAX(10,a);这条语句代替int c = MAX(M,a);插入到程序中
// 3 最后再扫描结果文本 int c = MAX(10,a);发现MAX也是#define定义,
//转换为其对应的宏体
// ((10)>(a)?(10):(a))
// 最后变为:
//int c = ((10)>(a)?(10):(a));
return 0;
}
注意:1 宏参数或#define定义的符号中可以出现其他#define定义的符号或宏,但是对于宏不能出现递归
2 当预处理器搜索#define定义的符号与宏时,字符串常量的内容并不被搜索!
5 宏与函数的对比
(1)在执行小型运算时,宏更有优势:
举例:
#include<stdio.h>
//#define MAX(x,y) ((x)>(y)?(x):(y))
int Max(int x, int y) {
return x > y ? x : y;
}
int main() {
int a = 3;
int b = 5;
//int c = MAX(a, b);
int c = Max(a, b);
return 0;
}
用函数所需的指令:
上面是调用函数执行的指令,共用了11条
这是执行函数中的语句所用的指令:
这是返回函数值所执行的指令:
用宏需要的指令:
结果一目了然,说明当功能运算语句占比较小时,宏的执行更能够节省时间
(2)宏对于函数的劣势
1 除非宏比较小,否则宏替换到程序中的代码量极大,预处理之后会产生极大的冗余代码
而函数则只有固定的一份代码
2 宏是无法调试的,因为我们看到的代码与预处理替换后的代码不同,我们不能查看到问题出现在哪里
3 宏的参数没有类型,不够严谨(当然这也是宏对于函数的优势,只能说是一把双刃剑)
(3)总的对比
6 #与##操作符
6.1 #操作符
此操作符的作用将宏的参数转换成**字符串字面量**(就是转换成参数的标识符的字符串形式)
它仅允许出现在带参数的宏体中(或者说是宏的替换列表中)
举例:
// 在举例之前,先补充一个背景知识点
#include<stdio.h>
int main() {
printf("helloworld\n");
// 如果用两对双引号打印呢?
printf("hello" "world\n");
return 0;
}
#include<stdio.h>
//用#操作符将参数n变为字符串字面量,即参数的标识符的字符串状态
#define DIGIT(n) printf("the value " #n " is %d\n",n);
int main() {
int a = 3;
//在传入实参后,替换的文本实际为:
//printf("the value " "a" "is %d\n", a);
DIGIT(a);
return 0;
}
6.2 ## 操作符
##操作符可以把它两边的符号合成一个符号,它允许宏定义从分离的文本片段
中创建标识符,这样的连接必须产生一个合法的标识符
7 #undef
用于移除一个宏定义
#undef NAME // 如果移除一个宏定义,移除其名字即可
#include<stdio.h>
#define SQUARE(x) x*x
int main() {
//#undef SQUARE
printf("%d\n", SQUARE(3));
return 0;
}
#include<stdio.h>
#define SQUARE(x) x*x
int main() {
#undef SQUARE
printf("%d\n", SQUARE(3));
return 0;
}
8 条件编译
我们可以通过一些语句来设定一些代码是否进行编译,这样即避免删掉可能有用的代码,又防止暂时不需要的代码消耗资源
举例:
#include<stdio.h>
#define DEBUG
int main() {
int arr[10] = { 0 };
for (int i = 0; i < 10; i++) {
arr[i] = i;
#ifdef DEBUG // 如果DEBUG被定义则下面这条语句可执行,反之不可
printf("%d\n", arr[i]);
#endif
}
return 0;
}
常见的条件编译指令
1 格式: #if 常量表达式
#endif
#if作为开始的标志,#endif作为结束的标志,如果常量表达式的返回值>0
则指令中间的代码块便可执行
举例:
#include<stdio.h>
#define M 10
int main() {
#if M>0
printf("hehe\n");
#endif // M>0
return 0;
}
2 多分支的条件编译指令
举例:
#include<stdio.h>
#define M 3
int main() {
#if M==1
printf("hehe");
#elif M==2
printf("haha");
#else
printf("wawa");
#endif // M=1
return 0;
}
3 判断是否被定义的条件编译
#include<stdio.h>
#define DEBUG
int main() {
int arr[10] = { 0 };
for (int i = 0; i < 10; i++) {
arr[i] = i;
//#if defined(symbol) #endif 组合 与#ifdef symbol #endif 组合的功能相同,
// 如果symbol被定义,则指令中的代码块可执行
#if defined(DEBUG)
//#ifdef DEBUG
printf("%d\n", arr[i]);
//#endif
#endif
}
return 0;
}
3.2 如果是不定义情况下可执行代码的条件编译:
#include<stdio.h>
#define M 10
int main() {
#ifndef M
printf("haha");
#endif // !M
return 0;
}
4 嵌套指令,前面的几个指令可以嵌套,就像是if else语句一样,
大家可以尝试一下
9 头文件的包含
头文件是存放函数声明的文件,让其他函数调用使用
9.1 头文件包含的形式:
9.1.1 本地文件的包含:
当我们引用本地文件时,即自己写的文件,格式为:
#include " FileName"
9.1.2 库文件的包含
当我们调用开发环境为我们提供的标准库函数时,就需要引用相应的头文件
格式为:#include <FileName>
比如调用下面的printf函数,就需要包含stdio.h文件
两种包含方式查找策略的不同:
1 对于用" " 包含头文件的形式,在查找相应的头文件时,先在本文件的
目录中查找,如果找到了则包含进来,如果没找到则去存放库函数的
位置所在的路径去查找。
2 而用<>包含头文件的形式,在查找相应的头文件时,直接去存放库函数
的位置所在的路径去查找
所以库文件也可以用" " 来包含,但是执行起来比<>慢
举例1:
找到了.h文件
在电脑中找到存放stdio.h头文件的路径
9.2 嵌套文件包含
当我们在调用头文件时,可能不止调用一个 ,如果出现这种情况:
如果文件的调用结构是这样的,那对于test.c来说,是调用了两次con.h
如果结构更复杂呢?会导致代码量的极大冗余
这个问题怎么解决呢?用条件编译!
举例:
#ifndef DEBUG
#define DEBUG
int Add(int x, int y);
#endif
// 这些代码的执行规则是:如果.h文件第一次被调用,那此时DEBUG还没有被定义
// 就定义DEBUG,然后将文件包含过去,即将声明赋值过去,当.h文件又被调用时,则
// 因为DEBUG已经被定义,所以指令中间的代码不被编译,不会被包含过去,而总项目
// 只需要声明这个.h文件一次即可,这样就避免了重复声明!
或者用
#pragma once
来避免头文件的重复引入