(仅供自我学习用)C++学习记录-2

        本节课为cherno的第六课学习记录。

        本节课进一步研究了编译器是如何运行的,以及在不同情况下会有什么编译结果。应该是以GCC编译的四个步骤——预处理,编译,汇编,链接。这个顺序来讲解的。本课讲前两个步骤。(我怀疑这部分内容的深入学习要等到学习另外一些课程之后才行,这里教授的知识还比较表层,我以此总结出的知识点也有很浓的照搬意味。希望在以后可以有机会进一步了解编译和链接。或许在Effective C ++等书里会有更详细的解释,或许在我不怎么积极阅读的CPP里有解释,挖个坑)


目录

        1. 文件对于C++编译器来说没有意义。

        2. 一个cpp文件也被称为一个翻译单元。

        3. 每个obj文件的大小与其代码内容多少有关,注意引入头文件。

        4.预处理器的认识

        5.浏览.obj文件

         (1)阅读asm文件

         (2)加深对编译器优化的认识


        首先,上节课指出了编译器可以将工程里的每个源文件都转化为cpp格式,然后交给链接器使他们链接成为一个exe。然后以下为这节课学习的笔记:

        1. 文件对于C++编译器来说没有意义

        编译器只是看见什么文件类型名就会进行相应的操作。.cpp——转换为obj文件;.h——以头文件方式处理;.c——以c语言文件处理。(事实上,我还是并不太懂什么叫做文件对于C++编译器来说没有意义,或许是指编译器不会看文件内容,而是根据文件类型名就进行相应的操作?)

        2. 一个cpp文件也被称为一个翻译单元

        而由一组包含关系的cpp文件组合而成的也是一个翻译单元。通常一个翻译单元会生成一个目标文件,即obj文件。

        3. 每个obj文件的大小与其代码内容多少有关,注意引入头文件。

        这里要知道的是,当文件里引入了头文件(比如#include <iostream>),头文件本身内部的所有代码也会被预处理,然后被一股脑放进obj文件里面,很有可能导致obj文件容量迅速增大。

        比如一个小实验来证明include引入头文件会增大内存:在上一次的HelloWorld的工程中,新建一个源文件Math.cpp,负责两个数相乘的功能。

可以看见编译后的obj文件很小

        而当我们将一个头文件引进去,比如#include <iostream>; 就会看见内存迅速增大:

        4.预处理器的认识

        编译的第一个阶段就是预处理,预处理器会将一系列头文件和宏定义进行处理;例如从上一个实验中认识到了如何预处理include头文件的(下一个实验还会进一步介绍include)。【*延伸拓展:编译的四个阶段】

        常见的预处理语句有:include, if...endif, define, pragma 等等。百度百科对预处理的解释是:“一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程”。

        那么预处理会对程序产生什么影响呢?接下来通过对include, define, if...endif几个语句的使用,来进一步认识。

(1)include实验

        本实验紧接着上一个Math.cpp。首先,在上一次的HelloWorld的工程中,新建一个右括号头文件Endbrace.h。内容很简单,只有一个右花括号。

         然后回到main函数里,将函数最后的右花括号改为#include语句:

         (*头文件用""和<>括起来的作用和目的是不同的)最后一行通过Endbrace.h成功替代了字符“ } ”。

        为了更清楚地了解预处理器在这个过程中进行了哪些操作,我们还可以将工程属性——预处理器——“预处理到文件”改为“是”:

        这样当我们再次编译Math.cpp,文件夹里就会产生一个名为Math.i 的文件——即预处理后的cpp代码。打开后内容如下:

        既然include将头文件Endbrace.h可以原封不动搬过来。那么当引入iostream会发生什么?(毕竟由前面实验可以看出有iostream头文件的obj文件会迅速增大内存;其实可以想到,这里打开.i文件后,编译器会把iostream的所有代码预处理出来)

总结:#include 是一个预处理指令,作用是寻找指令后面<>或“”中的文件名,并把这个文件的内容包含到当前的文件中。

 (2)define实验

         继续对Math.cpp文件进行研究。define可以把某个字符a替换成另一个字符b,但在预处理时会把另一个字符b替换回a。例子如下:

(3)#if #endif 实验

        #if #endif 语句可以用于系统优化。其简单使用的方法如下:

 #if 0时,代码直接省略编译了。

        5.浏览.obj文件

        在了解了GCC编译的第一步——预处理之后,来到了第二步——编译(.i——.s)。

        (1)阅读asm文件

        如果直接浏览Math.obj,那么就会得到满页的二进制代码,也就是编译后转换成的机器代码。

        

        (这里Notepad需要安装一个Hex-Editor的插件,才能够阅读二进制代码)

        为了我们自身阅读方便,我们需要在工程属性里再做如下调整。然后再次编译,得到文件Math.asm:

         打开文件夹中出现的Math.asm,就可以获得可阅读的汇编代码(如下)。在代码里,可以找到math函数,变量a、b、ret。

        阅读底部一段代码,解释如下:mov指令将a的值存入eax寄存器中,imul指令再实现a与b的值相乘,将值传给ret,最终再用mov指令将ret的值传给eax寄存器。

        从以上这段阅读,其实会看出:将乘数的值传给eax,eax又给ret变量,最终ret变量又把值传给eax——设置ret变量实际上是重复多余的。所以我们可以对代码进行优化:

         汇编代码也更加简洁。或许以后可以通过阅读汇编代码来优化设计?(还是感觉专门阅读汇编代码会很麻烦)

         (2)加深对编译器优化的认识

         为了让编译器的汇编代码更简单,让我们再对工程属性做出如下更改:

 

 

 进行编译,得到的部分编译代码会更加简洁,如下。

 

         很迷惑,我的代码文件里直接连a和b都找不到了,也不知道最大速度下编译器做了什么超级优化(毕竟我还不太会阅读汇编代码)。我再把视频中得到的asm代码也放在了右边。(很遗憾我并不能发现在这一份代码下,最大速度前后编译器做了哪些优化。可能是因为vs版本不同,优化方式也不一样,以后对汇编语言具体了解之后再回来看这份数据吧。)

        其实本课的另两个代码可以更确切地反映编译器的优化:

        part1.

        将 a * b 改为 5 * 2,编译后观察汇编代码:

         可以发现编译器直接将乘数结果10传给eax了。对于常数之间的运算,直接传送最终结果,这叫做“常数折叠”。(即:本实验介绍了一种编译器的最佳化技术)

         part2.

        再调查编译器优化的另一种方式。我们新建一个函数Log,负责返回接收信息:

         首先在取消最大优化的情况下编译一次。可以看见asm文件里出现了对于Log函数的调用:

         然后我们再将最大优化打开。再次编译后,打开asm文件:

         因为调用Log函数对Math.cpp整体并没有影响,Log函数的返回值甚至不要求保存,所以会编译器在最大优化时直接将Log函数给忽略了。——这就是编译器优化的表现之一。


        编译器的大致功能介绍完啦!学下来感觉学到了很多,但是也发现自己有很多需要学习的地方——汇编语言,编译的四个步骤及其内容,计算机原理......

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值