linux 下 C 编程和make的方法 (五、准备写C代码)

来源:互联网 发布:怎么查ftp的端口号 编辑:程序博客网 时间:2024/05/20 02:29
停下make的说明讲解。因为我讲得再多,就有点形而上学了。为了说明而说明。如果单就一个工程,一个src目录,上面的make说明,足以利用make完成代码编译等开发工作了,且不谈版本发布问题。同时,我还是希望交叉点,既然是linux 下的C编程和make。则也要说说C。选来选去,决定说说树。因此,先从tree说起。但在写C代码之前,我们得谈谈怎么样才算是个好代码,或者相对好点的代码,怎么规划代码。这些和tree没关系的闲话。
     有人说,快教几招实用的。虚的有啥好说的。我教你的劈柴,不告诉你得劈成什么样,你就会个动作,出去显摆,别人也不叫好,有什么用?写代码只是过程,并不是结果。无论自己用还是给别人评价,总要有个收获。因此,总得有个评判。由此我们先说说评判标准。啥是好,啥是坏。
     好的代码和好的系统一样,需要:
     1、稳定。
     2、安全。
     3、可塑。
     4、高效。

    这里一一解释。稳定,也就是可重复。电脑关了再开,开了再关,硬件软件逻辑是固定的,你至少得保证代码多次运行目标一致吧。这里不谈结果一致,是因为防止别人抬杠喷我,如就是要做个随机数输出,就是希望结果不一致。但目标是一致的。程序不是使用一次性筷子,只用一次。编程编程,编写流程,流程需要稳定,不然你编写它有什么意义呢。

    但有些入门而不熟练的人,会把程序的稳定和安全混在一起。例如

     int *p = 0;
     if (*q > 10){
     p = q;
}
     printf("%d\n",p);
有人说这是不安全的,也是不稳定的。我说这得怎么看。这里我把稳定和安全做如下划分:
     1、由于数据的改变,导致流程的输出目标不一致,这属于流程对灌入数据的不适应,这叫不稳定。也就是说,属于流程处理的数据变动导致的问题,属于稳定的范畴。
     2、由于使用者的行为改变,导致流程的输出目标不一致,这属于不安全。也就是说,流程处理的流动方向变化导致结果是否和目标一致,属于安全范畴。
     大家肯定认为我在嚼舌头。简单的说明,每个模块都是安全的,组合起来未必安全。每个模块都是稳定的,组合起来都是稳定的(只是效率有差异)。这算是最大的差异。也正是因为这样,在系统设计时不同的阶段,关注的目标不一样。你在模块组装,或模块相互协同工作时,独立测试要关注模块内部的自身安全和稳定,联合测试则只要对整体新规模系统的安全性进行检测。
     简单的举个例子吧。我家楼下的门禁。
     小区的每栋都有门禁系统。要刷卡,恩安全。而且每个登陆的卡,都能刷,同时根据权限决定,是否自动开门,恩,稳定。小区有地下车库,车库入口有刷卡匝,没卡一样进不去,又稳定又安全。但有个问题,为了方便,地下车库联通了每栋楼,换句话说,每栋楼的-1层都是地下车库,而且没有门禁,大家觉等整体是不安全,还是不稳定?
     稳定强调落入我的流程规范范围内的目标输出的一致。安全强调整体过程合理可控。回到上面的代码,因此怎么说,都是叫作不稳定,而不是不安全。这里说说病毒。大多数病毒利用的是系统不安全问题,例如无权限约束的系统文件改写,难道改文件有错吗?哪怕是系统文件,内核还可以替换呢。而也有些是利用系统的不稳定,例如代码本身的BUG。

     但从写代码而言,特别针对模块而言,安全问题往往是个系统性问题,可以少关注,稳定则是你编写流程的问题了。是必须要关注的了。诸如C代码中的指针等,都属于不稳定范畴。听到这,那位已经骑在窗台准备离开C学习,转学JAVA的朋友,先等等,这里比较高,跳下去不安全,你跳,地心引力可以 稳定的让你向下,而不会让你向上飞。楼梯也是 稳定的,什么时候走,都能到一楼。不过这么高的楼确实不安全,我们得把窗户安装铁栅栏,和富士康一样。
     安全在稳定中已经扯过了,谈下,可塑。这里要谈另外个词“鲁棒”,这个词我读研究生时,觉得简直是个万能贴或味精。但凡说点事情,都谈“这是个鲁棒”的系统。后来才理解,鲁棒综合了安全和稳定两个含义。讲究系统对内外部(内部就是过的数据,可以这简单的说)的变化,自身性质不会有所改变。但这个不是我谈的“可塑”。
     塑,即塑造,构造,也就是,可以重构的。或者拆下来再组装成另一种系统的 。如果还是原系统,那叫COPY。记得以前,比较早啦,上个世纪,刚学习面向对象时,有个书上说,面向对象有两个好处,一则,比较容易描述具备继承特性的对象群体的集合和关联。二则,可以令代码可最大化重复使用。于是乎,面向对象几乎成了万能的。什么东西都琢磨琢磨上面向对象。典型的例子就是用C++实现数据结构。有些数据结构用面向对象比较好。典型的是字符串。但有些数据结构,例如堆栈,你用面向对象实现,有这个必要吗?这和拿JAVA讨论算法计算性能一样无聊,你用JAVA就是快速实现面向对象的开发任务,在虚拟机上你谈什么计算性能啊?甚至有人把C,C++,JAVA同样写点算法,跑跑,居然得出JAVA其实不慢的结论。大可不要信,权当风景看看就行。
     其实C也可以做到高度可塑的,不是面向对象,而是面向模块。力求模块的最大化利用。典型的,利用最广泛的,就是库函数。说到可塑,必然谈到两个问题。
     1、模块的合理划分。
     2、模块内部的稳定性。
     前者包含了模块与模块拼接的效率问题。后者是个写代码的问题。这两个问题都没办法具体化。唯一说说的是,我们怎么组织C文件。前面说过,C标准每个编译针对一个文件。因此,如果把一个独立模块分在几个文件里去实现,不靠谱。把多个模块写在一个文件里也不靠谱。最好的标准,就是一个独立模块就是一个文件。但此时又出现两个问题。

一个是哲学问题,在物理中也存在:什么是最小的?

一个是系统问题,如何让一个C文件尽可能的不依赖其他文件。

     两个都挺难的。比如,第一个问题。有人说,一个函数的代码正常情况下不要超过50行,恩确实,但极端的人说不要超过25行也没错。还好我们的计算机体系结构对于高级语言,再怎么细分也是不会到机器指令的细分,但到快接近机器指令的程度已经没有高级语言存在的必要了。你干脆写汇编得了。
     而第二个问题,很少有不调用其他C文件的代码,你但凡调用就有关联。一关联,就不能解决一个理想目标,让C文件不依赖其他C文件。
     先解决第一个问题吧。我的态度是,有个逻辑组合,具备存在价值,而该价值的存在是不可拆分的。那么这个逻辑至少可以独立成一个最小单元。在C语言里,就是函数。别把函数想的太复杂或比较独立。函数就是描述一个不可拆分的,用语句组合出来,具备独立存在价值的逻辑集合而已。哈,我这等于在转移矛盾,把问题替换成,什么是独立存在价值。这个得具体问题具体分析了,包括可重复利用的角度。
     第二个,理想目标是否存在。哈,确实不存在。可以反证明嘛。
     假设每个模块均独立。那么模块与模块的关联的实现归属于哪个模块?如果归属于已有模块则和假设矛盾,反之则存在新模块和假设还是矛盾。 这个证明,希望大家不要认为多重要。
     但证明的陈述值得看看。也就是说,系统存在两类模块,一类是独立的,一类是模块与模块的关联。由于关联本身是系统自身确定的,除非系统自身作为一个大模块,否则,对于可复用是胡扯蛋的事情。因此,着重非关联的代码设计很重要。
     可重用,模块划分,还需要扩展,这里先打住,我们先看下效率。就是程序的高效性。
     高效性,一定是最后谈的,因为,不安全,不稳定,不可塑的代码,高效死了没有用。有人说,不对,我们在性能优化时,为了提升速度,就是尽可能的不要模块来模块去,恨不得几个函数合并起来,改成汇编,甚至用到特殊指令集。就我谈高效,以前给别人说,从效率上来看,按照以下,性能提升能力是逐渐减弱的。   
     1、理论优化
     2、系统优化
     3、逻辑优化
     4、指令优化
     举个例子。比如排序。快速排序和冒泡排序,对于N大于一定数后,性能差异很大,这就是理论的优化。你汇编死了,理论上的计算步骤少不了,你有多大能耐?
     还说排序。系统涉及到硬件,操作系统等等。整个硬件都你控制,但是你的数据量大于CACHE,或内存,你还是会慢。指令有啥用?但有人反驳了。按你的逻辑,系统优化,那么不同系统怎么复用,怎么兼容,难道不同的硬件平台重新写一套?话不是这么说。硬件平台确实有差异,但是计算机组成原理比较相同。也没说硬件平台一定要重写代码啊。C语言强大的#define ,#if 等就可以搞定一些硬件特性的调整。而且很多实际逻辑组合和硬件关联度不大。

    还说排序,实际操作步骤总要落到实处,比如快速排序,是用递归,还是不用递归,这不是理论问题。不是系统问题。也不是指令可以解决的。递归你就玩去吧。且不谈不同的实现方法有的可并发有的不可并发,不可并发的,你有并发指令集有什么意义?

    曾经有个例子,一屁乘加完后,有个加法再右移,用汇编,就一个移位器,只能做2组16位的移位,而且和乘法器结果必须空等2个cyle。但是有4个乘法器,咱不右移了,继续左乘,让最后有效8位落到高8位,而且有pack指令,直接组装,计算数量由大,还不用空等。这是逻辑优化,有人说你胡扯淡,不就是利用了DSP的并发指令嘛?那么如果卷积过程大于可容纳的位宽呢?这个限制直接导致实现方法的改变,怎么能说是指令优化。

     最后才是指令。虽然一般情况下,指令,特别是特殊汇编指令集,比如类似DSP,我的经验可以并发的,通常手写比最大自动优化编译器的效果好4到 20倍(那也是加上了逻辑优化的综合效果),要么我也不会被别人说是“人肉编译器”,不是我强,是C语言描述一些特殊指令集的组合使用很麻烦,但基数都是被前面的优化所确定的。到这步,基本上没优化前,就能看到你系统的性能了。所以说,高效,即便不谈程序是否有意义(跑都不稳定,跑都不安全,代码不可改),也得放最后谈。
     总结一下,C程序设计,讲究,稳定,安全,可塑,高效。否则没意思了。
     这里补充一下,可塑,模块化设计思想。系统设计,经常有两个相反的名词,从顶到下,从下到上。就是我们先规划整体,慢慢裂变,还是先分割模块独立实现再进行组装。其实都可以,只是操作要注意。而且两种操作都存在独立模块,和关联模块的区分。对于后者书本上基本很少说。画图太累,纯理论文字估计大家会睡着,不如举例子吧。说下家庭房屋功能分割。没有房子,没装过新房,你总住过父母的房子吧,再对比大学宿舍,没上大学的全当我没和你在说事。所以大家多少有直观的感觉。
     请大家举手,谁家你睡觉的床边上就是马桶。你宝宝的尿盆不算,固定的。再请大家举手,谁的学校宿舍,假如某个宿舍房间没马桶,而某个宿舍房间有马桶,前者的同学按照学校管理规定,有资格天天去后者的房间便便的?
     没错,马桶就是马桶,是个功能模块。一定要独立出来,没条件每个宿舍放个便便间,但至少一个走廊有个独立的房间是便便间。同时,便便就是个独立模块。但上面的情况不代表我们每个独立模块都要个关联模块。这又怎么说呢?宿舍没马桶,难道你打开宿舍门,就可以便便吗?显然不能。但好点房子,卧室里面有自己的卫生间。你不用出卧室,打开卫生间的门,就可以便便,当然此处省略走几步,转身,脱裤子等贾平凹的细节描述。
     那么上面两个不同的情况,房间里面有马桶,走廊尽头有马桶,关键节点在哪?是马桶的圈圈,还是卫生间的门?显然是卫生间的门。有人反对了。集体宿舍,那卫生间叫一个大啊,多少个坑坑啊。应该每个坑的坑门是关联点。这里有个问题啊,难道卫生间有这种区分,这个坑专门给女性用,就是到男生宿舍拜访的女生,其他只给男生用?如果有,我承认,卫生间的大门不是关联点,你就是去掉大门,还有要选择的。如果每个坑坑都一样,或同一类,只不过有坐式的,蹲式的,还有站式(谁用谁知道,此处不解释),那都是便便用的,管它是大还是小。
     这和写代码有什么关系啊。有关系。我们按照从顶向下的设计方法来看,实际是我们知道一个楼面,开始规划,反之我们是已经有了各个功能房间开始组装。务实的谈到C编程上,则我们如何组织C文件。
     有两种做法,一种,在一个设计图里,设计出各种马桶,在另一个设计图里,设计好所有的卫生间。但是需要引用马桶设计图里的设计。还有种做法,一个设计图是专门给居家用的,里面包含了马桶,卫生间的内容。另一个设计图是给穷学校,不能让每个宿舍房间都有卫生间的客户使用的。设计图就是C文件。我们用哪种?其实不用问我。你们去问问专门做建筑规划的。他们的设计图通常怎么布置的。如果他们的方法没有可取之处,他们这么多年吃白饭的?
     通常,马桶是这样设计的:和卫生间放一起。但有个特例。啥?库函数。为什么库函数通常如五金店。一对东西放进去,随便让你挑。想想也明白啊,标准的库函数作为OS和软件的接口工具,谁知道你想做什么,因此分门别类。但通常这不是你做的事情。回到目标上来,如何组织C文件。大多数情况应该按照如下原则:
     1、文件文件之间层次化。
     2、文件内部模块完整化。
     关于第一点:很简单,一个C文件A,如果调用了 B文件的函数。而B文件调用了D文件的函数,那么,尽可能的不要A调用D文件。原则上绝对禁止这么操作。文件与文件的依赖性,使用make处理。
     关于第二点:如果一个内容,不属于你调用的C文件所能提供的,又不属于你需要提供给调用你的C文件的,原则上,不要在其他C文件里实现。无论内容多少。如果超过3000行,可以考虑分层,将文件依赖关系隔离,就是B调用了E,E调用了D,B,E文件包含了原先B文件的所有任务实现,但新的B不调用D。

     综合一下上面两方面的内容。什么是好程序,和如何规划文件。给出点额外的东西。
     1、只要不是对调用你的C文件有直接关系的,一律用static静态确定起来,同时所有static的,在你设计程序时,落入稳定性关注,同时不要存在稳定性检测逻辑。例如,判断指针是否0.为什么呢?因为这个函数要想被调用,一定是由外部进来的,你当你移动啊,进了公厕大门收费,进了蹲坑门还要收费?
     2、上述的反之,C文件对调用者(外面给进来的)一律需要稳定性检测逻辑,但对被调用的,或者自身使用的,不要增加稳定性检测逻辑,例如一个指针,B文件的函数传递给C文件,C文件传递给了D,则,B传递给C时,C的入口函数要检测,但和B一样,在调用D文件的函数前,不要检测。这是提高效率的做法。也是反过来保障可塑的做法。因为你能决定你调用谁,你不能决定谁调用你。但有个情况需要注意,回调函数这已经不是一般的概念了,得另说。
     3、安全性检测。两种做法。组合枚举法,总线约束法。前者就是A有5种调用,B有4种实现,你组合20种不同逻辑一一测试检测。后者,A,B都只受总线约束,只要9种。但是是不是后者一定好呢?显然不是。画蛇添足,比如组合后就2种方式,还不是重点。重点是,但凡总线约束法,会增加你系统规划的逻辑,总线本身是在系统原始设计任务中不存在的。只是为了资源的集中管理和利用,才有总线的概念和设计任务的存在。你就是要睡觉和便便,为什么需要家里添个走廊。便便要经过它,睡觉要经过它,洗澡要经过它,烧饭还要经过它。

    4、模块利用。如果多个文件共同完成一个任务,想作为独立模块被其他系统使用,组装嘛,那么用一个文件不妥啊,不能解决所有问题,因为有递归效应。模块可能是模块的组合呢,究竟那个层级的模块放一个文件中?,因此但凡是多文件组成的模块,则此时用目录。目录好啊,树状结构,我摘个子目录COPY到另一个工程里,只要这个目录的文件不引用其他目录的东西(标准库不谈),就可以不用担心依赖问题了。而该目录下,有独立的make文件,子目录的内部依赖都解决了。还不够你爽的?(其实这里已经在挖坑了,如何在 make 下让多个目录的 makefile自动化工作,这个坑现在,以后咱们再跳一跳)。

    这里要引申说一下,调用别的库的问题。假设的系统如下分,最高层a,下面有个模块b,模块b又是模块c,d组合的。a调了一个库,b调了一个库,c调了一个库。通常弱手就是一对库放在一个lib里,然后gcc 后 一堆 -l 。庆幸你熟练使用make,不怕。但是模块移动再利用时,你就改把,改啥?新的make啊。因此,每个目录都有个lib自己的目录下的src内的代码调用的库放在自己目录下lib里的库。利用库时,我没说只把src ,inc ,和 makfile等文件COPY啊。直接打包复制过去。这里有人又提反对意见了。我a里调用库和c里调用库的版本不一样,我怎么检查。关我屁事,只要你两个库不是又交叉同时引用到一个独立的库,否则,c层目录下的库就是老版本,至少保证c模块的正确性,你a里调用的库是你a独立代码的事情。我c不是直接给你a用的。是我做好库文件再被你使用的。有问题吗 ?(当然注意是不交叉同时引用另一个库包括相同函数),当然为了和谐,尽可能的要保证不同层次的引用相同库的一致性。同时,除了标准库,不要跨层使用。不然你已经违反了模块化的原则。a,c两个层都用到一个库,而这里的库是一个专门的模块,你为什么不让c全部完成掉呢?你可能说我要快,我不想函数掉函数,靠!!要快,你需要c做什么。有本事你重写去,记得一个常识,80%的时间只运行20%的代码,利用这个尝试重新规划你的系统。别拿不合理规划的系统来反驳合理的模块划分方法。

     总结一下,既然咱们玩C,那么就要有模块化的思想,同时你先别急着学招式,例如while  和for是循环这种玩意,先得琢磨什么是好,什么是不好,这就是我这个章节的目的。心急吃不了热豆腐,难道你看到“若练神功,必先自宫”就去轮菜刀吗?不及不及“即便自宫,未必成功,纵未自宫,亦可成功”所以,安心理解一下这个章节,对后面C代码的实际展开是有帮助的。程序编译通过,不代表是程序,只是代码的组合符合标准规范而已。程序可运行,也不是目标,只是它在某个特定情况下,姑且可行而已
原创粉丝点击