造成DELPHI程序的稳定性原因及解决方法

来源:互联网 发布:ant编译java 编辑:程序博客网 时间:2024/05/21 17:55

软件质量是一个产品的生命线,也是关乎软件开发者的幸福关键所在,每天有很多程序员都在因为软件质量而通宵达旦的加班,经常遇到的情况是刚发布的程序不停的发布补丁包。软件质量就像一个噩梦一样,不停的在后面追赶着程序员,让他们疲于奔命,甚至于在程序员中流传着一句话:“生命不息,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”,这个就是因为单击btn2的时候由于GetTrue返回的是真,所以第二句不执行,但是btn1由于还要进行Variant到Boolean类型的转换,因此肯定会执行。

四、函数递归
如果存在递归函数,就需要特别注意,是否会正常退出函数执行,如果一直执行下去,会把程序调用堆栈全部吃完,导致程序异常终止,如下例:只要一点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地址和调用堆栈找到问题点。

阅读全文
1 0
原创粉丝点击