大家好我是小锋我们来开始新一节的内容——程序环境和预处理。
我们知道我们创建的.c文件经过编译链接变成可执行程序那么我们写的.c的文本文件怎么变成可执行程序?它的过程是什么?大家别急我为大家细细说明
程序的翻译环境和执行环境
在ANSI C的任何一种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境(运行环境),它用于实际执行代码。
我们的源文件在运行之前会经过翻译环境变成以.exe为后缀的可执行程序进入运行环境
那翻译环境中到底有哪些操作呢?
详解编译+链接
组成一个程序的每个源文件通过编译过程分别转换成目标代码(object code)。 每个目标文件由链接器(linker)捆绑在一起,形成一个单一而完整的可执行程序。 链接器同时也会引入标准C函数库中任何被该程序所用到的函数,而且它可以搜索程序员个人 的程序库,将其需要的函数也链接到程序中。
我们知道一个工程会有多个源文件当它们进入翻译环境后会单独的进入编译器,转换成后缀为.obj的目标文件,然后目标文件和链接库中的内容一起进入链接器形成单一完整的可执行程序。
接下来我用gcc编译器为大家演示翻译环境的每个过程
vs code的下载使用教程可以在b站上学习
大家看现象就行了
预编译
我们在test.c文件中敲下这样一段代码
在终端窗口输入 gcc -E test.c -o test.i
它就会生成一个test.i的文件而这个文件中的代码就是预处理的代码了
我们看见红色框中的代码有很多,这里其实在把#include<stdio.h>中的内容包含到test.c中来。
我们修改一下代码在测试一下
开始测试
大家对比一下是不是发现我们的注释没有了,还有我们的#define替换也没有了。
总结
在预处理是我们主要进行
1,头文件的包含
2,#define定义的符号的替换
3,注释的删除
我们发现预编译进行的都是文本操作,都是于预处理指令相关的操作
编译
我们在终端进行如下操作
. 编译 选项 gcc -S test.c
编译完成之后就停下来,结果保存在test.s中。
test.s的内容
这里都是汇编代码
有此我们可以知道
总结
在进行编译操作主要进行
1,词法分析
2,语法分析
3,语义分析
4,符号汇总
5,并且将源代码转换为汇编代码
汇编
我们在终端进行如下操作
汇编 gcc -c test.c
汇编完成之后就停下来,结果保存在test.o中。
我们生成了一个test.o的文件这个文件就是目标文件,这时有点朋友就要问了目标文件的后缀不是.obj吗?
其实在vs中的目标文件后缀是.obj
gcc中的目标文件后缀是.o
那test.o文件中的是什么呢?
这里面是二进制文件。
总结
汇编操作是
把汇编代码转换成二进制指令
并且形成符号表
链接
我们知道链接是将多个目标文件一起转换为可执行程序
我们接着操作
gcc test.o
我们看他这里是不是生成了.exe的文件了?
我也可以执行这个程序
. \test.exe
是不是就执行了这个程序了
总结
所以我们链接要做的事情是
1,合并段表
2,符号表的汇总和重定位
那什么是合并段表呢?
我们用图来说话
所以合并段表就是将多个目标文件中相同的段以相同的格式合并在一起
段表实际上是记录了该文件中所有段的偏移位置和属性,存储的是像代码段 数据段等的位置偏移或者属性
符号表
我们经过观察不难看出编译,汇编,链接都提到了符号表
那这个操作具体是什么呢?
我们还是用图说话
从图中我们可以看出符号表的变化过程
那图中的链接库是什么呢?
接下来我们从宏观的角度解读一下c语言
运行环境
程序执行的过程:
1. 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序 的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。
2. 程序的执行便开始。接着便调用main函数。
3. 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回 地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程 一直保留他们的值。
4. 终止程序。正常终止main函数;也有可能是意外终止。
预处理详解
预定义符号
这些预定义符号都是语言内置好的可以直接拿来用
# include<stdio.h>
int main() {
int a[10] = { 1,2,3,4,5,6,7,8,9,10 };
for (int i = 0; i < 10; i++) {
printf("%d %s %s %s %d\n", a[i], __FILE__, __DATE__, __TIME__, __LINE__);
}
return 0;
}
#define定义的标识符
我们来举些例子
这里我们用gcc来观察预处理之后的现象
我们可以看到语句也可以替换,
(注)
在define定义标识符的时候,尽量不要在最后加上 ;
因为在替换时;也会替换
#define 定义宏
大家看,参数也替换进去了这样就类似一个函数了
但是这这样有可能会出现问题
#define ADD(x,y) x*y
int main() {
int x = 3, y = 4;
int c = ADD(x+1, y+1);
printf("%d", c);
return 0;
}
大家觉得这段代码输出结果时多少?
我们运行试试
怎么会这样呢,我们预期不是应该等于20吗?
我们在gcc中预处理看看
原来它是这样调换的;
我们要怎么避免这种情况呢?
加了括号后是不是明了了许多。
所以宏定义时应多用括号。
#define 替换规则
#和##
int main() {
int add = 0, bdd = 1, cdd = 2, daa = 3;
printf("the value is add %d\n", add);
printf("the value is bdd %d\n", bdd);
printf("the value is cdd %d\n", cdd);
printf("the value is daa %d\n", daa);
return 0;
}
#define PRINT(a) printf("the value is "#a" %d\n", a)
int main() {
int add = 0, bdd = 1, cdd = 2, daa = 3;
PRINT(add);
PRINT(bdd);
PRINT(cdd);
PRINT(daa);
return 0;
}
宏和函数对比
我们来看看这段代码
#define ATT(x,y) ((x>y)?(x):(y))
int main() {
int a = 3, b = 9;
int c = ATT(a, b);
printf("%d", c);
return 0;
}
这里我们定义了一个宏用它来比较大小
那同学们我们这里能不能用函数来实现?
int att(int x, int y) {
return x > y ? x : y;
}
int main() {
int a = 3, b = 9;
int c = att(a, b);
printf("%d", c);
return 0;
}
那这里我们为什么用宏定义而不用函数?
# define ADD(mon,arr) (arr*)malloc(mon*sizeof(arr))
int main() {
int a = 0;
int *ps=ADD(10, int);
for (int i = 0; i < 10; i++) {
*(ps+i) = i;
}
for (int j = 0; j < 10; j++) {
printf("%d ", *(ps + j));
}
return 0;
}
命名约定
#undef
命令行定义
条件编译
int main() {
int a = 1;
#if 1//如果为真那么执行为假不执行
a = 0;
printf("haha");
#endif
if (a) {
printf("hehe");
}
return 0;
}
int main() {
#if 0
a = 0;
printf("haha");
#elif 2<1
printf("wawa");
#elif 5
printf("wawa");
#else
printf("heihei");
#endif
return 0;
}
# define MAR 0
int main() {
#if defined(MAR)
printf("hehe");
#endif
return 0;
}