程序环境和预处理:
翻译环境和执行环境
在ANSI C的任何一种实现中,存在两种不同的环境。
第一种是翻译环境,在这个环境中源代码被转换成可执行的机器指令。第二种是执行环境,它用于实际执行代码。
翻译环境
我们写的代码是如何从一开始的.c文件变化为可执行的.exe文件,就是在这一步完成的。
- 组成一个程序的每个源文件通过编译过程分别转换成目标代码
- 每个目标文件由链接器捆绑在一起,形成一个单一而完整的可执行程序
- 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且还可以搜索本地的程序库,找到需要的函数链接进程序中
下面就来详细讲一下编译的具体过程
编译分为三个阶段,预处理,编译,汇编。
1.预处理
步骤:
- 展开头文件
- 宏替换
- 去掉注释
- 条件编译
经过上述步骤后将.c文件处理成为.i文件
2.编译
步骤:
- 语法检查
- 将代码转换为汇编代码
这时.i文件会被处理成.s文件
3.汇编
步骤:
- 将汇编代码转换成为机器所能识别的二进制
这一步后,s文件被处理成为.o文件。
编译器的工作完成以后,就到了链接器的工作
链接
步骤:
- 将声明却又未定义的函数在其他文件中找到
这时候,多个.o文件将被处理成为可执行的exe文件。
运行环境
程序执行的过程:
- 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能使通过可执行代码置入只读内存来完成。
- 陈旭的执行便开始。接着调用main函数
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈,存储函数的局部变量和返回地址。程序同时也可以使用静态内存,存储于静态内存中的变量在程序的整个执行过程中一直保留它们的值。
- 终止程序。正常终于main函数;也有可能是意外终止。
宏
#define 定义宏
#define允许我们将一个文本替换另一个文本,也允许我们将参数替换到文本当中,这种实现通常称之为宏
#define 替换 被替换
例如:
#define type int
#define num 100
int main()
{
int a = 100;
type b = num;
printf("%d %d\n", a, b);
}
它们运行后是一样的,宏的作用就是将一个参数或者标识符在预处理阶段就可以替换成我们想要的名字。
宏也可以当作函数使用
声明方法:
#define name( parament-list ) stuff
注意:参数列表的左括号必须与name紧邻。如果两者之间存在空格,参数列表就会被解释为stuff的一部分
如果是对宏不太了解的人,可能会遇到下面这种情况
#define SQUARE( x ) x * x
int main()
{
int a = 5;
printf("%d\n", SQUARE(5));
printf("%d\n", SQUARE(5 + 1));
}
第一个答案是我们意料中的,但是第二个答案却让人感到奇怪,明明我们的参数是5 + 1,得到的应该是6的平方36,为什么会是这个奇怪的值呢?
之前我提到过,宏是单纯的替换,而非计算后再替换,所以替换后的公式如下
所以如果我们要使用宏函数,就必须要考虑到优先级问题,所以要在定义宏时加上括号
#define SQUARE( x ) ( x ) * ( x )
#define ADD(x) (x) + (x)
int main()
{
int a = 5;
printf("%d\n", 10 * ADD(a));
}
对于这个新的宏,我们按照之前的做法加上了括号,但是得到的答案还是错的,我们再次将宏替换进代码中
这个问题还是优先级的问题,所以我们需要再在外面加上一层括号
#define ADD(x) ((x) + (x))
总结:所以用于对数值表达式进行求值的宏定义都应该用这种方法加上括号,避免在使用宏时由于参数中的操作符或临近操作符之间不可预料的相互作用
#define的替换规则
在程序中拓展#define定义符号和宏时,需要涉及几个步骤
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
- 替换文本随后被插入到程序中与原来文本的位置。对于宏,参数名被它们的值替换
- 最后再次对结果文件进行扫描,看看是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意:
- 宏参数和#define定义中可以出现其他#define定义的变量。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索
宏与函数的对比
优点:
- 程序规模和速度更胜一筹
- 宏的参数与类型无关
缺点:
- 宏替换时会重复将代码插入程序,造成代码冗余。
- 宏无法进行调试
- 宏与类型无关,所以不够严谨
- 宏如果使用不当容易出现运算符优先级问题
条件编译
条件编译,顾名思义,就是满足条件才编译某些语句,不满足则不编译。
在我们编写代码的时候,可能存在一种情况,这段语句在某个环境下是需要的,在另一个环境下却又不需要,这个时候我们如果要在它不用的时候将它删除,就会十分麻烦,而使用条件编译就可以解决这个方法。
例如当我们一个项目中多次引用一个头文件,造成在最终程序中出现了多份这个头文件的内容,出席拿了文件内容重复的情况,这个时候我们就需要这个条件编译
#ifndef __TEST_H__
#define __TEST_H__
#endif
这就是一种条件编译
常见的条件编译如下:
1.
#if (常量表达式)
#endif
多个分支的条件编译
#if (常量表达式)
#elif (常量表达式)
#else
#endif
判断是否被定义
#if defined(x)
#ifdef x
#if !defined(x)
#ifndef x
嵌套指令
#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