| 网站首页 | 业界新闻 | 小组 | 威客 | 人才 | 下载频道 | 博客 | 代码贴 | 在线编程 | 编程论坛
欢迎加入我们,一同切磋技术
用户名:   
 
密 码:  
共有 3129 人关注过本帖
标题:[新增]程序设计方法及编写优质无错C程序秘诀
取消只看楼主 加入收藏
Knocker
Rank: 8Rank: 8
等 级:贵宾
威 望:47
帖 子:10454
专家分:603
注 册:2004-6-1
结帖率:92.86%
收藏
 问题点数:0 回复次数:6 
[新增]程序设计方法及编写优质无错C程序秘诀
[Post=10]论程序设计方法

作者:杨老师

如果你是初学者----------------请不要阅读;
但有志成为中高级程序员--------请务必阅读;
如果你是中级程序员------------请务必阅读;
如果你高级程序员--------------请批评指正。
  本文是我在“软件工程师班”开学第一节课的讲义,和“计算机软件设计发展”讲座上的内容整理而成。写作本文的目的是引导学生从更高的层次来看待程序设计方法,为将来成为高级程序员而做好理论准备。

一、计算机硬件环境对软件设计方法的限制
  计算机的发明到现在已经60年了,计算机程序设计方法也伴随着计算机硬件技术的提高而不断发展。硬件环境对软件设计既有严重的制约作用,也有积极的推动作用。
  在我的大学母校(此处删除6个字),数学系的一些老师,有幸成为了我国第一代的计算机DIY一族。呵呵,不要以为是组装PC机呦,他们组装的可是小型机。一人多高铁皮柜大小的主机,加上纸带机(后期改进为读卡机),组装好后,除了供学校自己的科研使用外,还在全国各地销售了十几台。当时(七十年代)一台的售价是10几万元人民币,如果换算到今天,相当于价值大约为100多万元,非常高档的小型计算机了。下面大家猜猜,这么高档的计算机,它的内存是多少那?(都把嘴闭好了,我要公布答案了)—— 4K。
一块50公分见方的内存板,
插入到主机箱中,好了------ 1K;
再插一块内存板,好了------ 2K;
再插一块内存板,好了------ 3K;
再插一块内存板,好了------ 4K;
再......不行了,插不起了,太贵了!这就是当时的环境。这样的环境下,用什么写程序那?当然只有机器码了。先用汇编写,然后翻阅手册手工改写为机器码,然后打卡或穿纸带,输入运行。可以想象,在当时的条件下,什么叫好的程序那?什么叫优秀的程序那?—— 技巧!
  程序设计的最初始阶段,是讲究技巧的年代。如何能节省一个字节,如何能提高程序运行的效率,这些都是要严肃考虑的问题。而所谓的程序的易读性,程序的可维护性根本不在考虑范围之内。
  今天,35岁以上的学习过计算机的朋友可能都使用过一种个人计算机——APPLE-II(中国也生产过这种计算机的类似产品“中华学习机”)。主频1M,内存48K(扩展后,最多可达到64K)。我就是使用这样的计算机长大的 :)。当年,类似的个人计算机产品,还有PC1500,Layser310等。这种计算机上已经固化了 BASIC 语言,当然只是为学习使用。要想开发出真正的商业程序,则必须使用汇编,否则的话,程序就比蜗牛还要慢了。于是,程序设计中对于技巧的运用,是至关重要的了。

题外话1:
  比尔盖茨是 BASIC 的忠实拥护和推动者。当年,他在没有调式环境的状况下,用汇编语言写出了一款仅有 4K 大小的 BASIC 解释器,且一次通过。确实另人佩服。(不象现在微软出品的程序,动辄几十兆。)这也许就是比尔对 BASIC 情有独忠的原因,每当微软推出(临摹)一个新技术,则他会立刻在 BASIC 中提供支持。

题外话2:
  在 APPLE-II 上有一款游戏软件“警察抓小偷”,当年熬夜玩游戏,乐趣无穷。后来这款游戏被移植到了PC上,咳~~~根本没有办法玩,因为小偷还没跑就被警察抓到了。硬件的速度提升,另我无法再回味以前的时光了。

二、结构化程序设计
  随着计算机的价格不断下降,硬件环境不断改善,运行速度不断提升。程序越写越大,功能越来越强,讲究技巧的程序设计方法已经不能适应需求了。记得是哪本书上讲过,一个软件的开发成本是由:程序设计 30% 和程序维护 70% 构成。这是书上给出的一个理论值,但实际上,从我十几年的工作经验中,我得到的体会是:程序设计占 10%,而维护要占 90%。也许我说的还是太保守了,维护的成本还应该再提高。下面这个程序,提供了两种设计方案,大家看看哪个更好一些那?

题目:对一个数组中的100个元素,从小到大排序并显示输出。(BASIC)

方法1:冒泡法排序,同时输出。

FOR I=1 TO 100
FOR J=I+1 TO 100
IF A[I] > A[J] THEN T=A[J]: A[J]=A[I]: A[I]=T
NEXT J
? A[I]
NEXT I

方法2:冒泡法排序,然后再输出。 FOR I=1 TO 100
FOR J=I+1 TO 100
IF A[I] > A[J] THEN T=A[J]: A[J]=A[I]: A[I]=T
NEXT
NEXT

FOR I=1 TO 100
? A[I]
NEXT
  显然,“方法1”比“方法2”的效率要高,运行的更快。但是,从现在的程序设计角度来看,“方法2”更高级。原因很简单:(1)功能模块分割清晰——易读;(2)也是最重要的——易维护。程序在设计阶段的时候,就要考虑以后的维护问题。比如现在是实现了在屏幕上的输出,也许将来某一天,你要修改程序,输出到打印机上、输出到绘图仪上;也许将来某一天,你学习了一个新的高级的排序方法,由“冒泡法”改进为“快速排序”、“堆排序”。那么在“方法2”的基础上进行修改,是不是就更简单了,更容易了?!这种把功能模块分离的程序设计方法,就叫“结构化程序设计”。

三、对程序设计中技巧使用的思考
  我可以肯定,大家在开始学习程序设计的时候,一定都做过这样一个题目:求100以内的素数。老师在黑板上,眉飞色舞地写出了第一个程序:(C程序)
方法1: for(i=1; i<100; i++)
{
for(j=2; j< i; j++)
if(i%j == 0) break;
if(j >= i) printf("%d,", i);
}
  然后,老师开始批判这个程序“这个叫什么呀?太慢了!因为我们都知道大偶数不可能是素数了,因此,要排除掉!” 于是,意尤未尽地写出了第二个程序:

方法2: printf("2,");
for(i=3; i<100; i+=2)
{
for(j=2; j< i; j++)
if(i%j == 0) break;
if(j >= i) printf("%d,", i);
}
  老师说:“看!我们只改动了一点点,程序运行的速度就提高了一倍多”。然后运用诱导式教学法继续提问“程序的效率,还能再提高吗?能!”,得意地写出第三个程序:

方法3: printf("2,");
for(i=3; i<100; i+=2) ''''不考虑大偶数
{
for(j=3; j< i/2; j+=2) ''''不考虑用偶数去测试,而且只验算到一半就足够了
if(i%j == 0) break;
if(j >= i) printf("%d,", i);
}
  “大家看,我们又只改动了一点点,运行速度又提高了一倍多。可以了吗?不可以!我们还能再提高”。于是又高傲地写出了第四个程序:

方法4: printf("2,");
for(i=3; i<100; i+=2)
{
int k = sqrt(i);
for(j=3; j<= k; j+=2)
if(i%j == 0) break;
if(j >= k ) printf("%d", i);
}
然后,开始证明为什么我们判断素数的时候,只需要验算到平方根就足够了:

  假设p是合数,那么令:p=a*b。反正法:由于我们已经判断了p的平方根以内的整数都不能被p整除,于是 a>SQRT(p)。基于同样的理由 b>SQRT(p)。于是 p = a * b > SQRT(p) * SQRT(p) = p 得出矛盾, 命题得正。
  的确,“方法4”的确比“方法1”的运行速度要提高了好几倍,甚至好几十倍。但我们仔细分析测试看看。
(1)“程序4”到底比“程序1”快了多少那?我在某台计算机上进行测试(P4,1.5G)得到的速度对比表:

计算范围
100 1000 10000 100000
速度差 0.00秒 0.01秒 0.18秒 15秒

(2) 在10万以上,才会看出一些差别。而这种差别根本就不够底偿程序设计阶段的付出。如果计算的范围再大,那么不管是“方法1”,还是“方法4”都不是好的算法。(计算素数的另外一个比较优秀的算法叫“漏筛法”)

(3)写出“方法1”,只要具有小学四年级的数学水平就够了,而“方法4”则需要初中三年级的水平并且还要具备一些“数论”的知识。

(4)从维护性看,如果你写的程序需要另外一个程序员来维护,或者若干时间以后,你重新来阅读这段程序,那么就会对这个程序产生很多疑问:这个求平方根是干什么用的?其实,就这个题目来说,使用到“方法3”就已经足够了。

总结发言:
I. 计算机的价格每年下降一半,而运算速度每年提高一倍”,因此我们应该把速度提高的任务交给硬件实现。
II. 从易读性、维护性出发,程序员只负责按定义给出软件实现。算法的问题是数学家解决的。
题外话:
  多年以来,人们一直在寻找动态图象(影视)的存储和回放的算法,但效果都不理想。直到有人发现,原来在200多年前的数学家早就帮我们解决了这个问题——傅立叶(Fourier)级数展开。因此我要说,优秀的算法不是我们程序员要考虑的问题,我们的任务只要按照数学家给出的算法翻译为计算机程序语言而已。(这句话恐怕要遭到大多数程序员抛出的板砖袭击)再比如,计算一元多次方程解的问题。我们使用的就是牛顿的迭代算法。不要怪我瞧不起你,你能发明这个方法的话,那就是当代的牛顿了。

四、程序的易读性与书写方法
  程序是否容易阅读和维护,与怎么书写有很大的关系。说实在的,C语言中为了方便程序员书写,允许使用++,--,<<,&&,?......这些运算符号。但很多人经常乱用,以为自己写的程序多么简洁,效率多高。其实,当你分行书写的话则更加容易阅读和维护,效率也不会降低,因为编译程序早就帮你优化为最快捷的代码了。先看一个简单的例子:

计算一个整数乘 255(C语言)

方法1:a *= 255;

方法2:因为移位运算比乘法运算要快很多倍,因此a*255的运算书写为:

a =(a<<8)-a; //a*255 = a*256 - a = (a<<8) - a
  方法1的书写非常简单,直截了当,显然更容易维护。而方法2的书写运用了移位的技巧,不容易阅读,但效率最高。是不是真的是这样那?把这两个程序编译为汇编代码看看。原来无论是方法1还是方法2,它们的汇编代码都是一样的:

mov ecx, eax
shl eax, 8
sub eax, ecx
  也就是说,你认为非常技巧的书写方法,其实编译器的优化功能早就帮你想到了。那么方法2的方式就很值得批判了。下面是几个有关C语言书写方面的重要原则:
 

尽量表达愿义,多加注释;
变量名称和函数名称,要使用有意义的符号,并且遵守“匈牙利命名法”;
不要为俭省内存,使一个变量在一个模块中表达多个含义。
在某个模块中,前半部分用i表示计数器,由于后半部分不再使用计数器了,于是又用i来保存某个中间的结果。等你维护这段程序的时候,保证你肯定会犯傻的。
在使用条件表达式的时候,不要混合书写运算表达式;
经常有人在书写for循环的时候,使用这样的方式: for(int a=1,s=0; a<=100 && (s+=a); a++);
天呀,这样写是不会提高程序运行效率的,尤其是当运算表达式复杂的时候,就更不容易阅读了,还是把运算写到for的循环体中吧。 int s = 0;
for(int a=1; a<=100; a++)
s += a; //计算1+2+...+100 这不很好吗?!
再比如,if(a=b)这个写法在语法上是允许的,但不要使用。要使用也要if(0!=(a=b))这样的方式。 还有值得一提的是慎用“,”(逗号运算符)。
不要连续使用++,--,<<,*,& .....这样的运算符号。
a = b++-(--c<<1+e&0x0f>>1); //这个人有病。出这个题目考试的老师,也有病。
常量要写在条件表达式的左边;
if(5 == a) 这是正确的写法,这样书写可以避免勿输入而导致的 if(a=5)这样的错误。
避免程序中{ }的嵌套层次太深;
最多4层。如果必须大于4层,那么写成调用子函数或宏的方式。
尽量多地使用断言;
当你在书写程序的过程中,凭你的智慧,你一定是知道:程序运行到我正书写的这行代码的时候某个变量一定是某个值。好啦,那么不要忧郁,马上加上一句代码:ASSERT(nnn == xxx);。将来在调式维护这段代码的时候,你会得到无限美妙的回报。
书写需要“成对匹配”使用的代码的时候,在写使用代码之前,就先把结束写出来;
file.Open(...); //当要打开文件的时候 char *lp=new char [100]; //当要申请内存的时候
...... //先不要写这段代码 ...... //先不要写这段代码
file.Close(); //马上写关闭 delete [] lp; //马上写释放

xxx.Loack(); //当某个对象需要锁定的时候 for(....)
...... //先不要写这段代码 { //写大括号的时候
xxx.Unlock(); //马上写解锁 } //马上写大括号结束
和这个道理相同,在C++的类中,如果需要申请内存,那么先在构造函数中给出 lp=NULL;然后马上在析构函数中书写 if(lp) delete []lp;
[/Post]

[此贴子已经被作者于2005-3-15 11:42:36编辑过]


搜索更多相关主题的帖子: 程序设计 秘诀 优质 编写 
2005-02-19 13:32
Knocker
Rank: 8Rank: 8
等 级:贵宾
威 望:47
帖 子:10454
专家分:603
注 册:2004-6-1
收藏
得分:0 
[post=10]可以适当地使用goto;
在结构化程序设计中,goto 是被排斥的。但是,如果适当地使用 goto 不但不影响斜率,而且还能提高程序的可读性。
题目:合并2个文件到一个新文件中。(不要挑我的毛病呀~~~~~,我使用的是类C的方式书写的。)
方法1:

FILE *f1,*f2,*f3;
if(Open(f1)成功)
{
if(Open(f2)成功)
{
if(Open(f3)成功)
{
...... //这里是真正干活的地方
Close(f1);
Close(f2);
Close(f3);
}
else //f3不成功
{
Close(f1);
Close(f2);
......
}
}
else //f2不成功
{
Close(f1);
......
}
}
else //f1不成功
{
......
}
==========================================================
方法2:FILE *f1=NULL,*f2=NULL,*f3=NULL;
if(Open(f1)不成功) goto err;
if(Open(f2)不成功) goto err;
if(Open(f3)不成功) goto err;
...... //这里是真正干活的地方
err:
if(f3) Close(f3);
if(f2) Close(f2);
if(f1) Close(f1);
  方法1是最最标准的结构化设计,好吗?不好!尤其是当{ }的层次比较深的时候,估计你寻找真正干活的代码的地方都找不到。而使用方法2的程序,不但程序容易读,而且没有{ } 的深度。在C++中,又提供了异常try/catch的设计结构,而异常的结构则比 goto 的结构更好、更完善了。

五、面向对象的程序设计
  随着程序的设计的复杂性增加,结构化程序设计方法又不够用了。不够用的根本原因是“代码重用”的时候不方便。面向对象的方法诞生了,它通过继承来实现比较完善的代码重用功能。很多学生在应聘工作,面试的时候,常被问及一个问题“你来谈谈什么是面向对象的程序设计”,学生无言,回来问我,这个问题应该怎么回答。我告诉他,你只要说一句话就够了“面向对象程序设计是对数据的封装;范式(模板)的程序设计是对算法的封装。”后来再有学生遇到了这个问题,只简单的一句对答,对方就对这个学生就刮目相看了(学生后来自豪地告诉我的)。为什么那?因为只有经过彻底的体会和实践才能提炼出这个精华。
  面向对象的设计方法和思想,其实早在70年代初就已经被提出来了。其目的就是:强制程序必须通过函数的方式来操纵数据。这样实现了数据的封装,就避免了以前设计方法中的,任何代码都可以随便操作数据而因起的BUG,而查找修改这个BUG是非常困难的。那么你可以说,即使我不使用面向对象,当我想访问某个数据的时候,我就通过调用函数访问不就可以了吗?是的,的确可以,但并不是强制的。人都有惰性,当我想对 i 加1的时候,干吗非要调用函数呀?算了,直接i++多省事呀。呵呵,正式由于这个懒惰,当程序出BUG的时候,可就不好捉啦。而面向对象是强制性的,从编译阶段就解决了你懒惰的问题。
  巧合的是,面向对象的思想,其实和我们的日常生活中处理问题是吻合的。举例来说,我打算丢掉一个茶杯,怎么扔那?太简单了,拿起茶杯,走到垃圾桶,扔!注意分析这个过程,我们是先选一个“对象”------茶杯,然后向这个对象施加一个动作——扔。每个对象所能施加在它上面的动作是有一定限制的:茶杯,可以被扔,可以被砸,可以用来喝水,可以敲它发出声音......;一张纸,可以被写字,可以撕,可以烧......。也就是说,一旦确定了一个对象,则方法也就跟着确定了。我们的日常生活就是如此。但是,大家回想一下我们程序设计和对计算机的操作,却不是这样的。拿DOS的操作来说,我要删除一个文件,方法是在DOS提示符下:c:> del 文件名<回车>。注意看这个过程,动作在前(del),对象在后(文件名),和面向对象的方法正好顺序相反。那么只是一个顺序的问题,会带来什么影响那?呵呵,大家一定看到过这个现象:File not found. “啊~~~,我错了,我错了,文件名敲错了一个字母”,于是重新输入:c:> del 文件名2<回车>。不幸又发生了,计算机报告:File read only. 哈哈,痛苦吧:)。所以DOS的操作其实是违反我们日常生活中的习惯的(当然,以前谁也没有提出过异议),而现在由于使用了面向对象的设计,那么这些问题,就在编译的时候解决了,而不是在运行的时候。obj.fun(),对于这条语句,无论是对象,还是函数,如果你输入有问题,那么都会在编译的时候报告出来,方便你修改,而不是在执行的时候出错,害的你到处去捉虫子。
  同时,面向对象又能解决代码重用的问题——继承。我以前写了一个“狗”的类,属性有(变量):有毛、4条腿、有翘着的尾巴(耷拉着尾巴的那是狼)、鼻子很灵敏、喜欢吃肉骨头......方法有(函数):能跑、能闻、汪汪叫......如果它去抓耗子,人家叫它“多管闲事”。好了,狗这个类写好了。但在我实际的生活中,我家养的这条狗和我以前写的这个“狗类”非常相似,只有一点点的不同,就是我的这条狗,它是:卷毛而且长长的,鼻子小,嘴小......。于是,我派生一个新的类型,叫“哈巴狗类”在“狗类”的基础上,加上新的特性。好了,程序写完了,并且是重用了以前的正确的代码——这就是面向对象程序设计的好处。我的成功只是站在了巨人的肩膀上。当然,如果你使用VC的话,重用最多的代码就是MFC的类库。

六、组件(COM)程序设计
  有了面向对象程序设计方法,就彻底解决了代码重用的问题了吗?答案是:否!硬件越来越快,越来越小了,软件的规模却也越来越大了,集体合作越来越重要,代码重用又出现的新的问题。

我用C++写的类,不能被BASIC重用——不能夸语言;
你要干什么,想重用我的代码?不行,这样你就看见了我的设计思想——只能在源程序级别重用,不能在二进制级别(可执行代码及)重用;
我耗尽毕生的精力,写了一个包罗万象的类库,但没有人用。因为他们说:你这个太大了,我的程序只有1K,你却给我一个 10000MB 的库——MFC 的尴尬;
太好了,我终于找到了程序中的一个BUG,已经修改完成,而且是只改动了一个字节。接下来我要重新向我的用户分发新的版本,我的用户有......10万个——升级的非鲁棒性,不是我分发累死了,就是用户重新安装累死了。(鲁棒:robust。意为强壮性的,平顺的,顺滑的.....鬼知道是哪个不懂计算机的人翻译的这个词汇。)
我想写一个集大成的软件,这个软件的功能是我中有你,你中有我。既能实现文字编辑,又能实现电子表格计算,又能实现自动翻译,还能画图,还能实现数据库检索,还可以看电影.....只要用了我的这个软件,想要什么就有什么,我要强占整个软件的市场------OLE实现的重用功能,只要学会了COM,这些都不是问题了;
用户甲要求我的软件窗口上下分割,用户乙要求我的软件窗口左右分割......我需要在我的软件基础上,派生出100个类型,可怎么办呀?将来怎么维护呀?------在脚本的支持下,实现同一程序的的灵活配置而重用,问题迎刃而解了;
我是个老板,你知道我有多痛苦吗?我手下的员工向我提出加工资的要求,我不得不答应呀。因为如果这个员工跳槽了,他的代码要维护起来有多难!!!——现在好啦,我要求员工统统用组件写模块,想加工资?门都没有,威胁我要走?那你走吧,这个月的工资也不发了。反正用组件写的代码,我可以很容易地进行包容和聚合实现维护。(老板的福音,程序员的悲哀);
还有好多那,现在想不起来了......
  COM程序设计方法,就是解决以上问题的一个方式。有很多朋友觉得COM非常复杂难懂,不想学习了。你一定学习过程序设计的最基本的方法(非结构化设计:汇编、gwBasic......),然后,你又学习了结构化程序设计(C、Pascal......),然后,你又努力学习并熟练掌握了面向对象的程序设计方法(C++、Delphi、Java......),那么不要怕,要有信心去学习组件程序设计,它只是一个设计方法和思想,并且是目前较高级的方法,如果不掌握,就太可惜了。

学习了结构化程序设计,你就会“藐视”那些不遵守结构化设计思想而写出的代码;
学习了面向对象设计,你就会“嘲笑”那些为找BUG而晕头转向的程序员;
同样,学习了组件程序设计,你就会站在更高的层次看待程序设计。
七、结束语
  写程序的目的是什么?养家糊口、兴趣使然、我的事业......这些都对。但我要强调的是:写程序的目的是为了修改程序。在这个观点上,那么写注释、写文档、选择语言、选择结构......都是为这个服务的。本文从软件设计方法的进化角度来反复阐述这个观点,希望爱好者能有所体会和思考。
文中所讨论的技术和观点,适合于大多数情况下的程序设计,而对于特殊的应用的(驱动开发,嵌入式开发,网络通讯,实时视频......),这些领域中,由于硬件环境的限制和极限效率的要求,有些观点就不合适了,需要具体情况具体分析。另外就是对于程序设计的初学者,可以先不考虑这么多问题,以掌握基本技巧方法和思想为要。

欢迎大家批评指正。文中一些调侃的部分,也请勿对号入坐^_^。[/post]

九洲方除百尺冰,映秀又遭蛮牛耕。汽笛嘶鸣国旗半,哀伤尽处是重生。     -老K
治国就是治吏。礼义廉耻,国之四维。四维不张,国之不国。   -毛泽东
2005-02-19 13:33
Knocker
Rank: 8Rank: 8
等 级:贵宾
威 望:47
帖 子:10454
专家分:603
注 册:2004-6-1
收藏
得分:0 
看清楚,“作者:杨老师”不是knocker,个人认为文章观点很好,虽然有些走极端。

九洲方除百尺冰,映秀又遭蛮牛耕。汽笛嘶鸣国旗半,哀伤尽处是重生。     -老K
治国就是治吏。礼义廉耻,国之四维。四维不张,国之不国。   -毛泽东
2005-02-19 13:38
Knocker
Rank: 8Rank: 8
等 级:贵宾
威 望:47
帖 子:10454
专家分:603
注 册:2004-6-1
收藏
得分:0 
[QUOTE]我是个老板,你知道我有多痛苦吗?我手下的员工向我提出加工资的要求,我不得不答应呀。因为如果这个员工跳槽了,他的代码要维护起来有多难!!!——现在好啦,我要求员工统统用组件写模块,想加工资?门都没有,威胁我要走?那你走吧,这个月的工资也不发了。反正用组件写的代码,我可以很容易地进行包容和聚合实现维护。(老板的福音,程序员的悲哀);
还有好多那,现在想不起来了...... [/QUOTE]



嘿嘿,俺写的代码谁也不想维护,所以,老板不敢炒俺........

九洲方除百尺冰,映秀又遭蛮牛耕。汽笛嘶鸣国旗半,哀伤尽处是重生。     -老K
治国就是治吏。礼义廉耻,国之四维。四维不张,国之不国。   -毛泽东
2005-02-19 13:40
Knocker
Rank: 8Rank: 8
等 级:贵宾
威 望:47
帖 子:10454
专家分:603
注 册:2004-6-1
收藏
得分:0 
此文的所有中国字及外国字本人都认识都会写,但是没那闲工夫

九洲方除百尺冰,映秀又遭蛮牛耕。汽笛嘶鸣国旗半,哀伤尽处是重生。     -老K
治国就是治吏。礼义廉耻,国之四维。四维不张,国之不国。   -毛泽东
2005-02-26 21:44
Knocker
Rank: 8Rank: 8
等 级:贵宾
威 望:47
帖 子:10454
专家分:603
注 册:2004-6-1
收藏
得分:0 
再转一个kevinliu!编写优质无错C程序秘诀!《经验谈》! 这里我将陆续给大家载出我在以前学习和编写c代码时遇到的问题和解决方法、学习的心得,有些是经过查询一些书籍和资料后提供给大家!

首先,当发现错误时,要不断就以下两个问题追问自己的结果: 1、怎样才能自动地查出这个错误? 2、怎样才能避免这个错误?

关于错误: 错误可以分为两类: 1、开发某一功能时产生的错误。 2、程序员认为该功能已经开发完成之后仍然遗留在代码中的错误。

第一种错误好解决,可以把编译器可以设置的警告等级开关打开,以及语法检查来排除;逻辑错误也可以使用跟踪手段来排除。跟踪逻辑错误就相对麻烦一些,要消除这些麻烦就要养成一个好的编程习惯和方法。 第二种错误时非常隐蔽的,需要长期的实践和经验在其中,还要对c语言具有深刻的了解才能够提高上来,这里就是要告诉大家一些这样的事情,通过代码解说来阐明具体事实。

(第一个问题) 考虑自己所用的语言和编程环境?使空语句明显化! 充分利用语言的特性和编程环境,把所有环境下的调试报错等级开关都打开,注意使用语言的保留字,例如下面的两段程序对比:

/*复制一个不重叠的内存块*/ void *memcpy(void *pvto, void *pvfrom,size_t size) { byte *pbto = (byte *)pvto; byte *pbfrom = (byte *)pvfrom; while(size-- > 0); *pbto++ = *pbfrom++; return (pvto); }

从以上缩进格式可以看出,while后的分号肯定是一个错误。但编译器认为这是一个合法的语句,允许循环体为空语句。报警开关都打开时,大多编译器都能够报出这一错误。但需要用空语句时,最好实用null(大写)明确出来:

char *strcpy(char *pchto, char *pchfrom) { char *pchstart = pchto; while(*pchto++ = *pchfrom++) null;/*此处null大写*/ return (pchstart); }

这样,编译器编译程序接受显式的null语句,把隐式空语句自动地当做错误标出。

(第二个问题) 无意的赋值。 例如:

if(ch = '\t') expandtab();

有些编译器允许在程序&&和||表达式以及if、for和while中直接使用赋值的地方禁止简单赋值,如果以上五种情况将==偶然地键入为=号,就会报错。

while(*pchto++ = *pchfrom++) null;

编译程序就会产生警告信息,为了防止这种情况出现,可以这样做:

while((*pchto++ = *pchfrom++) != '\0') null;

这样做的结果由两个好处: 1、现在的编译器不会为这种冗余的比较产生额外的代码和开销,可以将其优化掉。 2、可以少冒风险,尽管以上两种都合法,但这是更安全的用法。

(第三个问题) 参数错误: 例如: fprintf(stderr, "unable to open file %s.\n",filename); ...... fputc(stderr,'\n'); 这个程序看上去好像没有问题,实际上fputc的参数顺序错了。幸好ansi c提供了函数原型,在编译时自动查出这些错误。

ansi c标准要求每个库函数都必须有原型,stdio.h中可以查到: int fputc(int c, file *stream); 如果在程序文件头里给出了原型,这类错误就可以检查出。

ansi c虽然要求标准库函数必须有原型,但并不要求用户编写的函数也必须有原型。可以有,也可以没有。有些程序员经常抱怨对函数的原型进行维护,如果没有原型,就不得不依靠传统的测试方法来查出程序中的调用错误,大家可以扪心自问:究竟哪个更重要? 利用原型可以生成质量更好的代码。ansi c标准使得编译程序可以根据原型信息进行相应的优化。

有这样的名言: 投资者与赌徒之间的区别在于投资者利用每一次机会,无论它是多么小,去争取利益;而赌徒则只靠运气。我们应该将这一概念同样应用于编程活动。 把所有的警告开关都打开,除非有极好的理由才不这样做!

(原则一) 如果有单元测试,就进行单元测试。 你认识那个程序员宁愿花费时间去跟踪排错,而不是编写新的代码?肯定有这样的程序员,但我至今还没有见到一个。 当你写程序时,要在心中时刻牢记着假想编译程序这一概念,这样就可以毫不费力或者直费很少力气利用每个机会抓住错误。 如果想要快速容易地发现错误,就要利用工具的相应特性对错误进行定位。错误定位的越早,就能够越早地投身于更有趣的工作。 努力减少程序员查错的技巧。可以选择编译程序的环境来实现。高级的编码方法虽然可以查出或减少错误,但它们也要求程序要有较多的技巧,因为程序员必须学习这些高级的编码方法。

(原则二) 自己设计并使用断言。 利用编译器自动查错固然好,但实际上只是很少一部分。如果排除掉了程序中的所有错误,大部分时间程序会正确工作。

看一下下列代码: strcopy = memcpy(malloc(length),str,length);

该语句在多数情况下会工作的很好,除非malloc的调用产生失败。一旦产生,就会给memcpy返回一个null指针,而memcpy处理不了null指针,这样的错误产生,如果在交付用户之前将导致程序的瘫痪。但如果交付了用户,那用户就一定“走运”了。

解决方法: 对null指针进行检查,如果为null,就给出一条错误信息,并终止memcpy执行。ee

/*拷贝不重叠的内存块*/ void memcpy(void *pvto, void *pvfrom, size_t size) { void *pbto = (byte *)pvto; void *pbfrom = (byte *)pvfrom; if(pvto == null || pvfrom == null) { fprintf(stderr, "bad args in memcpy!\n"); abort(); } while(size-- > 0) *pbto++ = *pbfrom++; return(pvto); }

只要调用时错用了null指针,这个函数就会查出来。但测试的代码增加了一倍,降低了执行速度,这样“越治病越糟”,还有没有更好的方法? 有,利用c的预处理程序! 这样就会保存两个版本。一个整洁快速,用于交付用户;另一个臃肿缓慢(包含了额外的检查),用于调试。这样就要同时维护同一个程序的两个版本,利用c的预处理程序有条件地包含相应的部分。

例如:只有定义了debug时,才对应null指针测试。 void memcpy(void *pvto, void *pvfrom, size_t size) { void *pbto = (byte *)pvto; void *pbfrom = (byte *)pvfrom;

#ifdef debug if(pvto == null || pvfrom == null) { fprintf(stderr, "bad args in memcpy!\n"); abort(); } #endif

while(size-- > 0) *pbto++ = *pbfrom++; return(pvto); }

这样,调试编译时开放debug,进行测试程序和找错;交付用户时,关闭debug后进行编译,封装之后交给经销商。 这种方法的关键是保证调试代码不在最终产品中出现。

那么还有没有比以上两种更好的方法,有!下次再讲。

待续。。。


九洲方除百尺冰,映秀又遭蛮牛耕。汽笛嘶鸣国旗半,哀伤尽处是重生。     -老K
治国就是治吏。礼义廉耻,国之四维。四维不张,国之不国。   -毛泽东
2005-03-15 11:34
Knocker
Rank: 8Rank: 8
等 级:贵宾
威 望:47
帖 子:10454
专家分:603
注 册:2004-6-1
收藏
得分:0 
(准则二续) 利用断言进行补救。

实际上,memcpy中的调试代码编的非常蹩脚,喧宾夺主。他能产生好的效果,这无疑,但许多程序员不会让他这样存在的,聪明的程序员会让调试代码隐藏在断言assert中。 assert是个宏,定义在头文件assert.h中,每个编译器都自带。前面的程序完全可以使用assert来处理,看一下下面代码,把7行减为了1行代码。

void memcpy(void *pvTo, void *pvFrom, size_t size) { void *pbTo = (byte *)pvTo; void *pbFrom = (byte *)pvFrom;

assert(pvTo != NULL && pvFrom != NULL);

while(size-- > 0) *pbTo++ = *pbFrom++; return(pvTo); }

这里要强调的是:assert是个只有定义了DEBUG才起作用的宏,如果其参数的计算结果为假,就中止调用程序的执行。

当然程序编制也可以编制自己的断言宏,但要注意不要和assert冲突,因为assert是全局的。举个例子:

先定义宏ASSERT: #ifdef DEBUG void _Assert(char *, unsigned); /*自定义断言函数的函数原型*/ #define ASSERT(f) if(f) NULL; esle _Assert(_FILE_ , _LINE_ ); #else #define ASSERT(f) NULL #endif

从上述我们可以看到,如果定义了DEBUG,ASSERT将扩展为一个if语句。 当ASSERT失败时,他就是用预处理程序根据 _FILE_ 和 _LINE_ 所提供的文件名和行号参数调用 _Assert。 _Assert在标准错误输出设备stderr上打印一条错误信息,然后中止:

void _Assert(char *strFile, unsigned uLine) { fflush(stdout); fprintf(stderr, "\nAssertion failed: %s, line %u\n", strFile, uLine); fflush(stderr); abort(); }

程序中的相关函数,大家可以查阅头文件帮助来了解,这里就不在详述了。 下一讲:使用断言对函数参数确认。 (准则二 续二) 使用断言对函数参数确认。

掌握原则为:“无定义”就意味着“要避开”。

读一下ANSI C的memcpy函数的定义,最后一行这样说:“如果在存储空间相互重叠的对象之间进行了拷贝,其结果无意义。”那么当使用相互重叠的内存块调用该函数时,实际上在做一个编译程序(包括同一编译程序的不同版本),结果可能也不同的荒唐的假定。 对于程序员来说,无定义的特性就相当于非法的特性,因此要利用断言对其进行检查。

通过增加一个验证两个内存块决不重叠的断言,可以把memcpy加强: void memcpy(void *pvTo, void *pvFrom, size_t size) { void *pbTo = (byte *)pvTo; void *pbFrom = (byte *)pvFrom;

ASSERT(pvTo != NULL && pvFrom != NULL); ASSERT(pbTo >= pbFrom+size || pbFrom >= pbTo+size);

while(size-- > 0) *pbTo++ = *pbFrom++; return(pvTo); } 从今以后,在编程时,要经常停下来看看程序中有没有使用了无定义的特性。如果使用了,就要把它从相应的设计中去掉,或者在程序中包含相应的断言,以便在使用了无定义的特性时,能够向程序员发出通报。 不要让这种事情发生在你的身上: 在1988年晚些时候,Microsoft公司的摇钱树DOS版的Word被推迟了三个月,明显地影响了公司的销售。这件事情的重要原因,是整整六个月来开发小组成员一直认为他们随时都可以交出Word。 问题出在Word小组要用到的一个关键部分是由公司另一个小组负责开发的。这个小组一直告诉Word小组他们的代码马上就可以完成,而且小组的成员对此确信不疑。但他们没有意识到在他们的代码中充斥了错误。 这个小组的代码与Word代码之间一个明显的区别是Word代码从过去到现在一直都是用断言和调试代码,而他们的代码却几乎没有使用断言。因此,其程序员没有什么好办法可以确定其代码的实际错误情况。错误只能慢慢地暴露出来。如果他们在代码中使用了断言,这些错误本该在几个月前就被检查出来。 (须注意问题一)

前面所述的做法,为其他的程序员提供代码库(或操作系统——例如各个厂家的编译器)时显得特别重要。如果为他人提供过类似的库(或者自己使用自己以前编过的库时,或者使用了不同厂家提供的库时),就应该知道当程序员试图得到所需要的结果时,就会利用各种各样的无定义特性。更大的挑战在于改进后新库的发行,因为尽管新库与老库完全兼容,但总有半数的应用程序在试图使用新库时会产生瘫痪现象。问题在于新库在其“无定义的特性”方面,与老库并不100%兼容。 明白了这些,在编程时,就要考虑程序的移植性、兼容性、容错性、安全性、可发行性、商品性等等方面。而不是说在一个编程环境下能够实现功能就万事大吉了。程序员的道路不知是停留在编程的语法学习、技巧、实现功能上。要全面、全方位考虑所编制的程序的有可能造成的后果。 各个厂家的编译器都有所不同,一个厂家的编译器版本不同时,特性也不同。要想很好的编程,这些都是需要了解的,去尽量了解这些特性,才能真正学到编程。才能提高编程效率。有时会出现这样的情况。看人家的代码,感觉到非常傻,自以为很聪明,实际上是自己错误,因为人家考虑的更加广泛,考虑的更多,实际上那样的代码特别具有可移植性和容错性。只是自己的思想受到了局限,只从一个角度来看问题造成的。劝告大家:千万不要夜郎自大!


九洲方除百尺冰,映秀又遭蛮牛耕。汽笛嘶鸣国旗半,哀伤尽处是重生。     -老K
治国就是治吏。礼义廉耻,国之四维。四维不张,国之不国。   -毛泽东
2005-03-15 11:37
快速回复:[新增]程序设计方法及编写优质无错C程序秘诀
数据加载中...
 
   



关于我们 | 广告合作 | 编程中国 | 清除Cookies | TOP | 手机版

编程中国 版权所有,并保留所有权利。
Powered by Discuz, Processed in 0.027303 second(s), 8 queries.
Copyright©2004-2024, BCCN.NET, All Rights Reserved