(本文转自Alf的blog)
你可以从各种途径学习计算机,学习编程,你也可以很快地学会(当然并不一定精通)一大打的编程语言。但是,我补充一点,学会编写好的程序,才是非常重要
的。下面,我就以实际开发中的情况为例子,记叙一下我遇到过的各种不好的编程习惯。希望由此自省,更希望对别人有所帮助。
我是做Linux下开发的,所以本文更加侧重Linux环境下的Console C程序设计。
格式问题
你可以任意挥撒你的思路,却绝对不能任意涂抹你的程序。因为,并不是你写出来,编译出来,就完事了。它是要被人读的,至少,你以后可能读到。好的格式,无疑是快速理解程序的开端。大概有以下这几种情况。
- 语句中双目的赋值、运算、逻辑、比较等运算符号两边,保留一个空格。
- 逻辑操作中,尽量用小括号表明操作顺序。
- for语句中三个分号后边,加一个空格,并且尽量不要写得太复杂。
- #define、#ifndef、#endif等语言,一定要顶格写,这已经不太是格式问题了。有的编译器,会对不顶格的条件编译语句进行错误的处理。
- 用空行把相关度低的语句分隔开,使整体上有一种分块的感觉。这也不仅是格式问题,好的编辑器(如VI)会按空行分别段落,有利于你快速地定位自己的输入位置。
- 应该缩进的地方一定要缩进,使程序看上去层次分明。并且,缩进一定要用标准的TAB,不要用4或8个空格代替。
- 不要在一行中写多条语句,虽然你认为它们非常亲近。但是,它们也没有亲近到睡一张床的程度。
注释也要写好
注释是以后读这些源码最重要的提示,写得好了,可以事半功倍。
- 函数功能、输入输出之类的说明,写在库文件中,因为更多地时候,别人看得是函数声明。
- 大块地for、while语句的结尾,写好注释,以便标志好是哪的循环结束了。
- 注释不要写得太多,一行一注,没有必要。注释的作用是提醒,不是手把手地教科书。画龙点睛,才是上品。
- 注释风格要统一,这个不用多说。
变量名问题
变量名的命名,直接影响着你的程序的易读性以及易写性。
- 变量名不要太长。有的程序设计书上鼓励变量名写得长一些,说反正编译时会全部转化掉,不会影响实际程序。那他是没有读过喜欢长变量名的人程序,坐
着说话不腰疼。我最近读了个同事的程序,通篇都是the_own_string_10_length_int量级的int类型的变量名,看得我头都大。如
果不是有VI的自动补全,我升级这个程序都升级不了。
- 变量名不应该短的地方,也不要短。短,得在易于理解的情况下。比如,len都知道代表了length,str代表了string,但一个l或s,就有点难于理解了。
- 不要一个过程里声明太多同类型的变量,如果你的函数开头int了一大堆i, j, k, l, m, n,检查一下程序吧,看看同时能要到几个,剩下的删除。
初使化问题
变量声明了,一定要注意初使化。用得好了,这里面有很多技巧。
- 如果能保证以后的操作不会直接使用默认值,就不要初使化了。比如,你声明了一个FILE *fp,以后肯定会用它打开文件的,那就不用再加一条fp = NULL了。初使化,多少也是要浪费一点点时间及程序语句的嘛。
- 要用到的值,会在以后用到默认值,一定要有手动地初使过程。不要假定声明的整数值是零或者假定声明的指针是NULL。
- 初使化过程的位置,最好与使用过程在同一个语句块中。最近看一个朋友的程序,每次调用一个求值的函数给自己赋值之前,还要先把值清零,一看函数实现才发现,过程中要用这个零值进行计算得出函数结果。这样不好。既然函数的功能是赋值,那就在函数中去清零。
条件判断语句
我读过不少别人的程序,发现这是出问题最多的地方。
- float、double型的变量,不要直接与0做比较,0.0也不行。因为浮点数都是用计算机的二进制表示,都是近似的,计算结果为0往往表现为一个极小值。所以,如果想比较a-b是否等于0,应该比较a-b是否比0.0001(或者更小的值)还小。
- 指针值不要与0比,要与NULL比,虽然NULL的值就是零。这样给人一种前面那个变量是整数值的错觉,并且也可能因为要进行一次强制转换而影响效率。
- 不
要出现没有==的条件判断。很多人喜欢if
(a),来当成a是一个非零值,甚至像if(a=b)一样的与赋值操作写在一起,导致优秀的编译器出现善意的警告。这样不好!我一个同事很多地方出现
FILE *fp; fp = fopen(file, "r"); if (fp <
0)的情况,严重错误,我假定他是与底层地open调用搞混了。
内存省着点使
这绝对是个严重的问题。很多人不注意,导致程序整体性能低下,浪费资源严重。
- 如果不是编写特点要求即时性的程序,不要轻易以空间换时间。这是个涉及相当多技术手法的活,不要初学就适,先好好了解一下汇编及编译的知识,了解下数据存储,再用。用了,就要用得值。
- 能重用的变量就重用。看过一个家伙的程序,喜欢一个函数里有多少个char *就用多少个int来求strlen,气得我流血。一个int len,挨个求出来处理不行吗?
- 除
了可以用union的地方用union以外,变量名的声明也要注意,不要三五十的数量就用unsigned long int或是GCC扩展的long
long int,int或short足够了。我感觉在实际地环境里,有byte类型的话,其实大部分用byte就可以。
- 想全局进行define的地方,换成const声明变量吧。一是效率问题,二是方便调试。define的变量,调试器认不出。
- 如果一个变量只在这一个文件里用得到,就写在.c文件里,不要麻烦头文件了。这样,可以避免每包含一次,内存里就多一个拷备。
- 如果出现char a[1024]类似语句的时候,而又不太要求效率,是不是考虑动态分配相应的内存呢?
不要内存泄漏
这个问题,通常总是不得到重视。真正出事了,就晚了。
我写过一个程序,要进行多次的建删除链表操作,每次总是只free了表头,结果导致程序运行一分钟,就要泄掉90M内存外加200M的swap后被系统杀死。
有一家公司开发过一个Deamon,每隔一个月就要重启一次,就是因为有一个函数每被调用一次,就泄漏一个字节,导致一个月就要用光全部内存。
- 在一个语句块里,有一个malloc,就要有一个free。这必须严格对应。
- 不要在函数体里分配函数外要用的空间。这样的话,你不得不记好哪个函数调用完要free东西,手动记载也会把你弄晕的。函数体里分配,就在函数体里用,用完free掉。
认真定义函数或程序返回码
处理好函数或程序返回值,才能很方便地被程序或其它程序调用。shell下的那么多程序,可以被方便地写进脚本,联合起来,执行强大的功能,就是因为它们一致的接口、输入输出以及返回值。
- 判断真假的函数体,一般用0表示假,用1(或者有的用非0)表示真。
- 判断执行成功与否过程,一般用0表示没有错误,正常结束,用其它值,表示错误,多种不同值,也就可以区分各种错误。
- 判断是否相等的过程,一般用0表示相等,用正数表示大于,用负数表示小于。当然,这里的大于、小于,是相对大小,不一定是真的大小。
- 计算、赋值功能的函数,最好返回的就是结果。比如,计算过程等,这样可以方便地调用。
- 处理字符串一类的函数,最好返回结果的指针。看看strcpy是多么的好用,以至弄得那么多公司笔试要用它为难刚出校门的学生。
- 无关紧要的过程,返回void。比如程序中的help(); version(); usage();等函数,puts些语句罢了。不用返回值。
严格处理调用失败
- 打开文件或分配内存时,一定要判断是否成功,否则你将常常面对Segment fault。
- 调用函数时,也要在关键之处判断成功与否,有的,要仔细检查errno或h_errno。
程序结构化
结构化的程序,易于理解,易于编写,易于维护。
- 如果程序中有两个以上的地方,要用到一块功能差不多的代码,就最好把它们摘出来,写成一个函数。
- 如果一个过程太长,就要考虑把其中的功能进行分块,把重要的几块写成几个分离的过程来处理。这样,对你编写、调试及维护会非常有益处。这个太长,没有严格的定义,一般来说,超过两屏的语句,就会大大降低程序的可理解性。
- 一定注意,一个函数只实现一个功能,不要一个函数的功能七勾八连,越独立越好。
关于输出
输出操作可能是程序设计中最多的操作,同时,也是目前用得最滥的操作。
- 没什么实际意义的语句,不要输出。像什么“程序正在启动”之类的话,如果你的程序没有慢到让人以为没启动完成就死掉了的程度,就不要打印它了。
- 输出格式一定要严谨。有个原则,宽进严出。就是说,程序(或函数)可以接收的值,一定要考虑全面,输出的值,一定要严格,这样,才会方便其它程序或人的使用。
- 输出的地方要标准。正常数据到标准输出,错误或警告到标准错误,文件到文件,应该到/dev/null了到/dev/null,哪一条都不可马虎。
效率问题
在现在高速的处理器上,写一些哄人玩儿的小玩意,效率问题可能并不是很重要。但是你不要把目光放得这么低,写就写高效的、稳定的、强壮的程序。
- 尽量减少在循环中的语句,只要循环体中的操作不会影响的值,就不要在循环体中进行赋值、计算或比较。
- 操作系统IO时,尽量一次
性进行大量的操作。因为CPU、内存,都比IO要快数百乃至数千数万倍,不应该让慢速的IO耽误快速的运行。比如计算数值写文件,不要计算一个,写入,计
算一个,写入,那样是在自杀。一次计算N个,一次写入N个,效率会大幅提升。(N视具体情况而定)
- 如果不是技术问题,尽量用回溯代替递归。回溯与递归之间的效率与资源使用差异,通常是指数级的。
- 如果一个功能有现成的库函数,并且你不能保证你能写出比库函数更优秀的过程,就一定记着用库函数。库函数怎么也是经过时间考验的,在通用性、跨平台性以及效率上,多少是强一些。
- 尽量不要使函数参数超过5个,越少越好。首先是易理解性的问题,另外,很多系统的实际运行中,少于5个的函数参数是放在EBX、ECX、EDX、ESI、EDI寄存器里的,而超过5个的,则要进行别的转化处理,会大大降低指令的执行效率。
- 减少分支,减少判断。流水线模式的处理器,加上强大的预读预写处理,对于顺序指令,会高速地执行。但是,一旦遇到分支,它不得不停下来,等待条件语句执行完毕,虽然现在的处理器能进行分支预取,但效率的影响还是巨大的。
- 加1与减1操作,用++与--,不要用赋值的a = a + 1形式。在处理器上,自增与自减操作,与取址、取值、加立即数、取址、赋值的操作,差别很大。好的编译器,会自动的把加1及减1操作,解释成自增及自减,但并不是都是这样。
编译相关的东西
这也是写程序的一个方面,如果你只是想写一些hello world,当然不用理编译。但我们要的是写好的、功能强大的、稳定高效的程序。所以,编译方面是一个不得不认真对待的问题。
- 高度重视编译警告,不要编译过去就完事大吉。源码被编译通过了,仅仅代表语法上问题不大,还过得去,往往大量地有用信息,包含在警告信息中。如果你用GCC,我建议你alias gcc='gcc -Wall'一下,保证写出的程序,没有一个错误,也没有一条警告。
- 库文件一定要有#ifndef判断,保证库文件不会多重包含。不用要#ifdefine填加大量的调试信息。混在程序中,会大大降低程序的可理解性以及大大增加程序的源代码行数。
- 慎用编译器的优化选项,由其是最高极别的优化,巧用编译器的调试选项。总之一句,编译选项,好好研究下吧。
这几天看别人程序,改别人的程序,很是感慨,于是乎有个想法,我想写篇东西,现在就写出来了。没有写完,也没有写好。可能很多已不是自己的本来意思。
感兴趣的看看,互相交流吧!献丑了。
* 编写高效的Linux C程序续
少用字符串烤备。
假设字符串有10个字节,这样,每判断一个字节是不是'\0',复制一个字节,CPU就要至少执行两次。 10个字节,就是20次。并且,由于每次都进行判断,使得CPU的指令流水线无法或者低效处理,更是雪上加 霜。所以,字符串烤备,是一项对内存要求不大,但是确非常浪费CPU的操作,应该尽量避免。
在一个好的C程序中,会把所有的静态变量,写到一起,程序中用指针来指定。除非有输入或者输出, 否则很少很少会用字符串烤备。
少动态分配空间
在相当多的情形下,动态分配的空间,要远远大于我们的想象。不信的话,你可以每次动态分配若干个 字节,看看分配多少次内存被用光。因为,在堆上分配内存,我们得到的是一块空间,但是实际内存并不一 定是连续的,很可能是一些灵乱的空间小块,每块用双向的链表指针连接起来。这样的话,动态分配的这块 空间,就比我们想象的大很多。
另外,动态分配空间,本身就是一项复杂的操作,涉及到大量的链表甚至遍历、判断等操作。我们在栈 上,分配一块空间,只是栈顶指针偏移一下,但堆上就远远不是。所以,应该尽量减少动态分配内存,而是 采用静态的方式。这样,效率要高许多。
有人喜欢做字符串输入的时候,先判断字符串长度,再申请长度加1的内存,这是非常得不偿失的举动。 另外,千万不要在一个函数中,把本可以静态分配的结构,非要动态分配,再在末尾释放。这更是一种愚蠢 的编程方式。
不要自做聪明
程序应该是在保证功能、保证可读性、保证性能的前提下,越简短越好的。并不是写得花哨一些,就有 多好。相反,有的时候,就是无用而且有害的。
我看到一个人写程序,为了防止注入,把本来的strcpy (dst, src),硬生生写成 strncpy (dst, src, strlen (src))。这除了增加一轮字符遍历导致性能下降而外,没有任何用处。 因为,strcpy本身就是靠末尾的0结束符来判断烤备完成的,而strlen也是靠末尾的0结束符来判断长度的。 这样用strncpy,就是把一项操作,调用了两次。正确的做法是,比如你的dst只有10字节长,那么, 你用strncpy (dst, src, 9),然后,硬性把dst[9] = '\0'。
不要愚公移山似的不怕辛苦
一个好的程序员,应该是做有意义的事,尽量少做重复的事。
有人写程序,经常在一个大的函数下,根据输入参数的不同,而处理大量根本不同的操作。这样的话, 可以在调用这个函数的时候,不用判断参数,而方便一些。而结果是,导致别人读起来异常吃力,并且,效 率也并不高。因为要判断的,在哪都是要判断的。这种情况,最好是用不同的函数,实现不同的功能,在调 用的地方,判断情况,分别调用不同的函数。
还有的人,则相反一些,功能相似的函数,只是一点参数不同,一写写一大堆(当然可能是烤备出来 的),这样不好,非常不好。其实这样的地方,多半可以用宏来解决。或者,用偏移地址定位。比如,你要 根据输入的enum值,来返回不同的字符串,就可以把字符串声明成一个大的二维数组,这个函数用来返回二 维数组的偏移量指针,最好不要不辞辛苦地写一大片switch或者if else。
不要让常规观念影响自己
我看到一个现象,写Windows程序熟练的人,喜欢把一列上的一个node,给一个int类型的 handle(id),然后在程序中,到处传递这个handle(id),每次要定位这个node的时候,就是根据它的 handle(id),到列上去查找,然后返回node。这可能是受关系数据库的观念影响吧!
为什么要这样?直接在程序各处用这个node的指针不是挺好么?指针是四个字节,int也是四个字节,而 用指针根本不用遍历查找。
不要动不动初使化一些东西
很多第三方库的使用,需要进行一个初使化,然后使用它的功能,最后在释放这个初使化。这就使得一 些保险的程序员,为了保证程序不出错,一用到库中的功能的时候,就初使化一下。这样做,非常糟糕。因 为类似这种情形的初使化,一般来说都是资源消耗非常大的。
比如很多要求性能的程序,连系统的内存都不会每次使用时再申请,而是有自己的内存管理算法,分配 出来,多次使用。
对这类的库的好的方法是,程序运行最初,初使化这些东西,最后退出时再释放。如果怕出问题,就加 一个全局的标志,只要进行过初使化,就标志一下,以后怕出问题的地方,都查看这个标志位。
|
|
|
阅读(593) | 评论(0) | 转发(0) |