(一)C语言预处理理论
1.由源码到可执行程序的过程(逐步细化)
- 源码.c->(编译)->elf可执行程序
- 源码.c->(编译)->目标文件.o->(链接)->elf可执行程序
- 源码.c->(编译)->汇编文件.S->(汇编)->目标文件.o->(链接)->elf可执行程序
- 源码.c->(预处理)->预处理过的.i源文件->(编译)->汇编文件.S->(汇编)->目标文件.o->(链接)->elf可执行程序
- 预处理用预处理器,编译用编译器,汇编用汇编器,链接用链接器,这几个工具再加上其他一些额外的会用到的可用工具,合起来叫编译工具链。gcc就是一个编译工具链。
2.预处理的意义
- 编译器本身的主要目的是编译源代码,将C的源代码转化成.S的汇编代码。编译器聚焦核心功能后,就剥离出了一些非核心的功能到预处理器去了。
- 预处理器帮编译器做一些编译前的杂事。
3.编程中常见的预处理
#include(#include <>和#include ""的区别) |
注释 |
#if #elif #endif #ifdef |
宏定义 |
4.gcc中只预处理不编译的方法
- gcc编译时可以给一些参数来做一些设置,譬如
gcc xx.c -o xx
可以指定可执行程序的名称;譬如gcc xx.c -c -o xx.o
可以指定只编译不连接,也可以生成.o的目标文件。
gcc -E xx.c -o xx.i
可以实现只预处理不编译。一般情况下没必要只预处理不编译,但有时候这种技巧可以用来帮助我们研究预处理过程,帮助debug程序。
- 实例
#define pChar char *
typedef char * PCHAR;
int main(void)
{
pChar a, b;
PCHAR c, d;
return 0;
}
typedef char * PCHAR;
int main(void)
{
char * a, b;
PCHAR c, d;
return 0;
}
5.总结
- 宏定义被预处理时的现象有:
- 第一,宏定义语句本身不见了(可见编译器根本就不认识#define,编译器根本不知道还有个宏定义);
- 第二,typedef重命名语言还在,说明它和宏定义是有本质区别的(说明typedef是由编译器来处理而不是预处理器处理的);
6.头文件包含
- #include <> 和 #include"“的区别:<>专门用来包含系统提供的头文件(就是系统自带的,不是程序员自己写的),”"用来包含自己写的头文件;更深层次来说:<>的话C语言编译器只会到系统指定目录(编译器中配置的或者操作系统配置的寻找目录,譬如在ubuntu中是/usr/include目录,编译器还允许用-I来附加指定其他的包含路径)去寻找这个头文件(隐含意思就是不会找当前目录下),如果找不到就会提示这个头文件不存在。
- ""包含的头文件,编译器默认会先在当前目录下寻找相应的头文件,如果没找到然后再到系统指定目录去寻找,如果还没找到则提示文件不存在。
- 总结+注意:规则虽然允许用双引号来包含系统指定目录,但是一般的使用原则是:如果是系统指定的自带的用<>,如果是自己写的在当前目录下放着用"",如果是自己写的但是集中放在了一起专门存放头文件的目录下将来在编译器中用-I参数来寻找,这种情况下用<>。
- 头文件包含的真实含义就是:在#include<xx.h>的那一行,将xx.h这个头文件的内容原地展开替换这一行#include语句。过程在预处理中进行。
7.注释
- 注释是给人看的,不是给编译器看的。
- 编译器既然不看注释,那么编译时最好没有注释的。实际上在预处理阶段,预处理器会拿掉程序中所有的注释语句,到了编译器编译阶段程序中其实已经没有注释了。
- 实例
8.条件编译
- 有时候我们希望程序有多种配置,我们在源代码编写时写好了各种配置的代码,然后给个配置开关,在源代码级别去修改配置开关来让程序编译出不同的效果。
- 条件编译中用的两种条件判定方法分别是#ifdef 和 #if
- 区别:#ifdef XXX判定条件成立与否时主要是看XXX这个符号在本语句之前有没有被定义,只要定义了(我们可以直接#define XXX或者#define XXX 12或者#define XXX YYY)这个符号就是成立的。
- #if (条件表达式),它的判定标准是()中的表达式是否为true还是flase,跟C中的if语句有点像。
- 实例
(二)宏定义
1.宏定义的规则和使用解析
- 宏定义的解析规则就是:在预处理阶段由预处理器进行替换,这个替换是原封不动的替换。
- 宏定义替换会递归进行,直到替换出来的值本身不再是一个宏为止。
- 一个正确的宏定义式子本身分为3部分:第一部分是#dedine ,第二部分是宏名 ,剩下的所有为第三部分。
- 宏可以带参数,称为带参宏。带参宏的使用和带参函数非常像,但是使用上有一些差异。在定义带参宏时,每一个参数在宏体中引用时都必须加括号,最后整体再加括号,括号缺一不可。
- 实例
2.宏定义示例1:MAX宏,求2个数中较大的一个 #define MAX(a, b) (((a)>(b)) ? (a) : (b))
- 关键:
- 第一点:要想到使用三目运算符来完成。
- 第二点:注意括号的使用
3.宏定义示例2:SEC_PER_YEAR,用宏定义表示一年中有多少秒
#include <stdio.h>
#define SEC_PER_YEAR (365*24*60*60UL)
int main(void)
{
unsigned int sec = SEC_PER_YEAR;
printf("sec = %d.\n", sec);
return 0;
}
- U和L是 整数文字量的后缀修饰,用于显示指明整数文字量的类型为unsigned int(U)和long int(L)。
类似的还有浮点数文字量的后缀修饰F或f,用于指明文字量表示的是一个float,而不是默认情况下的double。
4.带参宏和带参函数的区别(宏定义的缺陷)
- 宏定义是在预处理期间处理的,而函数是在编译期间处理的。这个区别带来的实质差异是:宏定义最终是在调用宏的地方把宏体原地展开,而函数是在调用函数处跳转到函数中去执行,执行完后再跳转回来。
- 注:宏定义和函数的最大差别就是:宏定义是原地展开,因此没有调用开销;而函数是跳转执行再返回,因此函数有比较大的调用开销。所以宏定义和函数相比,优势就是没有调用开销,没有传参开销,所以当函数体很短(尤其是只有一句话时)可以用宏定义来替代,这样效率高。
- 带参宏和带参函数的一个重要差别就是:宏定义不会检查参数的类型,返回值也不会附带类型;而函数有明确的参数类型和返回值类型。当我们调用函数时编译器会帮我们做参数的静态类型检查,如果编译器发现我们实际传参和参数声明不同时会报警告或错误。
- 注:用函数的时候程序员不太用操心类型不匹配因为编译器会检查,如果不匹配编译器会叫;用宏的时候程序员必须很注意实际传参和宏所希望的参数类型一致,否则可能编译不报错但是运行有误。
- 总结:宏和函数各有千秋,各有优劣。总的来说,如果代码比较多用函数适合而且不影响效率;但是对于那些只有一两句话的函数开销就太大了,适合用带参宏。但是用带参宏又有缺点:不检查参数类型。
- 实例
5.内联函数和inline关键字
- 内联函数通过在函数定义前加inline关键字实现。
- 内联函数本质上是函数,所以有函数的优点(内联函数是编译器负责处理的,编译器可以帮我们做参数的静态类型检查);但是他同时也有带参宏的优点(不用调用开销,而是原地展开)。所以几乎可以这样认为:内联函数就是带了参数静态类型检查的宏。
- 当我们的函数内函数体很短(譬如只有一两句话)的时候,我们又希望利用编译器的参数类型检查来排错,我还希望没有调用开销时,最适合使用内联函数。