大家好啊✨
先简单介绍一下自己
本人目前大二在读,专业是计算机科学与技术。
写博客的目的是督促自己记好每一章节的笔记,同时也希望结交更多同仁,大家互相监督,一起进步!☀️
在这篇文章中,将对程序环境、代码文件转换为可执行程序的过程、C++的函数重载以及它们之间的关系进行讲解。这可是大厂面试重点哦✨
我的宗旨就是将所有知识点一网打尽
内容有点多,大家一定要耐心看完。切记,学习如逆水行舟,不进则退.在学习的路上一定要坚持!坚持!再坚持!
如果大家看完觉得有所收获,不妨关注点赞收藏方便以后回顾,也当做给博主的小小鼓励了~❤️
文章目录
- 一、程序的翻译环境和执行环境
- 二、详解编译+链接
- 2.1翻译环境
- 2.2组成编译的几个阶段
- 2.3运行环境
- 三、解释为什么C++可以实现函数重载,C语言却不可以
- 3.1什么是函数重载
- 3.2C++和C语言的函数名修饰规则
- 四、总结
一、程序的翻译环境和执行环境
在ANSI C的任何一种实现中,存在两个不同的环境。
第1种是翻译环境,在这个环境中源代码被转换为可执行的机器指令。
第2种是执行环境,它用于实际执行代码。
本篇文章将重点对翻译环境中的各种细节进行讲解。
二、详解编译+链接
2.1翻译环境
我们知道,在一个工程里可能包含多个源文件,那计算机是怎么将这些文件整合起来最终形成一个可执行程序的呢❓
其实每一个源文件都会单独经过编译器的处理形成一个目标文件,然后这些目标文件会和链接库一起经过链接器的处理,最终形成一个可执行程序。
那么在编译器和链接器里到底进行了什么操作呢❓
下面来一一解释
首先我们先写一个简单的源文件
#define _CRT_SECURE_NO_WARNINGS#include int main(){int arr[10] = { 1,2,3,4,5,6,7,8,9,10 };int i = 0;for (i = 0; i < 10; i++){printf("%d ", arr[i]);}printf("\n");return 0;}
然后让这段代码执行起来,之后就会在代码路径下看到这么一个文件:
其实这个以obj为后缀名的文件就是上面所说的目标文件。
同样的,如果工程中含有多个源文件,则编译器处理之后,就会生成多个对应的.obj文件。
需要注意的是,在不同的编译器下,生成的目标文件的后缀名会不同,例如在Linux gcc编译器下会生成.o为后缀的目标文件(这篇文章中也会进行演示)
而所谓的可执行程序也就是以.exe为后缀的文件。
这里再补充一个链接库的作用:在我们写程序的时候,往往会包含一些头文件,而这些头文件所依赖的就是链接库,也就是说,有了链接库的支持,我们才能正常地使用头文件中的各种函数。
再补充一点,所谓的编译器和链接器就是分别为cl.exe和link.exe的文件(可执行程序),如下:
2.2组成编译的几个阶段
下面对源文件转换成可执行程序的过程进行分解:
源文件经过编译和链接生成可执行程序,而编译又分为预处理、编译和汇编。
由于在VS环境下,一旦我们开始执行程序,就会直接生成最终的可执行程序,所有我们没有办法观察到各个阶段到底发生了什么,因此在这里我们采用Linux来进行演示。
先给出一个在Linux环境下编写的一个C程序,代码如下:
- 预处理 选项 gcc -E test.c -o test.i
预处理完成之后就停下来,预处理之后产生的结果都放在test.i文件中。- 编译 选项 gcc -S test.c
编译完成之后就停下来,结果保存在test.s中。- 汇编 gcc -c test.c
汇编完成之后就停下来,结果保存在test.o中。
先执行第一步,生成一个test.i文件:
然后打开test.i:
可以看到,test.i文件中有长达几百行的代码,而在代码的最后是test.c里的内容。唯一不同的是,test.i文件中并没有出现”#include “,所以我们不难得出结论:test.i文件前面几百行代码其实就是头文件中的内容。—-故预处理过程的作用之一就是包含头(展开)文件
下面我们对test.c中的内容稍加修改:
在test.c文件中添加了一个宏、一个注释,并且使用了这个宏。
接下来,重新生成test.i文件:
根据上面出现的不同,可以得出,文件预处理阶段还会进行#define定义的值得替换,删掉定义的宏,并且删除注释。
接下来执行第二个命令,进入编译过程:
可以看到,执行-S选项后,在生成的test.s文件中出现了一堆汇编代码。
而产生这些汇编代码的过程又包括:语法分析,词法分析,语义分析和符号汇总。这里我们着重讲解符号汇总❗️❗️❗️
符号汇总主要就是将程序中出现的函数汇总在一起,最终生成一个.s文件,而这些.s文件又会在下一步的汇编过程中生成.o文件,如下:
而这里的test.o文件中存放的是一些我们看不懂的二进制符号,它其实是由汇编指令转化而来的。
而与此同时,前面已经进行过的符号汇总又会形成一个符号表,举个例子:
上面的代码在编译阶段会分别经过符号汇总,然后再汇编阶段形成自己的符号表:
下面就要进入链接阶段了,在这一阶段,主要进行了合并段表以及符号表的合并和重定位。这里重点讲符号表的合并和重定位。
顾名思义,符号表的合并就是将汇编阶段所产生的所有符号表合并为一个,而重定位就是把所有相同的符号合并为一个,并使用一个地址。例如上述add.o和test.o中的Add就将被合并为一个。而又因为test.c中的Add只是一个声明,并没有什么价值,所以,合并后的Add所使用的地址就是地址一。
总结一下上面四个过程:
经过上面的四个阶段,就生成了可执行程序。
2.3运行环境
程序执行的过程:
- 程序必须载入内存中。在有操作系统的环境中:一般这个由操作系统完成。在独立的环境中,程序
的载入必须由手工安排,也可能是通过可执行代码置入只读内存来完成。- 程序的执行便开始。接着便调用main函数。
- 开始执行程序代码。这个时候程序将使用一个运行时堆栈(stack),存储函数的局部变量和返回
地址。程序同时也可以使用静态(static)内存,存储于静态内存中的变量在程序的整个执行过程一直保留他们的值。- 终止程序。正常终止main函数;也有可能是意外终止。
三、解释为什么C++可以实现函数重载,C语言却不可以
3.1什么是函数重载
自然语言中,一个词可以有多重含义,人们可以通过上下文来判断该词真实的含义,即该词被重载了。
比如:以前有一个笑话,国有两个体育项目大家根本不用看,也不用担心。一个是乒乓球,一个是男足。前
者是“谁也赢不了!”,后者是“谁也赢不了!”
函数重载:是函数的一种特殊情况,C++允许在同一作用域中声明几个功能类似的同名函数,这些同名函数的形参列表(参数个数 或 类型 或 顺序)必须不同,常用来处理实现功能类似数据类型不同的问题
下面的几个函数构成重载
int Add(int left, int right) {return left+right;}double Add(double left, double right){ return left+right; }long Add(long left, long right){ return left+right; }int main(){ Add(10, 20); Add(10.0, 20.0); Add(10L, 20L); return 0;}
但这两个并不构成重载:
short Add(short left, short right) { return left+right; }int Add(short left, short right) { return left+right;}
3.2C++和C语言的函数名修饰规则
根据上文,我们可以知道编译过程中要进行符号汇总,链接过程中要进行符号表的合并,接下来分别看一下Linux下gcc和g++中的函数名修饰规则:
C语言代码如下:
执行-c选项结果如下:
很显然,C语言并不能出现函数名相同的函数。
具体原因通过汇编代码了解一下:
在这之前要重新写一个.c文件:
执行-S选项产生的汇编代码如下:
可以看到gcc对C语言处理得到的汇编中,函数名最后生成的(修饰过的)名字就是他们本身,所以当出现相同名字的函数时,编译器无法区分,故而报错。
下面再来看一下C++中的情况:
代码如下:
执行-S选项后结果如下:
可以看到,两个相同名字的函数经过修饰后得到的函数名并不相同,这里以第一个为例解释命名规则:
_Z3Addii
_Z是名字前面固定不变的前缀
3代表函数名的字符长度(Add为3个字符)
数字后面紧跟着的就是函数名字Add
最后的两个i是两个参数类型的简写
所以只要C++中的函数符合函数重载的要求,就不会报错。
但因为函数名修饰规则不同,C语言中的修饰规则过于单一简单,故不能区别函数名相同的函数,也就不能构成函数重载。
四、总结
本篇博客主要介绍了程序环境和预处理各个过程中进行的操作,以及C语言和C++中函数名修饰规则,还进行了C++可以实现函数重载的原理分析。这些都是大厂面试的重点!
当然这些过程并没有进行最底层的分析,因为最底层原理内容远比想象的更多,又想进一步了解的小伙伴可以看一下《程序员的自我修养》这本书,读完一定受益匪浅。但如果面试室被问到,只要把这篇文章中的内容讲出来就已经能得到面试官的认可了!
不得不说,写这么一篇博客还是挺累的,朋友们看完如果感觉有收获的话,就点点赞和收藏给博主一点小鼓励吧!
博主也在学习中,如有不严谨之处,希望各位不吝指出,我们一起加油,一起进步!