文章目录
- 前言
- Ⅰ 程序的翻译环境
- 1. 编译的过程
- 2. 链接的过程
- Ⅱ 程序的执行环境
- Ⅲ 预定义符号
- Ⅳ 预处理指令 #define
- 1. #define 定义标识符
- 2. #define 定义宏
- 3. #define 替换规则
- Ⅴ 预处理操作符 # 和
- 1. # 操作符
- 2. ## 操作符
- Ⅵ 宏和函数的对比
- Ⅶ 预处理指令 #undef
- Ⅷ 条件编译
- 1. 单分支条件编译
- 2. 多分支条件编译
- 3. 判断符号是否被定义
- Ⅸ 文件包含
- 1. 本地文件包含
- 2. 库文件包含
前言
在标准 C 语言的任何一种实现中,存在两个不同的环境:
- 翻译环境:将源代码翻译为可执行的机器指令。
- 执行环境:用于实际执行代码。
Ⅰ 程序的翻译环境
- 当一个 .c 文件,最终要翻译成 .exe 文件时,需经过 翻译 + 运行两个环境。
- 而翻译环境也需要经过两个过程:编译 + 链接。
可执行程序的生成过程
- 组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。
- 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。
- 链接器同时也会引入标准 C 函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人的程序库,将其需要的函数也链接到程序中。
不同环境下生成的目标文件
- windows:生成的目标文件是 xxx.obj
- Linux:生成的目标文件是 xxx.o
1. 编译的过程
编译分为 3 个过程:预编译 (预处理) → 编译 → 汇编
1. 预编译 (预处理)
在预处理完成后,会生成一个 xxx.i 文件。该文件会完成以下操作
- 注释的替换,将注释替换成空格。
- 头文件的包含,#include <>
- #define 符号的替换。
注:# 开头的都被称为预处理指令。所有的预处理指令都在预处理阶段就被处理掉。
2. 编译
将 C 语言代码翻译成汇编代码。执行过程如下
- 词法分析
- 语法分析
- 语义分析
- 符号汇总
3. 汇编
计算机无法看懂在编译阶段后翻译出的汇编代码,此时就需要使用汇编器将汇编代码翻译成二进制指令。生成了 .o 文件(目标文件)。
2. 链接的过程
- 链接目标文件和链接库生成可执行程序 (二进制的程序)。
- 合并段表:将目标文件相同数据的段落进行合并。
- 符号表的合并和重定位
Ⅱ 程序的执行环境
程序运行的过程
- 程序载入内存。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
- 程序的执行开始。调用 main 函数。
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。
- 终止程序。正常终止main函数;也有可能是意外终止。
Ⅲ 预定义符号
- C 语言本身就预定义好的符号,这些符号可以被直接使用
符号名 | 符号功能 |
---|---|
__FILE__ | 显示进行编译的源文件 |
__LINE__ | 显示文件当前的行号 |
__DATE__ | 显示文件被编译的日期 |
__TIME__ | 显示文件被编译的时间 |
__STDC__ | 如果编译器遵循 C 标准,其值为1,否则未定义 |
__FUNCTION__ | 显示正在编译的是哪个函数 |
使用实例
- 显示文件在编译过程中的文件、行号、日期、时间。
Ⅳ 预处理指令 #define
#define 的功能
- 定义标识符
- 定义宏
1. #define 定义标识符
语法形式
#define 名字 内容
举个例子
#define MAX 100 //MAX 就是标识符的名字,100 就标识符的内容
#define 的实现过程
- #define 定义的标识符在预处理过程中,执行的是替换操作。不进行运算操作。
因为 #define 实现的是替换操作,所以在使用 #define 定义标识符时才不能出现分号。
#define MAX 100; //如果出现分号
printf("%d\n",MAX); //将 MAX 替换之后的结果就成了 printf("%d\n",100;);
2. #define 定义宏
#define 允许把参数替换到文本中,这种实现通常称为宏或定义宏。
宏的声明方式
#define 名字(参数列表) 内容
- 其中的参数列表是一个由逗号隔开的符号表,它们可能出现在内容中。
举个例子
- 利用宏来实现乘法
#define MUL(x,y) x * y
//1.将 2 + 3 和 4 + 5 传给 x y 成了 #define MUL(2 + 3,4 + 5)
//2.然后将替换后的 x y 传进内容中 #define MUL(2 + 3,4 + 5) 2 + 3 * 4 + 5
int main()
{
printf("%d\n", MUL(2 + 3, 4 + 5));
//3.将 MUL(2 + 3,4 + 5) 替换成 2 + 3 * 4 + 5
//结果就是 printf("%d\n",2 + 3 * 4 + 5);
return 0;
}
定义宏时要舍得加括号
- 因为宏实现的是替换值,所以宏在实现的过程中因为优先级的问题导致会很容易出现错误,因此在定义宏的时候,不要舍不得加括号。
3. #define 替换规则
在程序中扩展 #define 定义符号和宏时,需要涉及 3 个步骤:
- 在调用宏时,首先对参数进行检查,看看是否包含任何由#define定义的符号。如果是,它们首先被替换。
- 替换文本随后被插入到程序中原来文本的位置。对于宏,参数名被他们的值所替换。
- 最后,再次对结果文件进行扫描,看看它是否包含任何由#define定义的符号。如果是,就重复上述处理过程。
注意事项
- 宏参数和#define 定义中可以出现其他#define定义的符号。但是对于宏,不能出现递归。
- 当预处理器搜索#define定义的符号的时候,字符串常量的内容并不被搜索。
Ⅴ 预处理操作符 # 和
1. # 操作符
功能
- 将一个宏的参数以字符串的形式插入到字符串中。
举个例子
2. ## 操作符
功能
- 将位于它两边的符号合成一个符号,它允许宏定义从分离的文本段创建标识符。
举个例子
Ⅵ 宏和函数的对比
宏的优点
- 用于调用函数和从函数返回的代码可能比实际执行这个小型计算工作所需要的时间更多。所以宏比函数在程序的规模和速度方面更胜一筹。
- 函数的参数必须声明为特定的类型。所以函数只能在类型合适的表达式上使用。反之这个宏怎可以适用于整形、长整型、浮点型等可以用 > 来比较的类型。宏是类型无关的。
宏的缺点
- 每次使用宏的时候,一份宏定义的代码将插入到程序中。除非宏比较短,否则可能大幅度增加程序的长度。
- 宏无法调试。
- 宏由于类型无关,也就不够严谨。
- 宏可能会带来运算符优先级的问题,导致程序容易出现错。
宏和函数的对比
属性 | #define 定义宏 | 函数 | 优势方 |
---|---|---|---|
代码长度 | 每次使用时,宏代码都会被插入到程序中。除了非常小的宏之外,程序的长度会大幅度增长 | 函数代码只出现于一个地方;每次使用这个函数时,都调用那个地方的同一份代码 | 函数 |
执行速度 | 更快 | 存在函数䣌调用和返回的额外开销,相对慢一些 | 宏 |
操作符优先级 | 宏参数的求值实在所有周围表达时的上下文环境里,除非加上括号,非则邻近操作符的优先级可能会产生不可预料的后果,建议宏在书写时加多括号 | 函数参数只在函数调用的时候求值一次,将它的结果值传给函数,表达式的求值结果更容易预测 | 函数 |
带有副作用的参数 | 参数可能被替换到宏体中的多个位置,所以带有副作用的参数值可能会产生不可预料的结果 | 函数参数只在传参的时候求值一次,结果更容易控制 | 函数 |
参数类型 | 宏的参数有与类型无关,只要对参数的操作时合法的,它就可以用于任何参数类型 | 函数的参数类型与类型有关,如果参数的类型不同,就需要不同的函数,即使它们执行的任务时不同的 | 宏 |
调试 | 宏不方便调试 | 函数可以逐语句调试 | 函数 |
递归 | 宏不能递归 | 函数可以递归 | 函数 |
如果选择函数 / 宏
- 如果逻辑比较简单,可以使用宏来实现。
- 如果计算逻辑比较复杂,就要使用函数。
Ⅶ 预处理指令 #undef
- 移除一个宏定义
举个例子
Ⅷ 条件编译
- 在编译一个程序的时候将一条语句 (一组语句) 编译或放弃。
条件编译
- 满足编译条件才允许执行编译。
以下为常见的条件编译指令
1. 单分支条件编译
语法格式
#if 常量表达式
//代码
#endif
- 表达式结果如果为真,中间的代码参与编译,反之不参与编译
举个例子
2. 多分支条件编译
语法格式
#if 常量表达式
//...
#elif 常量表达式
//...
#else
//...
#endif
举个例子
3. 判断符号是否被定义
符号已定义则参与编译
#if defined(symbol)
#ifdef symbol
符号未定义则参与编译
#if !defined(symbol)
#ifndef symbol
举个例子
int main()
{
//以下为定义了符号才会去编译的条件编译指令
#if defined(M)
printf("world hello!\n");
#endif
#ifdef M
printf("world hello!\n");
#endif
//以下为未定义符号才回去编译的条件编译指令
#if !defined(M) //未定义 M 才编译代码
printf("hello world!\n");
#endif
#ifndef M
printf("hello world!\n");
#endif
return 0;
}
Ⅸ 文件包含
1. 本地文件包含
包含的是自己的 .h 文件
语法格式
#include "xxx.h"
查找策略
- 现在源文件所在目录下查找,如果该头文件未被找到,编译器则去标准位置查找头文件。
- 如果找不到则提示编译错误。
举个例子
#include "test.h"
//寻找 test.h 文件
//1.先在当前的 .c 文件所在的文件夹下寻找
//2.如果没有找到,则取标注库文件夹下寻找
//3.如果在这两个地方都找不到则报错
2. 库文件包含
包含的是标准库的头文件
语法格式
#include <xxx.h>
- 查找头文件直接去标准路径下去查找,如果找不到就提示编译错误.