编程高手进阶参考
编程高手箴言-读书笔记
编程高手箴言-读书笔记
1 程序点滴
1.1 程序不等于软件
共享软件是避开商业渠道的一种方法。它避开了商业的门槛。
程序要编程软件,这中间是一个商业化的过程。
1.1.2 认清自己的发展
基于是从耐心中产生的。
任何软件的实现都有n种方法,即使你用最差的那种方法实现的,也没有问题,最后它还是能运行。
通用软件所有做的工作就不是这么简单了,正确的入门方法很关键。
从最底层开始做起,从最基本做起。
1.2 高手是怎样练成的
1.2.1 高手成长的六个阶段
第一阶段,此阶段主要是能 熟练地使用某种语言。这就相当于练武中的套路和架式这些表面的东西
第二阶段,此阶段能 精通基于某种平台的接口(例如我们现在常用的Win 32的API函数)以及所对应语言的自身的库函数。到达这个阶段后,也就相当于可以进行真实散打对练了,可以真正地在实践中做些应用。
第三阶段,此阶段能 深入地了解某个平台系统的底层,已经具有了初级的内功的能力,也就是“手中有剑,心中无剑”。
第四阶段,此阶段能 直接在平台上进行比较深层次的开发。基本上,能达到这个层次就可以说是进入了高层次。这时进入了高级内功的修炼。比如能进行VxD或操作系统的内核的修改。
这时已经不再有语言的束缚,语言只是一种工具,即使要用自己不会的语言进行开发,也只是简单地熟悉一下,就手到擒来,完全不像是第一阶段的时候学习语言的那种情况。一般来说,从第三阶段过渡到第四阶段是比较困难的。为什么会难呢?这就是因为很多人的思想转变不过来。
第五阶段,此阶段就已经不再局限于简单的技术上的问题了,而是 能从全局上把握和设计一个比较大的系统体系结构,从内核到外层界面。可以说是“手中无剑,心中有剑”。到了这个阶段以后,能对市面上的任何软件进行剖析,并能按自己的要求进行设计,就算是MS Word这样的大型软件,只要有充足的时间,也一定会设计出来。
第六阶段
此阶段也是最高的境界,达到“无招胜有招”。这时候, 任何问题就纯粹变成了一个思路的问题,不是用什么代码就能表示的。也就是“手中无剑,心中也无剑”。
对于练功的人来说,他已不用再去学什么少林拳,只是在旁看一下少林拳的对战,就能把此拳拿来就用。这就是真正的大师级的人物。这时win32或者linux在你眼里没有差别
很多人进入第三阶段的使用后,就很难有境界上的突破了。这是就会产生一种观念,认为软件无非如此。认为自己无所不能。
绝大多数都是初级的程序员,中级程序员很少,高级的就更少了。
1.2.2 初级程序员和高级程序员的区别
初级程序员考虑用什么方法才能做出来。
中级程序员考虑用什么容易的方法做出来。
高级程序员考虑怎样才是效率最高、性能最稳定。
软件和别的产品不同,在软件中要达到某个目标,有n种方法,但是在n种方法中,只有一种方法或者两种方法是最好的,其他的都很次,所以要租号一个系统是需要耐心的,如果没有耐心,就没有细活(慢工出细活),一定要投入。
当程序员到达最高境界的时候,想的就是,我就是程序,程序就是我,以机器的思路来考虑问题,以程序的思考方式来思考程序。而不是以我去设计程序的方式去思考程序。我钻入到这个程序里面去了,这时候我没有我自己的任何思维,我所有的思维都是这个程序。
可视化本身就是我去设计这个程序,而真正的程序高手是“我就是程序”
当你到达高手状态,你就是操作系统,你就能做任何程序。
高级程序员应该具备开放性思维,从里到外的所有的知识都能了解。看到世界最新的技术就能马上掌握。实际上,技术到达最高的境界后,是没有分别的。任何东西都是相通的。什么问题一看就能明白,一看就能抓住最核心的问题,最根本的根本。
要有非常强的耐心和非常好的机遇。雄心的一半是耐心,雄心的三分之二都是耐心,如果你越有野心,你就越要有耐心。
计算机技术没有任何时候是突变的,今年和去年不会差很多,但是回过头来看三年以前的情况,和现在的距离就很大。
如果你迷失了方向,你就觉得计算机没有味道,越做越没劲。
追求技术最高境界的时候,实际上是没有年龄限制的。要时刻保持技术的最前端,这样就没有累的感觉。
白天睡觉,晚上干活,那当然累死了,这是自己折腾自己。
1.3 正确的入门方法
一开始就要有耐心,如果你准备花5年时间成为高手,那根本不用等到5年,只要有这个耐心就足够了。你可能2年-3年内就能达到目标。但是如果逆向一年时间内成为高手,即使5年后,你还是成不了高手。
到达高手的境界以后,所有的实物都是触类旁通的。
当你成为C语言的高手,那么你就很容易进入到操作系统的平台里面去。
必须具备开放性思维,可能一个月能拿五六万的这些人,他们的思维不一定能达到很高的境界。
学物理的人会有非常广的思维,他考虑的小到粒子大到宇宙,思维空间非常广阔,思考问题的时候,就会很有深度。
1.3.1 规范的格式是入门的基础
真正的商业程序绝对是规范的。
如果写出来的代码人家都看不懂,那绝对是垃圾。
正确的程序设计思路是成对编码
任何时候都是可以调试的。
成对编码就涉及到代码规范的问题。
代码一定不能乱,一定要格式非常清楚
结合成对编码思维,分块阅读程序,很明显两个大括号之间就是一块代码。
VC自动给你生成的一堆堆的垃圾框架,相对于网上Linux程序来说,它可能更臭一些
在软件没有形成行业,程序等同于软件的时候,那时候程序很容易体现出价值来。
注释格式是非常重要的,但是很少有人注意他
整整要做一个有价值的程序,开发程序的思维就很重要,这种思维的具体提现就在注释及规范的代码本身。
1.3.2 调试的重要性
如果不懂调试,就永远成不了高手。
写这个程序只用了一天的时间,但是调试可能会花二三天的时间。
有可能完全是编译器错误,也有可能因为程序里面增加了什么,二队程序产生干扰。
任何一个东西,只管创建不管释放销毁,这就是很多程序员做的程序没用几下就会四级的原因,MFC让你这么做,就是让你永远成不了高手,你写的程序永远不可能稳定。
1.4 开放性思维
用力与平台之上的系统和实际的应用软件是不现实的
任何一个软件一定都是跟一个平台相关联的,脱离平台之上的软件几乎都是不能用的。
任何一个问题,如果你能把它拆开来思考,这就是简单的开放性思维。
连函数都不会分的话,那就是典型的封闭式思维。
1.4.1 动态库的重要性
有了动态库,当你要改进某一项功能的时候,你就可以不动任何其他的地方,只要改其中你拆出来的这一块。
不存在没有BUG的编译器
1.4.2 程序设计流程
第一步就是要拆除模块
接着额就是砍
从小到大进行设计
什么是可预测性,从症状就能判断出是哪些代码产生了问题,这就是可预测性。
1.4.3 保证程序可预测性
所有的代码必须是经过测试的,必须是一步一步调试过的。
代码在汇编级是怎么执行的,你都得非常清楚,代码对哪个部分进行了什么操作,你都得知道,如果达不到这点,你的可预测性就很差。
开放性思维非常重要,你必须从最底层到最上层都要清楚。
局限在一个封闭的思维里面,做系统就很难。
2 认识CPU
2.2 16位微处理器
8086/8088,与6502之间最大的不同在于指令的体系结构
面临的最大难题是64KB的内存限制
这套笨拙的体系,一直延续到IA64为止
随着CISC(复杂指令体系)的工作频率的提高和技术的发展,RISC现在已经黯然失色了。
2.2.4 中断处理
终端可以认为是一种函数的调用,不过这个函数是随时可以调用的,这样中断就很好理解了。
0-32个中断是CPU 出错用的,成为异常
64位的CPU中,中断扩充成16位,则理论上可以有64KB个中断。
到386才有真正的改革,操作系统才真正进一步发挥作用,从16位真正跨入32位程序。
2.3 32位微处理器
速度上突破100MHz,超过了RISC的CPU
产生动态执行技术,使得CPU可以乱序执行。
2.4.3 程序原理
在实模式下用段寄存器左移4位于偏移量相加,还是在保护模式下用段描述符中的基地址加偏移量。
地址的行程总是从不可看见部分取出基址值与偏移相加形成地址。
对于CPU在形成地址时,是没有实模式与保护模式之分的。它只管用基址(不可见部分)去加上偏移量。
4.2 高级语言的原理
C/C++的原理
C++为了解决某个问题会绕一大圈,代码就会比较大,并且里面会有一些没用的代码,但是C++的好处,就是针对不同的对象去做实例化,这就是所谓的对象化。
任何的程序编译出来都和平台有关。如果脱离平台,任何语言都没有什么意义。
如果要求效率,那就传递一个指针,否则对象复制效率低
自从有了堆栈以后,才出现函数,有了函数,才有全局变量和局部变量的出现。
重载,事实上是同名函数在编译器编译的时候被换名了。所有类中的函数是不能取地址的,除非用静态的方法。
有些人说不要用全局变量,这样会导致程序互相冲突,其实如果用的是不同的动态库,那么是不会出现问题的。
4.2.2 解释语言的原理
解释程序就是一个字符串的解释器。
同名函数使用的时候容易引起误解,还是不用为妙
重载在实际中用处不大,但在教科书中介绍很多。
做程序一定要多问多想,求本求源的治学方法才是成为真正高手的必由之路。
一个代码是不是完全正确,一定要在汇编指令中看一看。
汇编最大的好处就是可以直接控制CPU的运算。
汇编可以确定程序出错的真正原因。
做程序一定要用程序的方法去思考问题
很多人遇到不能解决的问题的时候,总是一遍一遍地去试用各种方法,而不是从代码编译结果出发去解决问题,这样很容易掉进旋涡出不来。
希望用一两个月就成为高手的人永远成不了高手,一定要有踏实的根底,一步一步的磨练。
4.4 挂钩技术
4.4.1 Windows上C的挂钩
用C直接挂钩
- 我的函数地址要取代系统的函数地址。
- 这些地址常常是不可写的,而只是只读的。在这种情况下,就要用WriteProcessMemory函数来写内存。
- 在内存中取得要挂钩程序的LAT地址的JMP表就可以了
用PE的方法实现
Windows系统提供的挂钩方法
- 模块、进程、线程的区别
- 模块是一个静止概念,是文件在内存中的映像,在内存中展开执行文件
- 进程是一个执行环境
- 而线程则是一个执行单位,给模块分配堆栈
- 分时调度是线程调度,而不是进程调度
- 进程是一个任务,是通过主线程执行的,主线程可以创建其他线程
4.4.2 C++的挂钩技术
对COM的挂钩要比函数的挂钩简单很多
5 代码的规范和风格
代码杂乱无章,甚至自己隔了一段时间也看不明白了,这样的程序就没法维护,也就是一种垃圾,
编写程序甚至就像写诗一样,能给人以美感
5.2.3 变量的对齐规定
变量的上下排列顺序按照上宽下窄的倒三角排列方式进行排列
5.10 成对编码的实现方法
在C++中,内存的分配、释放由程序员自己管理,如果不能很好的解决内存的分配和释放的问题,就很容易引起内存泄漏或者内存非法访问,对该问题较好的处理方法是在编程时养成成对编码的习惯,即内存的分配与释放成对进行。在编写内存分配代码的时候,就相应编写内存释放的代码。而不是等到最后再写。
当有一段代码需要理解的时候,可以不必着急从头到尾进行阅读,读代码的时候要分块阅读,而不是从头读到尾。如果代码规范,无论多么复杂的代码,分析起来就如庖丁解牛一般,不用费什么气力就可以抓住主要层次。
成对编码的好处是可以随时运行正在编写的程序。
成对编码的规则就是对等,这个规则可以使得自己集中精力做重要的事情。
如果所有的变量都是可以随意申请的,所以就东一个西一个,如果程序比较大的时候,就容易出现重复申请的错误。
在可执行文件中,数据和代码是分开存储的,如果随意申请变量,就可能在程序编译中,出现代码和数据混乱的夹杂在一起的情况,这样程序运行中就可能出现一些意想不到的错误。
虽已申请变量不能较好的实现成对编码的思想,如果你是随意的申请,就很可能忘记应该什么时候进行释放,导致程序运行一段时间之后,系统就崩溃了。
从上可以看出,变量一定要集中申请。
动态与静态变量
动态分配很可能由于某种原因在运行中不能进行内存的分配,这时可能使整个程序出现错误。如果是静态分配则资源不足操作时系统就会进行提示。
动态分配的空间,如果程序忘记释放,就可能引起内存泄漏
动态分配空间增加了编程量
5.11 正确的成对编码的工程编程方法
如果想成为一个高手,决不能有浮躁怕麻烦的思想。如果一开始就怕麻烦,以后就会越做越麻烦。
5.11.1 编码之前的工作
要合理设置,加速编译
设置合理的debug或者release模式
程序时刻都要保持一种正确的可运行状态
5.11.2 成对编码的工程方法
编写程序的时候,要让程序始终保持在一种可运行的状态下。
把键盘的输入速度设置为最大,可以提升编写程序的速度。编程是以键盘为主,而不是以鼠标为主的工作
在Debug模式下,编译系统被插入很多的代码,例如堆栈检查、初始化变量等。在Debug环境下的代码就会有很多依赖性,也就是依赖于编译系统自动生成一大堆的代码。这样转入Release模式的时候,就没有那些代码,就可能导致出错。
6 分析方法
6.1 分析概要
玩MFC玩得越深,中毒就越深,就越拔不出来
MFC的很多东西都是针对界面来做的,所以它事实上没有多大的用处。
通用软件系统不能简单按照软件工程的方法来进行设计,因为完全按照软件工程的方法,可能就会实现不了。
任何的软件产品不可能一开始就能吧软件的所有细节都考虑到,这是绝对做不到的。
做软件有一条原则最重要,那就是减少问题的累积。
每一个阶段的工作都是可以预测的,因为编程讲究的是可预测性。可预测性非常重要。
按照传统软件工程的方法,即一开始就可以把所有的问题都找到。但是通常你是不可能预测到程序在开发过程中会遇到的所有的问题的。退一步说,即使你把所有的框框条条都设计好,也可能当突然遇到问题的时候,一时找不到正确的解决方法,而后你又试图去解决它,这样就可能需要花很长的时间去解决这个问题。当你发现这个问题是很难解决或不能解决的时候,整个工程都会流产。
遇到不能解决的问题的时候,就可以绕过去,或者干脆去掉小功能,到遇到不可解决的问题的时候,就不用改变原来的开发计划,一开始只要设计好软件的核心的核心demo即可。就可以避免此类情况的发生。
要设计一个程序,首先要明确第一步做什么,第二步做什么,第三步做什么,并且应该知道这个软件能不能实现。
我们一开始只需要做一个Consol程序,把所有这些库测试通过后,再加一个可视的界面也不迟,因为这些库是核心的核心,如果核心都不能实现,做出再多再漂亮的界面也是多余的
6.2 接口的提炼
6.2.1 分离接口
保存地址的动态内存由谁来分配,又由谁来释放呢?一般的原则是谁使用谁分配谁释放。
接口的目的就是把外部条件和内部条件分离开来。
6.2.2 参数分析
因为软件有这样一个特点,只要是变动程序的任何一个部分,那么这部分相关的工作都会全部作废,稳定的函数就会变得不稳定,以前已经测试使用的很正常的稳定模块也可能变得不稳定。
做程序的时候,重要的是保证软件的可扩充性,如果软件是一种不可扩充的状态,就叫做封闭式的开发。这种开发是一种灾难。
6.3 主干和分支
核心就是整个系统中最重要、最基础、最简单的部分。主干就是核心的核心,其后再添加的其他部分就是枝叶了。
6.4 是否对象化
按照面向对象的方法就是首先要建立一些基类,把所有的基本操作都抽象出来,然后再通过一级一级的类的继承方法来实现,这种思维方式就是封闭的思维方式。封闭的思维会限制思维火花。
而开放性的思维不用继承的方法,而是用对象接口的方法。真正面向对象的方法是提供一种实现的接口。
用独立类的方法,当出现问题的时候,就可以很快的找到问题的对象。把对应的类接口直接替换掉就可以了。而如果用大封装的方法实现对象很难做到这一点,因为子类和父类、类和类之间有着错综复杂的关系。
要对象化时的条件如下:这些功能必须是同时使用的,虽然是串行的,但是它针对的每个功能的条件不同也可以对象化。
从函数的运行角度来说,对象只是一个可重入的函数的扩大化,一个函数可重入意味着可以同时使用,那么它扩大化后就变成了一个对象。可重入的意思就是,函数可以递归。
做对象化的时候还有一条准则,即针对不同的应用去设计程序,而不是针对一种所谓的虚拟模型去设计一个对象。
杀鸡取卵就是指设计的软件提供了很多的功能,其实用到的功能只是那么一丁点,可能做了一个巨大无比的对象,但是只用到了对象很小的一些功能而已。
程序越简单越不容易出错
编程就是要保证核心的核心不出错。
6.5 是否DLL化
第一步只要把所有函数实现体的投和注释一起复制到文件的开始部分做函数的原型就可以了
如果没有明确的功能块,就可以以函数在程序中调用的顺序进行排序。
一个好的习惯是,把编译中的所有警告都排除掉,让程序一点警告都没有。很多警告在不同编译环境中会有不同的结果,所以一定要去掉所有的编译警告。
在对程序进行编译的时候,可能会有一大堆的错误。这时候不要着急,而是应该先找第一条错误。对其进行解决后立刻编译。如果发现还有问题,则继续解决第一条错误。
内存和资源申请及释放要同时书写,这是成对编码的原则。
在其他地方分配的资源在DLL中进行释放,则一定会出错。
纯虚函数有一个特性,即当某个函数为纯虚函数的时候,其他集成的类就一定要重新定义,并且其类的指针总能找到他锁指定的运行的函数实现体。
6.5.2 DLL动态与静态加载的比较
凡是动态的过程都会加上一层控制,所以不管是直接用API,还是把这个API封装为一个对象,这样产生的动态过程都可能产生很多问题。同样,内存分配的时候,如果静态分配一块内存最好就用静态,如果搞一堆动态的过程,就会出现一堆问题。
6.6 COM的结构
对象指针才是真正的对象
6.7 几种软件系统的体系结构分析
软件的好坏很大程度上是由软件体系所决定的
任何一个软件都可以画出体系结构,并且动静分离,只有这样你才能知道怎么去做好软件。
7 调试方法
7.1 调试要点
7.1.1 调试与编程同步
所有的代码都是调试出来的,而不是编出来的
编程的时候,先把主要的部分进行确认,这样就会减少后面的错误,因为计算机软件的特点是,在相关的不捡中,如果其中一个有错误,就会引起错误的级数级上升。错误的增加不是一种和的关系,而是级数的关系。
调试一定要和编程的方法一起进行。
在调试的时候要了解代码和寄存器的聚合度
是软件就会有BUG,这个不能避免
C里面有变量修饰符,可以指定某一个变量用寄存器,这虽然不一定管用,但是C的编译器会优先考虑。
编译器整个代码产生的过程如下
第一轮:编译都会按照不优化的方法进行,变量都放在内存中。
第二轮:编译器试图看哪个变量用的最多,哪个变量的使用频率最高,编译器就会把这些变量从内存中去掉,把该变量指定某个寄存器。
要做好一个软件,不在乎一天能产生多少代码,而是在于一天确认了多少代码。
7.2 基本调试实例分析
任何程序都不是一次就能完全正确的编写好的,只有通过不断地调试,不断地修改,才能保证程序是正确的。
常用的调试方法有三种
- 设置断点和单步跟踪调试方法,其目的就是看运行过程。
- 看源代码生成的汇编代码,目的是看算法的正确性
- 显示程序中运行的信息,用cout printf等输出程序运行过程信息。
程序只能反映你的思想,但那并不是机器的思想,所以一定要看一看经过编译后的最后结果的思想是不是正确的描述了你的思想。
8 内核优化
8.1 数据类型的认识
日常中很少用到double类型的运算,大部分用float就够了。
8.2 X86优化编码准则
8.2.1 通用的X86优化技术
- 短格式
- 简单指令,REG reg op men
- 相关
- 内存操作数,ALU,算数逻辑运算单元
- 寄存器操作数,ESP,EBP
- 栈引用,PUSH,MOVE
- 栈分配
- 数据嵌入
- 循环
- 代码对齐
8.2.2 通用的AMD-K6处理器X86代码优化
-
使用可短译码指令
-
将可短译码指令配对使用
-
避免使用复杂指令
-
避免使用多重及累计的前缀
-
0Fh前缀法
-
避免长指令
-
使用读-修改写指令代替分立的等价操作
-
将罕见使用的代码和数据搬移到单独的页内。
-
避免混用代码尺寸类型
-
成对使用调用CALL和返回RETURN指令
-
利用整数和浮点数惩罚的并行执行
-
避免超过16层的子程序嵌套调用
-
将频繁使用的栈数据放在距离ESP所指向地址128字节范围内。
-
避免超集相关
-
避免过长的循环展开和内嵌代码
-
避免在32Bit代码内分解16bit内存访问
-
避免在某一个指令附件发生依赖于数据的分支操作。
---------------转载------知乎