程序的编译与链接
本章内容如下:
1:程序的翻译环境与执行环境的介绍
2:详解程序的翻译环境(编译+链接)
- 2.1预处理阶段干了啥
- 2.2编译阶段干了啥
- 2.3汇编阶段干了啥
- 2.4链接阶段干了啥
3:预处理详解
- 预定义符号的介绍
- #define 的介绍(宏与标识符号)
- #与##的介绍
- 宏与函数的对比
- #undef的介绍
4:条件编译
5:#include文件
- #include<>与#include""的区别
- 条件编译在头文件包含中的使用场景介绍
以上就是本文章所要介绍的大概内容,下面让我们一起来学习以上的知识点吧!
1:程序的翻译环境与执行环境
首先我们知道计算机是只能识别二进制的文件的,而我们平常所写的c语言文件并不能够被计算机所直接识别,所以才有了程序的翻译环境
所以简单的来说:
翻译环境:是将文本文件(c语言写的源代码文件)转化为计算
机所能识别的二进制文件。
执行环境:用于执行实际代码的环境。
关于执行环境的内容
1:首先每个可执行程序都必须先加载到内存当中去,这个步骤在有操作系统的环境中,由操作系统来完成的。
2:程序的执行便开始了。开始调用main函数
3:开始执行程序的代码,开辟对应的函数栈桢,用来存放局部变量和返回地址,同时也可以使用静态的内存,用来存储静态变量的值和地址
4:程序的终止。这个终止可以包括意外的终止和正常终止main函数。
2:详解程序的翻译环境(编译+链接)
首先我们需要了解的就是翻译环境包括两个部分,编译+链接。
而编译又包括3个阶段。
1:预处理阶段
2:编译阶段
3:汇编阶段
我们在vs2019这样的集成开发环境下并不能够区别这三个阶段到底干了啥事,所以我们采用在linux环境下来介绍。
首先我们先来介绍在linux环境下的三条指令:
1: gcc -E 要编译的文件 -o 生成的文件名
意思为:当编译器编译到预处理阶段完成后就停止对程序的编译,也就
是只完成编译的预处理阶段。
2:gcc -S 要编译的文件 -o 生成的文件名
意思为:当编译器编译到编译阶段完成后就停止对程序的编译,也就
是只完成编译的编译阶段。
3:gcc -c(小写) 要编译的文件 -o 生成的文件名
意思为:当编译器编译到汇编阶段完成后就停止对程序的编译,也就
是只完成编译的汇编阶段。
首先我们先写一段代码,然后在linux系统下看看这段代码在不同的阶段完成了什么事情。代码如下:
#include<stdio.h>
//定义一个宏常量
#define M 3
#define N 2
int main()
{
//main函数体
int a =2;
int b =2;
int c =a+b+M;
printf("%d\n",c);
printf("%d\n",N);
return 0;
}
我们能够看到在预处理阶段完成后的文件中,代码量明显就增多了
从原本的10多行到现在的800多行,并且在源文件中本来有的注释
和定义的宏,在预处理阶段完成后,都消失了。
这也从侧面告诉了我们预处理阶段会干的事情如下:
预处理阶段会干的事情
1.#include头文件的展开
2. 预定义符号的替换
3. 去除注释
注意:所有预定义符号的替换都是在预处理阶段完成的。
然后我们在来观察在编译阶段会做的事情
上图就是我们经过编译阶段所形成的test.s这个文件,我们发现里面并不是我们能够读懂的c语言代码了,而变成了汇编指令了。
所以在我们的编译阶段会进行如下的过程
1.语法分析
2.语义分析
3.词法分析
4.符号汇总
编译目的:将c语言代码转化为汇编代码,并且进行符号汇总。
我们再来看一看汇编阶段到底干了啥事>
我们发现经过汇编所形成的文件后,文件我们就看不懂了,因为此时的文件是二进制文件。
汇编阶段会干的事情
1.形成符号表
2.将汇编代码转化为计算机能够识别的二进制代码
其实在linux系统下,gcc所产生的目标.o文件,可执行程序,
它的文件格式为ELF这种类型的文件格式来进行存储的,
而在linux环境下,可以用readelf命令来读取这样的文件。
关于编译阶段符号汇总与汇编阶段形成符号表的意思
符号汇总,其实本质上来说就是统计文件中所使用的函数名,这些符号
而形成符号表的意思我们可以简单的理解为:将函数与它的地址看作整
体。在链接的时候在使用这张表
如下图的意思
这三个步骤的完成就标志着我们翻译环境的编译过程就完成了。
链接阶段:
gcc 源文件 -o 形成文件名
本质上来说链接阶段就是形成可执行程序的最后一步。
链接阶段所干的事情:
1.合并段表
2.符号表的合并与重定位
段表的意思为:我们的每个目标文件都是按照elf的文件格式进
行排版的,而elf会将文件划分为很多段,每一段执行不同的功
能,而我们在一个项目中可以有很多个源文件,每一个源文件生
成的目标文件都会按照这种格式进行分段,所以在链接的时候我
们将具有相同功能的代码,放在同一个段表内。
符号表的合并也是将不同源文件所包含的同一种符号进行合并,
重定位表示的是给符号确定正确的地址。
对于符号表,段表的理解可能不是什么非常清楚,大家可以去看<编译原理这本书>
3:预处理阶段详解
1.预定义符号的介绍
预定义符号以下的几个都是语言内置的
__FILE__ //进行编译的文件
__LINE__ //文件输出这个当前的行号
__DATE__ //文件被编译的日期
__TIME__ 文件被编译的时间
2.#define定义的标识符与宏
#define定义的标识符语法 #define name 内容 (无;)
#define M 3 //在预处理阶段只要程序中有M 就替换为3
#define DOU double// DOU-->double
#define reg register// reg--->register
#define定义的宏
#define的一个规定,可以将参数替换到文本中去,这种实现就叫做宏。
我们通过讲解2个宏来掌握宏,并且了解一些注意事项。
#define ADD(x,y) ((x)+(y))
我们通过图片来讲解上面的宏
3.#与##的作用
#:可以将宏的参数化为字符串
##:可以将宏参数进行合并
直接讲语法可能有点抽象,我们通过具体的代码来进行讲解
#define PRINT(N,formate) printf("the value of
"#N" is "formate"\n",N)
int main()
{
/*printf("hello ""world\n");
printf("hello world\n");*/ //这两种情况是一样的
int a = 10;
int b = 15;
PRINT(a,"%d");
PRINT(b,"%d");
float c = 3.14f;
PRINT(c, "%f");
return 0;
}
//##在宏中的作用是将两个常数符号连接起来
#define CAT(x,y) x##y
int main()
{
int c110 = 2024;
int a = CAT(c, 110);
printf("%d\n", a);
return 0;
}
4.宏与函数的区别:
- 宏是直接对代码块进行替换的,函数则需要去掉用相应的函数。
- 宏不能进行调试,函数可以。
- 宏不能进行递归
- 宏没有函数参数类型的检查,可能比较危险
- 代码长度宏需要进行替换,所以长度大于函数的长度
- 运行效率 宏>函数
- 宏替换时,可能会涉及到操作符的优先级的问题
其次在好的编程习惯来讲,宏名一般全是大写,而函数不需要
5:#undef
作用:当我们在以后的代码中不需要在使用对应的宏的时候,我们可以用#undef 来去除对应的宏
命令行定义:我们可以在编译的过程中定义符号,
命令为 : gcc 编译文件 -D 符号定义
5:条件编译
在编译一个程序的时候我们需要放弃一条语句或者一组语句是非常容易
的,因为我们有条件编译指令
常见的条件编译指令有以下一些:
如:
1.单分支
#if 常量表达式
....
#endif
2.多分支
#if 常量表达式
........
#elif 常量表达式
......
#else
......
#endif
这两种条件编译的指令当表达式为真时就会保留对应的代码,比如说:
#define x 10
int main()
{
#if x==10
printf("haha\n");
#elif x==2
printf("hehe\n");
#else
printf("heihei\n");
#endif
return 0;
}
我们在linux系统的环境下进行查看。
应为我们定义了x=10,所以保留了haha
如果定义了符号则保留代码
#ifdef symbol
......
#endif
//如果没有定义则保留代码
#ifndef symbol
....
#endif
这个语句是看我们定义了符号没,只要定义了就保留,我们在linux
下看一看
5#include文件的包含
1.#include<> 与#include“”文件的区别
#include<>,一般来说这是包含库里面的文件,而""是包
含我们自己所写的头文件。
这两者的不同存在与:#include<>会直接去指定的标准路径
下去进行查找。
而#include“”会先在与源文件相同的路径下查找,如果没有
找到则会想寻找库函数头文件的方式到标准的指定文件下去
进行查找。
- 条件编译在头文件包含中的使用场景介绍
我们知道当我们有许多源文件和头文件的时候,因为我们会包含对应的头文件,可能会导致在一个文件中可能会出现引入多个相同的头文件,而在预处理阶段头文件又会进行展开,导致代码的长度会持续的增加,所以这时候就需要我们的条件编译出场了
#ifndef __TEST.H__
#define __TEST.H__
#endif
或者
#pragma once
这样就可以避免头文件的多次包含了。
到这里本章就结束了,感谢大家的观看。