https://zhuanlan.zhihu.com/p/32509289
花了一年的时间,终于把这本书读完;中间有几次间断,第一次间断是去学习流媒体技术,第二次间断是去学习网络编程,估计花费的时间总计是半年。这本书一共450多页,半年未6个月,24周,平均每周大概20页,感觉速度太慢。但终究是读完,多少有一点成就感,更为重要的是读完这本书让我的编程境界有质一样的提升,也许我已经从一只大虾编程牛人,这本书功不可没,c/c++程序员必读的圣经,良心推荐。 在这里我还想啰嗦一下关于学习方法的问题,以前我在学习《卓有成效的管理者》时,一边看书,一边做笔记,当然还在一边思考如何解决实践中的问题,感觉这样的方法学习效率很高,能够集中注意力,能够加深印象和更好地实践。读这本书的时候,我差不多也使用这种学习方法,可是对学习效率不大满意;我仔细分析其中的原因:《卓有成效的管理者》是理论性学科,而《程序员的自我修养---链接、装载与库》是实践性学科,实践性学科需要更多的实际操作,比如把书上的源代码都自己实现一遍。以前我提出过一种学习思想:终生学习的思想已经落后,终生研究才是晋级大师的绝招,在读这本书的过程中,我发现自己还处于学习阶段;也许研究应该是基于学习,先熟悉一下基本的理论,然后才能开始研究性学习?也许是我缺乏耐心吧?也许是我还没找到如何把理论变成实践的好方法? 读完这本书,收获如下: 1. 一个可以运行的程序产生过程:代码编写、预编译、编译、链接。 不管是静态语言c、c++、c#、java,还是动态语言html、js等都满足这个套路,我主要是从c、c++角度理解。代码编写就不用多说,预编译可能就是很多程序员不清楚的概念,也包括以前的我;这里需要先介绍一下预编译命令,就是#符号开头的代码,如#include,#pragma,#define,#comment;预编译过程就是把这些符号开头的东西处理掉,比如#include这种符号后面的就会把文件内容复制过来,#define的就会搜索整个cpp并执行宏替换,#ifdef这种就删掉不满足条件的代码,这么一处理之后cpp文件就变得很纯净,所有的编译过程值在本文件执行。预编译错误发生在这里,比如#include的文件不存在、#ifdef没有对应的#endif; 编译过程首先是此法分析,就是本语言支持的语法,如array[index]=(index+4)*(2+6),解析成array(标识符)、[左方括号、 index标识符、]又方括号、=赋值、(左圆括号、)右圆括号。。。。。。分析最小单位是一个语句即以;结尾,接下来是语法分析,将产生一个语法树,因为这个语句是=赋值语句,所以把=作为树根节点,然后按照计算优先级展开[]、*,现在就是[]变成array、index,*下面的子节点是+、+,从语法树的分析可以看出,语法展开是按照计算优先顺序和运算符展开的;接下来就是语义分析,会给每个标识符和运算符指明具体的含义,比如array指明是数组,index指明是一个int类型的变量;接来是中间语言的生成,就是产生三地址码,格式如x=y op z,op代码操作符如=、+、-,例子中的c语言代码的三地址中间代码如t1=2+6;t2=index+4;t3=t2*t1;array[index]=t3,感觉这种三地址代码适合入门级程序员啊,哈哈,不过编译器还是有点聪明,稍微优化一下就变成t2=index+4;t2=t2*8;也许很多人会认为这里产生新变量是不是会增加栈空间内存,肯定是不可能的,这些新变量都是直接拿cpu的寄存器当存储空间;就下来就是生成汇编代码,将这些汇编代码保存在一个文件中,这个文件叫做目标文件,windows中后缀名是obj。编译错误比如缺少分号、括号、变量类型错误、重定义等。 链接过程就是把各个目标文件有序地合并在一起,合并的一句是各个模块中函数、全局变量的相互调用关系;在编译的时候会为本cpp找不到的符号(函数、全局标量)预留空间,比如函数int Add(int a,int b)本模块没有定义就不知道函数地址,变成生成目标文件的时候就先不管,做个需要连接的标记即可,此时也不会报编译错误;在连接的时候就去其它模块搜索有int Add(int a,int b)定义的地址,比如搜索到Add的地址是0x012345667,就把这个地址娶过来填上,如果搜索不到这个地址就会报连接错误;将所有的函数符号和全局变量符号都填上有效值之后,就将所有目标文件合并成一个目标文件,windows上这里已经叫可执行文件,当然还要把函数入口地址即main函数的地址填好,就这样一个可以奔跑的程序就诞生了,感觉自己在创造世界,而创造世界的全过程已经讲完。这个过程是产生连接错误的地方,比如函数声明但未定义就会报错,因为找不到函数地址;静态库实际上是目标文件的打包,有时候静态库根本没问题,结果放到你的程序中静态库中发生连接错误,这时候你不要怀疑你代码哪里写错了,其实是静态库本身的问题,需要找到静态库的实现文件把加入进来才行。 2. 程序的装载与运行。 程序和进程的区别,程序就是放在磁盘上的可执行文件,进程就是把可执行文件存到了内存中;当然放在磁盘上的程序内容要多一些,比如文件格式、导出全局符号表等,加载到内存中的主要是代码段和数据段;以前程序的装载方式是使用段内存,现在使用的都是虚拟内存,虚拟内存将所有进程的地址空间独立开,增加了程序运行的安全性和内存的复用率;首先将可执行文件的信息一一映射到内存中,这里不会真的加载文件到内存中,只是建立一个可执行文件与内存的映射表;映射表建立好之后就开始执行文件,当系统发现页错误也就是实际的数据还没加载到内存的时候就去磁盘对应的位置把数据加载进来,然后开始执行。当cpu发现某些数据段不常用,就会把内存页给卸载到磁盘中,或者当内存比较紧缺的时候,会把不正在使用的数据缓存到磁盘中,这就是为什么4G内存却可以同时运行更多的程序的原理。 程序的运行过程从程序装载之后说起,程序首先执行的代码并不是main开始的代码,也就是程序的真正入口不是main函数,而是运行库的入口函数;运行库会 先把main函数需要的参数、环境变量等准备好;然后将标准输入输出文件描述符打开,这样才能保证main函数开始就可以使用printf;然后把堆初始化,这样才能保证程序可以自由地执行malloc、new等;还有就是把全局变量初始化、全局构造函数执行完;做完这么多工作之后运行库就执行回调函数main,这时候程序才开始进入人们常说的函数入口。程序的运行环境组成是:程序本身逻辑代码、运行库、系统内核、内存;内存分为用户空间和内核空间,只要内核才使用内核空间,其它的包括运行库都是使用用户空间;程序运行空间分为栈空间和堆空间,函数运行的环境就是栈空间,栈空间都是有固定大小的,一般是2M,地址增加方向是向低地址扩张;堆空间比较大,使用也很灵活,这个堆空间一般都是运行库在帮你管理,堆分配算中中最简单的就是空闲链表算法;程序运行完之后,运行库还要帮你把所有的后事处理掉,释放堆空间、关闭所有打开的文件描述符、释放所有的进程资源等,这就是进程关闭内存泄漏的那些空间能够得到回收的原因;从程序的整个过程可以看书,main函数只不过是运行库的一个回调函数,不是真正的函数入口,当然我们也可以自己写一个运行库,这样就可以直接运行在系统内核之上了,运行库主要部分就是标准c接口的实现,听上去并不复杂。 读完这本书之后,感觉自己已经掌握程序的编译运行原理,并且对操作系统的结构有很深入的认识;对内存、cpu的体系结构也有很清晰的认识,感觉整个程序对于我来说它的各个细节已经在我掌握之中;还有从更高的角度认识了多线程、多进程;总的说来就是感觉自己的编程境界上升了一个档次。 总结完之后才发现,自己还是一个菜鸟,书中的好多内容我都没有说清楚,甚至没有理解到;而且网络编程方面还需要我好好学习一下,毕竟未来是网络时代,服务器编程至关重要;人工智能时代即将来临,而我还没有在人工智能领域入门。学无止境,乐在其中就好。
|