一、翻译环境和运行环境
在ANSI C的任何⼀种实现中,存在两个不同的环境
第一种是翻译环境,在这个环境中,源代码被转换为可执行的二进制指令。翻译环境即我们日常使用编译器,将一个 " xxx.c " 的文件最终变成一个 " xxx.exe " 的可执行文件的一个过程。
第二种是运行环境,它用于实际执行代码。运行环境一般是由操作系统对 " xxx.exe " 可执行文件进行解析执行的结果。
二、预编译
1.预定义符号
C语言设置了⼀些预定义符号,可以直接使用,预定义符号也是在预处理期间处理的。
__FILE__ //进⾏编译的源⽂件 __LINE__ //⽂件当前的⾏号 __DATE__ //⽂件被编译的⽇期 __TIME__ //⽂件被编译的时间 __STDC__ //如果编译器遵循ANSI C,其值为1,否则未定义
2.#define
(1)#define定义常量
在define定义标识符的时候,不要在最后加上;
#define MAX 1000 if(condition) max = MAX; else max = 0;
加了分号后,if和else之间就是2条语句,而没有大括号的时候if后边只能有一条语句。这⾥会出现语法错误。
(2)#define定义宏
#include <stdio.h> #define MAX(x,y) (x>y?x:y) int main() { int a = 1; int b = 5; int E = MAX(a, b); // 预处理后变成 int E = (1>5?1:5); printf("%d\n", E); return 0; }
(2.1)宏的陷阱
#include <stdio.h> #define SQ(x) x*x int main() { int a = 6; int e1 = SQ(a); // 预处理后:e1 = 6*6; //36 int e2 = SQ(a + 1); // 预处理后:int e2 = 6+1*6+1; //8 printf("%d\n", result1 ); printf("%d\n", result2); return 0; }
宏带来了运算符优先问题。由于 #define 在定义宏的时候,是直接对参数进行替换的。所以我们第二个预期为 " 49" 的结果,最终变成了 8.
#include <stdio.h> #define SQ(x) (x)*(x) int main() { int a = 6; int e1 = SQ(a); // 预处理后:e1 = (6)*(6); //36 int e2 = SQ(a + 1); // 预处理后:int e2 = (6+1)*(6+1); //49 printf("%d\n", result1 ); printf("%d\n", result2); return 0; }
可以利用加括号的方式,避免掉入宏的陷阱。解决优先级的运算符的问题
(2.2)带有副作用的宏参数
#define MAX(a, b) ( (a) > (b) ? (a) : (b) ) int main() { x = 5; y = 8; z = MAX(x++, y++); printf("x=%d y=%d z=%d\n", x, y, z); }
z = ( (x++) > (y++) ? (x++) : (y++)); //x=6 y=10 z=9
(3)#define 定义宏和函数的区别
(4)#undef
#undef 用于移除一个宏定义,在err的上一行,就是移除了 MAX 这个宏。之后再使用的时,就会报错
#include <stdio.h> #define MAX(x,y) (x>y?x:y) int main() { int a = 1; int b = 25; int E = MAX(a, b); printf("%d\n", E); #undef MAX int E = MAX(a, b); // err return 0; }
(5)#和##
1.#运算符将宏的⼀个参数转换为字符串字⾯量。它仅允许出现在带参数的宏的替换列表中。 #运算符所执行的操作可以理解为”字符串化“。有⼀个变量 int x = 10; 的时候,打印出 the value of x is 10 . 就可以写:
#define PRINT(n) printf("the value of "#n " is %d", n);
PRINT(x);当我们把x替换到宏的体内时,就出现了#x,而#x就是转换为
printf("the value of ""x" " is %d", x);//预处理之后
2.## 可以把位于它两边的符号合成⼀个符号,它允许宏定义从分离的文本片段创建标识符。 ## 被称 为记号粘合 这样的连接必须产⽣⼀个合法的标识符。否则其结果就是未定义的。
#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 m = int_max(2, 3); printf("%d\n", m); float fm = float_max(3.5f, 4.5f); printf("%f\n", fm); return 0; }
3.头文件的包含
#include 头文件包含属于预编译的过程,它其实也是进行了相关的文本替换。但C语言 的头文件分为两种,第一种是和库相关的库文件;第二种是本地文件包含。
本地头文件的查找策略:先在源文件所在目录下查找,如果该头文件未找到,编译器就像查找库函数头⽂件⼀样在 标准位置查找头⽂件。 如果找不到就提示编译错误。对于库⽂件也可以使用 “” 的形式包含,但是这样做查找的效率就低些,当然这样也不容易区分是库文件还是本地文件了。
#include<stdio.h> //库文件 #include"SList.h" //本地文件
#include 指令可以使另外⼀个⽂件被编译。就像它实际出现于 #include指令的地方⼀样。这种替换的方式很简单:预处理器先删除这条指令,并用包含文件的内容替换。 ⼀个头⽂件被包含10次,那就实际被编译10次,如果重复包含,对编译的压力就比较大。
如果xxx.h⽂件比较大,这样预处理后代码量会剧增。使用条件编译指令来防止多次调用xxx.h。
#ifndef __TEST_H__ #define __TEST_H__ //头⽂件的内容 #endif
或者使用pragme
#pragma once //文件
三、翻译环境
其实翻译环境是由编译和链接两个大的过程组成的,而编译又可以分解成:预处理(预编译)、编译、汇编三个过程
1. 预编译
预处理阶段主要处理那些源文件中#开始的预编译指令。如:#include,#define,处理的规则如下:
(1)将所有的#define 删除,并展开所有的宏定义。
(2)处理所有的条件编译指令,如: #if、#ifdef、#elif、#else、#endif 。
(3)处理#include预编译指令,将包含的头文件的内容插⼊到该预编译指令的位置,这个过程是递归进行的,也就是说被包含的头文件也可能包含其他文件。
(4)删除所有的注释
(5)添加行号和文件名标识,方便后续编译器生成调试信息等。或保留所有的#pragma的编译器指令,编译器后续会使用。经过预处理后的.i⽂件中不再包含宏定义,因为宏已经被展开。并且包含的头⽂件都被插⼊到.i⽂件中。所以当我们无法知道宏定义或者头⽂件是否包含正确的时候,可以查看预处理后的.i⽂件来确认。
2.编译
编译过程就是将预处理后的文件进行⼀系列的:词法分析、语法分析、语义分析及优化,生成相应的汇编代码文件。
词法分析:将源代码程序被输⼊扫描器,扫描器的任务就是简单的进行词法分析,把代码中的字符分割成⼀系列 的记号(关键字、标识符、字⾯量、特殊字符等)
语法分析:接下来语法分析器,将对扫描产生的记号进行语法分析,从而产生语法树。这些语法树是以表达式为节点的树。
语义分析:由语义分析器来完成语义分析,即对表达式的语法层⾯分析。编译器所能做的分析是语义的静态分 析。静态语义分析通常包括声明和类型的匹配,类型的转换等。这个阶段会报告错误的语法信息
3.汇编
汇编器是将汇编代码转转变成机器可执行的指令,每⼀个汇编语句⼏乎都对应⼀条机器指令。就是根 据汇编指令和机器指令的对照表⼀⼀的进行翻译,也不做指令优化
4.链接
链接是⼀个复杂的过程,链接的时候需要把⼀堆文件链接在⼀起才生成可执行程序。 链接过程主要包括:地址和空间分配,符号决议和重定位等这些步骤。 链接解决的是⼀个项目中多⽂件、多模块之间互相调用的问题。