目录
编译环境和运行环境
编译过程详解
预处理
编译
汇编
链接
运行环境概述
预处理指令详解
预定义符号
\ - 续行符
# - 宏参数的字符串转换
## - 内容拼接
#define - 宏定义
宏的危险性
#define 替换规则
#define的特殊用法
宏和函数的区别
#undef - 移除宏
条件编译
文件包含
在ANSI C的任何一种实现中,都存在两个不同的环境。
(1)翻译环境,在这个环境中源代码被转换为可执行的机器指令。
(2)执行环境,它用于实际执行代码。
通常我们编写的C语言程序,是在编译阶段(编译环境下)先将所有的源文件(.c文件)编译为一个个目标文件,然后通过链接器,将他们链接为一个可执行程序(类似于一个打包的过程),然后这个可执行程序就会在运行阶段(运行环境下)运行。大概的过程如下图
在C语言中.c文件需要经过4个过程过程才能生成我们所需要的可执行程序,它们分别是:预处理、编译、汇编、链接。接下来我们将详细讲解这四个过程。
首先是预处理阶段,在预处理阶段时编译器会处理.c文件中的预处理内容(头文件展开、宏替换、预定义符号等等),并进行注释删除,最终生成.i文件。即此时生成的.i文件内容也都是C语言的内容,里面包含了头文件展开的内容(很长的那种),并对宏定义的内容进行替换(这也就是为什么宏在调试时检测不到的原因,因为已经被替换成了别的东西),同时删除了注释。此时的.i文件更像一个完整版的C语言源码程序。比如hello.c中第一行的#include命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中,结果就得到了另一个C程序,通常是以.i作为文件扩展名。
在编译阶段我们对预处理阶段生成的.i文件进行操作。在编译阶段我们将.i文件中的C语句进行语法分析、词法分析、语义分析、符号汇总等一系列操作,最终生成一个后缀为.s的汇编文件,即.s的文件内容都是汇编语言的内容,汇编语言程序中的每条语句都以一种标准的文本格式确切的描述了一条低级机器语言指令。
在汇编阶段我们对编译阶段生成的.s文件进行操作。将hello.s翻译成机器语言指令,把这些指令打包成一种可重定位目标程序的格式,最终生成一个.o文件(VS生成的是.obj文件),其中.o文件是一个二进制文件(这是因为计算机的底层是只识别二进制信息的),它的字节编码是机器语言指令而不是字符,如果我们在文本文件中打开hello.o文件,看到的将是一堆乱码。其中,在汇编阶段有形成符号表的这一过程,其中不同文件的同名函数以及同名全局变量等都是在这个阶段进行处理的。因为在编译阶段,这些内容都是以符号的形式存在,在符号表汇总时,会对其进行相应的处理。
汇编阶段我们生成了一个个.o的二进制文件,在链接阶段我们对这一个个.o文件进行“合并”,此过程中链接器(ld)进行合并段表,合并与重定位符号表等操作,最终将多个.o文件生成为一个可执行文件(Windows环境下是exe文件),此可执行文件可以被加载到内存中,由系统运行。
程序执行的过程:
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用main函数。
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止。
__FILE__ //进行编译的源文件
__LINE__ //文件当前的行号
__DATE__ //文件被编译的日期
__TIME__ //文件被编译的时间
__STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
用法示例:
printf("%d\n", __STDC__);
printf("%s\n", __FUNCTION__);
printf("%s\n", __FUNCDNAME__);
当然,C语言中的预定义符号远不止这些,这里只是简单列举一些相对常用的预定义符号。如果想要详细了解的话,可以去类似cplusplus这种的参考网站学习。
在编写预处理指令时,如果一行写入的数据太长,会影响阅读,这时可以在需要换行的地方加入'\'续行符,然后在\后换行。例如代码1和代码2是没有区别的。
//代码1
#define output(x) printf("the var's \
value is %d",\
x)
//代码2
#define output(x) printf("the var's value is %d", x)
首先我们来看一下如何把参数插入到字符串中, 我们看这样一段代码:
char* p = "hello ""bit\n";
printf("hello"" bit\n");
printf("%s", p);
运行结果:
hello bit
由此,我们发现,字符串是有自动连接的特点的。
那么我们接下来看这样一段代码:
#define PRINT(FORMAT, VALUE)\
printf("the value is "FORMAT"\n", VALUE);
那么这段代码的运行结果又是什么呢?
先不着急下结论,我们将其与下方的代码进行观察比较
#define PRINT(FORMAT, VALUE)\
printf("the value of " #VALUE "is "FORMAT "\n", VALUE);
不难发现,这两段代码只有#那里是有区别的,那么它们的效果一样吗?答案是一样的,所以我们推测#VALUE就等效于"VALUE"。
由此我们可以得出结论,#的作用就是将一个宏参数变成对应的字符串。
这里只有当字符串作为宏参数的时候才可以把字符串放在字符串中。
其实#与##的作用并没有什么联系,##可以把位于它两边的符号合成一个符号,它允许宏定义从分离的文本片段创建标识符。例如:
#define ADD_TO_SUM(num, value) \
sum##num += value
……
ADD_TO_SUM(5, 10);//给sum5增加10
简言之,##就相当于一个链条,起到的是连接的作用。但要特别注意,使用##连接必须产生一个合法的标识符,否则可能会导致很危险的未定义结果。
宏是一种批量处理的称谓,是在预编译的时候进行替换。宏的一个好处就是只要修改宏,其他地方在预编译的时候就会重新替换。而且作用范围是从定义宏的地方到本文件末尾。
宏的格式:
不带参宏: #define 标识符 变量
带参宏: #define 标识符(形参) 运算符
定义宏时的注意事项:
1、标识符和形参之间不能有空格,其余地方要有空格。预处理在末尾不加分号
2、不带参宏的标识符直接替代标识符后面的数值——字符型(串)、整型、浮点型等
3、带参宏的标识符(形参) 替代标识符后面的的运算符的结果,其中形参要进行赋值。带参宏就相当于一个简单的函数
宏的用法示例:
分析:三个宏定义,第一个求圆的半径,PI直接替代3.1415926。第二个求ab之积,78代表x 46代表y,其中x和y并不能直接单独使用(如不能直接x=78和scanf输入x)。第三个是条件运算符,其中78和467分别代表ab,同样XO(a,b)只能以数据的形式使用
宏定义只是简单的替换,不会自动加括号(即按照括号-乘除-加减的顺序计算),如果使用不当,那么很容易出现bug。下面将通过一个案例来分析。
//使用define时容易出现的bug
#define SUM(a,b) a*b //代码1
#define SUM(a,b) (a)*(b) //代码2
#define SUM(a,b) ((a)*(b)) //代码3
/*********************************/
printf("%d\n",SUM(7,8));//输出1
printf("%d\n",SUM(7,8+1));//输出2
printf("%d\n",64/SUM(7+1,8));//输出3
代码1、2、3看似效果一样,实则大不相同。具体的输出结果如下
输出1 |
输出2 |
输出3 |
|
代码1 |
56---7*8 |
57---7*8+1 |
17.14…---64/7+1*8 |
代码2 |
56---(7)*(8) |
63---(7)*(8+1) |
64---64/(7+1)*8 |
代码3 |
56---((7)*(8)) |
63---((7)*(8+1)) |
1---64/((7+1)*(8)) |
通过对比我们不难发现如果宏定义时没有考虑周全,很容易就会出现莫名其妙的bug
#define 替换规则在程序中扩展#define定义符号和宏时,需要涉及几个步骤。
1. 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
2. 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
3. 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
4. 宏参数和#define 定义中可以出现其他#define定义的符号。
5. 宏是无法实现递归的。
6. 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
#define 新变量名称 旧变量名称 #define entrance ent
#define scanf scanf_s
一般来讲函数的宏的使用语法很相似。所以语言本身没法帮我们区分二者。那我们平时的一个习惯是:把宏名全部大写函数名不要全部大写
宏被调少次就会展开多少次,执行代码的时承数调用的过程,不需要压栈弹栈。所以带参宏是浪费了空间节省时间。
函数只有一个入口,但每次调用函数都有要压栈出栈。所以说带参函数是浪费了时间节省了空间。
具体的对比图如下:
这条指令用于移除一个宏定义。
#undef NAME
//如果现存的一个名字需要被重新定义,那么它的旧名字首先要被移除。
(1)检测是否定义
#ifdef 常量表达式
代码段一
#else
代码段二
#endif
解释:如果在当前.c文件中 #ifdef 上边宏(define)定义过常量表达式,就编译代码段一,否则编译代码段二。注意区分选择性编译和 if else 语句的区别,if else 语句都会被编译,通过条件选择性执行代码。而选择性编译,只有一块代码被编译。其中 #ifdef 常量表达式也可以写成 #if defined(symbol)
代码示例:
#define AAA
int main(int argc, char *argy[])
{
#ifdef AAA
printf("hello kitty!!\n");
#else
printf("hello world\n");
#endif
return 0;
}
(2)检测是否未定义
#if !defined(symbol)
#ifndef symbol
上述(1)刚好与其互补,如果没定义过symbol……
(3)分支条件编译
#if 表达式
……
#elif 常量表达式
……
#elif 常量表达式
……
#else
……
#endif
上述内容与if - else if - else理解类似。
(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
#include< > 用尖括号包含头文件,在系统指定的路径下找头文件
#include" " 用双引号包含头文件,先在当前目录下找头文件,找不到,再到系统指定的路径下找
include 经常用来包含头文件,也可以包含 .c 文件,但是一般不包含.c文件
因为include 包含的文件会在预编译被展开,如果一个c 被包含多次,展开多次,会导致函数重复定义。所以轻易不要包含.c 文件。
预处理只是对include 等预处理操作进行处理并不会计,这个阶段即使有语法错误也不会报错,因为第二个阶段即编译阶段才进行语法检查。