【转】 DELPHI编写服务程序总结三--代码质量

来源:互联网 发布:淘宝联系售后 编辑:程序博客网 时间:2024/06/05 08:56

一、提高DELPHI程序的稳定性
软件质量是一个产品的生命线,也是关乎软件开发者的幸福关键所在,每天有很多程序员都在因为软件质量而通宵达旦的加班,经常遇到的情况是刚发布的程序不停的发布补丁包。软件质量就像一个噩梦一样,不停的在后面追赶着程序员,让他们疲于奔命,甚至于在程序员中流传着一句话:“生命不息,BUG不止”。
今天我们要探究的不是哪些可以重现的BUG,我们把哪些可以重现的BUG不定义为BUG,只有哪些不可重现的BUG,会让你茶饭不思、坐立不安。我曾在一家公司开发服务器软件,结果因为程序不稳定,而且都是一些不可重现的错误,导致我们需要不停的派人盯着服务器运行。不稳定就像一个恶鬼一样终日萦绕在我们心头,领导的不停催促,客户的不停投诉,让我们项目组个个疲于奔命,叫苦连天。我在查了无数个不可重现的BUG发现,主要是由于以下八种原因引起的: 
1. 变量没有初始化;
2. 函数返回值没有初始化;
3. 编译优化导致的错误;
4. 函数递归;
5. 消息重入;
6. 野指针;
7. 内存泄漏;
8. 并发;
你会发现都是一些细小问题,因此程序员在日常开发中一定要养成好的习惯。
二、变量没有初始化
DELPHI默认初始化的变量是:全局变量、类成员,其它在函数体的变量都不会初始化,因此一些用于判断或者循环的变量一定要记得初始化,另外枚举类型、申请的内存都需要初始化,PCHAR一定要在末尾加#0。例如:下面的返回结果有可能会出现乱码。
function TempPath: string;
begin
SetLength(Result, GetTempPath(0, PChar(Result)));
GetTempPath(Length(Result), PChar(Result));
Result := PChar(Result);
end;
正确的写法应该
function TempPath: string;
begin
SetLength(Result, GetTempPath(0, PChar(Result)));
ZeroMemory(PChar(Result), Length(Result));
GetTempPath(Length(Result), PChar(Result));
Result := PChar(Result);
end;
这个程序就是典型的在申请内存的时候,没有对PCHAR进行初始化,因此末尾有可能是随机值,但是通过ZeroMemory就把末尾赋#0。
三、函数返回值没有初始化
在DELPHI中退出函数是使用Exit函数的,有很多函数在退出的时候,没有对函数返回值初始化,那么函数的返回值返回就是一个随机值,对程序运行造成不可重现错误。例如:下面程序的执行结果会让你大吃一惊。
procedure NotInitResult;
var 
i: Integer;
function GetString(AValue: Integer): string;
begin
    if AValue = 0 then
      Result := 'True';
end;
begin
for i := -1 to 1 do
begin
    ShowMessage(GetString(i));
end;
end; 
你看到的运行结果是:‘’、‘True’、‘True’,正确的写法应该是:
procedure NotInitResult;
var 
i: Integer;
function GetString(AValue: Integer): string;
begin
    if AValue = 0 then
      Result := 'True'
     else
       Result := ‘’;
end;
begin
for i := -1 to 1 do
begin
    ShowMessage(GetString(i));
end;
end; 
因此针对if或者Case语句一定要赋初始值,上面的函数的写法也可以写为:
function GetString(AValue: Integer): string;
begin
Result := ‘’;
if AValue = 0 then
    Result := 'True';
end;
function GetString(AValue: Integer): string;
begin
case AValue of
    0: Result := ‘True’;
    else Result := ‘’;
end;
end;
四、编译优化导致的错误
现在的编译器在编译代码的时候会优化掉一些可以不执行的代码,例如:布尔类型优化是最常见的一种,下面的例子能很好的说明这个问题。
procedure TForm1.btn1Click(Sender: TObject);
var 
s: string;
begin
if GetTrue or GetValue1(s) then
    ShowMessage('Hello ' + s);
end; 
procedure TForm1.btn2Click(Sender: TObject);
var 
s: string;
begin
if GetTrue or GetValue2(s) then
    ShowMessage('Hello ' + s);
end; 
function TForm1.GetTrue: Boolean;
begin
Result := True;
end;
function TForm1.GetValue1(var s: string): Variant;
begin
Result := True;
s := 'World';     
end;
function TForm1.GetValue2(var s: string): Boolean;
begin
Result := True;
s := 'World';
end; 
你会发现单击btn1时出现的结果是:“Hello Word”,但是单击btn2的时候是:“Hello”

 

五、函数递归
如果存在递归函数,就需要特别注意,是否会正常退出函数执行,如果一直执行下去,会把程序调用堆栈全部吃完,导致程序异常终止,如下例:只要一点btn1,程序就会无声无息死掉,而且没有LOG,这类代码在以服务方式运行需要特别注意,因为你的服务是无人值守的情况下运行的,如果出现这种情况,你的服务会直接退出,而且没有任何提示,对于查找问题无从下手。
procedure TForm1.btn1Click(Sender: TObject);
procedure Recursive;
begin
    Recursive;
end;
begin
Recursive;
end;
六、消息重入
消息重入的概念是:有一个消息执行过程还没有执行,相同的一个消息又进入相同的函数处理。消息重入很大原因是在很多软件中调用Application.ProcessMessage来更新界面,如果是一个操作需要很长的时间,可以改为线程来执行,或者不调用Application.ProcessMessage函数。例如:下面的函数就很容易导致消息重入。
procedure TForm1.btn1Click(Sender: TObject);
var
i: Integer;
begin
for i := 0 to 10000000 do
begin
    Application.ProcessMessages;
end;
end;
如果必须要用Application.ProcessMessage来更新界面,你应该确保在函数执行过程中,这个消息不会第二次投递,如这个例子你可以通过把btn1的状态禁用来防止消息重入,正确的写法是:
procedure TForm1.btn1Click(Sender: TObject);
var
i: Integer;
begin
btn1.Enabled := False;
for i := 0 to 10000000 do
begin
    Application.ProcessMessages;
end;
btn1.Enabled := True;
end;
另外在发送消息的时候,也需要特别注意SendMessage和PostMessage的区别,SendMessage是发送等待消息处理完成再返回,PostMessage是投递到消息缓冲池排队,立即返回(这时消息可能没有处理),消息需要等到轮到它的时候再处理。
七、野指针
野指针在编译时候是无法检测的,只有在运行时候才会出现,出现野指针最常见的错误就是Access violation错误(简称AV错误),出现这种错误是你指向的物理内存不可用。出现野指针主要是由于以下四种引起:1、指针变量没有初始化;2、指针被Free或Dispose之后再次使用;指针操作超越了变量的范围;4、取string的地址,没有判断string是否已经分配内存。
代码在判断指针是否是空指针是通过判断指针的值是否介于0x00000000和0x0000FFFF之间,如果在这之间用if语句是可以判断,如果不介于这之间,则认为指针是有效的。因此指针在申请之后或者释放之后,指向的地址是随机值,因此用if语句是无法判断。另外在DELPHI中,你把指针置为nil,翻译成汇编代码就是异或一下,可以打开CPU窗口查看,如:
Fm := nil;生成的汇编是:xor eax eax,即把指针置为0x00000000。

八、内存泄漏
内存泄漏指的是软件在运行过程中对于申请的内存空间没有释放,导致内存占用越来越大,最后程序异常崩溃,而且此时也不会留下任何痕迹,没有任何系统日志可查。内存泄漏也分为两种,一种是程序一起动,然后占用了内存,不会随着程序运行增长;一种是随着程序运行不停增长的;如果是第一种可以放过,对二种一定要仔细检查,检查工具推荐用FastMM,并且把DELPHI的项目属性Compiler->Use Debug DCUs和Linker->Map file->Detailed选中,这样FastMM就可以把申请内存的调用堆栈和MAP地址打出来,非常利于查找内存泄漏。查找内存泄漏一般可以从以下几个方面考虑:
1. 使用Dispose释放内存的时候要加上定义信息,如果不加定义信息,对于一些指针或者string释放不了,对于结构体内部有指针的应先释放内部指针;
2. 使用FreeMem或FreeMemory释放内存的时候,可以不加大小信息,这是因为DELPHI内存管理器内部知道指针大小信息;
3. Override函数一定要inherited来释放父类申请的内存;
4. 申请的内存要确保释放,可以用Try … finally … end来确保内存的释放,但是应杜绝这种代码风格try …申请内存…finally …释放内存… end;
5. 系统内核对象要确保关闭;
6. 申请的指针如果在某些情况下分配空间,要记得初始化为nil,释放的时候要判断是否为空,因为释放空指针也会导致内存泄漏;
7. 另外PostMessage也有可能导致内存泄漏,这种情况是通过PostMessage发送结构体,释放内存放在消息处理函数中,这时如果频繁的调用PostMessage,消息处理循环忙不过来,就会丢掉一些消息,造成内存泄漏,默认的Windows消息队列长度是4000,如果说消息队列有4000个,你这时再用PostMessage投递消息,就会被丢掉,造成申请的结构体无法释放,造成内存泄漏;
九、并发
如果程序涉及多线程,而且线程之间有协作关系,如果这时线程挂死了,就要查线程同步,一般这类问题比较难查,而且需要对代码执行流程非常了解,属于比较难以处理的一类问题。可以借助一些三方工具,比如“procexp.exe”就是一个非常优秀的工具,用他可以看到每个线程的状态,如果一个线程停在哪不动,你就可以通过MAP地址和调用堆栈找到问题点。如Excel的线程状态如下图:



十、一些有效的建议
针对以上的这些问题,我们在日常的开发中,应该注意哪些问题呢,下面是我给出的一些建议:
1.探索需求,需求理解越深写出代码的质量、架构就越轻巧,可读性和维护性大大提高;
2. 测试驱动开发;
3. 良好的代码风格,良好的编码习惯对于软件质量有非常大的提高;
4. 变量(指针、数组)被创建之后应及时把他们初始化;
5. 检查变量的初始值、缺省值错误,或者精度不够;
6. 类型转换,一定要善用as和is;
7. 检查变量上溢或下溢,数组越界;
8. 检查I/O错误,I/O不是总返回真的;
9. 数据结构够用就好,不要设计面面俱到、非常灵活的数据结构;
10. 差劲的代码,不要想着改改又可用了,应当重新编写,因为极有可能导致按下葫芦浮起瓢;
11. 对程序编译出现的每一个告警,都认真对待,要编写无警告的代码;
12. 对于不需要修改的参数带上const,不但可以提高效率,而且可以增强安全;

原创粉丝点击
热门问题 老师的惩罚 人脸识别 我在镇武司摸鱼那些年 重生之率土为王 我在大康的咸鱼生活 盘龙之生命进化 天生仙种 凡人之先天五行 春回大明朝 姑娘不必设防,我是瞎子 婆婆的娘家人从我家住怎么办 八个月宝宝断奶后不吃奶粉怎么办 吃母乳的宝宝不吸奶嘴怎么办 宝宝吸了奶嘴不吸母乳怎么办 婴儿吃了奶嘴不吸母乳怎么办 十一个月宝宝断奶不喝奶粉怎么办 孩子三门成绩全不及格家长该怎么办 宝宝快十个月了还不会爬怎么办 小孩写字老把手向里扭曲怎么办 孩子该上四年级了数学差的很怎么办 孩子上三年级了数学成绩好差怎么办 三年级数学老考70-80分怎么办 叛逆期的孩子用死来威胁家长怎么办 叛逆期的孩子抽烟喝酒家长该怎么办 大学遇到不好的老师加课怎么办 两岁的宝宝脾气古怪不听话怎么办 16个月宝宝不听话脾气大怎么办 如果你很害怕去面对一件事怎么办 孩子上幼儿园哭老师不理孩子怎么办 发现幼儿园给孩子吃药片该怎么办 做老师的打学生被家长投诉怎么办 学生认为老师向家长打报告怎么办 老师发打12分的试卷给家长怎么办 孩子在学校顶撞老师不让上学怎么办 被老师骂了不敢去学校怎么办 孩子不爱去幼儿园 总是哭怎么办呢 孩子在幼儿园被老师罚家长该怎么办 孩子不喜欢幼儿园里的体能课怎么办 初中叛逆期的孩子怎么办老师做法 二年级孩子不受老师待见怎么办 孩子该上初中了没学籍怎么办 四个多月的婴儿不喜欢看人怎么办 2个月婴儿不喜欢吃奶粉怎么办 想学习但是又学不进去怎么办 在省外读书 回来读高中学籍怎么办 一岁宝宝这几天不爱吃饭怎么办 2岁的宝宝吃多了怎么办 小孩吃了退烧药吐了怎么办 牙齿与牙齿之间有洞喜欢塞牙怎么办 胃有点烧心天天没食欲不饿怎么办 1岁半宝宝不吃饭光喝奶粉怎么办