为什么会出现内存泄漏,如何修复?

5
unit Unit7;

interface

uses Classes;

type
  TListener = class(TThread)
    procedure Execute; override;
  end;

  TMyClass = class
    o1,o2: Tobject;
    procedure FreeMyObject(var obj: TObject);
    constructor Create;
    destructor Destroy; override;
  end;

implementation

uses Windows, SysUtils;

var l: TListener;
    my: TMyClass;

procedure TListener.Execute;
var msg:TMsg;
begin
  while(GetMessage(msg, Cardinal(-1), 0, 0)) do
    if(msg.message=6) then begin
      TMyClass(msg.wParam).FreeMyObject(TObject(msg.lParam));
      Exit;
    end;
end;

constructor TMyClass.Create;
begin
  inherited;
  o1:=TObject.Create;
  o2:=Tobject.Create; // Invalid pointer operation => mem leak
end;

destructor TMyClass.Destroy;
begin
  if(Assigned(o1)) then o1.Free;
  if(Assigned(o2)) then o2.Free;
  inherited;
end;

procedure TMyClass.FreeMyObject(var obj: TObject);
begin
  FreeAndNil(obj);
end;

initialization
  l:= TListener.Create();
  my:=TMyClass.Create;

  sleep(1000); //make sure the message loop is set
  PostThreadMessage(l.ThreadID, 6, Integer(my), Integer(my.o2));
finalization
  l.Free;
  my.Free;
end.

我使用消息处理程序来说明我的问题,以便您能够理解。实际设计要复杂得多。函数'FreeMyObject'实际上是使用多态范例来释放和创建实例,但这里不需要。我只想指出设计应该保持不变。
现在的问题和难题是 - 为什么会发生这种情况,以及如何解决它?似乎'if Assigned(o2)'不适用于它。
我所想的是:向my.o2发送一个指针将释放和nil o2,我试图这样做,但我无法在消息处理程序中从指针转换为对象,不知道为什么。
有人可以帮忙吗?谢谢

“Invalid pointer operation => mem leak” 这句话真的适用于 “o2:=Tobject.Create;” 这行代码吗? - mjn
@mjn。它可能属于析构函数中的相应行。 :) - GolezTrol
最好使用大于WM_APP的值作为消息号。6是WM_ACTIVATE,可能会引起麻烦。 - GolezTrol
@daemon_x 我相当确定真正的代码是完全不同的。我认为批评这段代码没有多大意义,因为它显然是从一个真实的程序中割裂出来,用于在这里发布。 - David Heffernan
3
这段话的意思是,if Assigned(x) then x.Free;这个代码模式是不必要的。因为Free方法已经会检查x是否存在,所以只需要简单地使用x.Free;即可。 - Rudy Velthuis
显示剩余2条评论
3个回答

6

您需要两次释放o2。一次是由于消息的结果,另一次是由析构函数引起的。

当您调用FreeMyObject时,您认为您将o2设置为nil,但实际上并非如此。您实际上是将msg.lParam设置为0。

o2是一个保存对象引用的变量。您正在传递o2的值,并且当您按值传递时,您无法修改其值的变量。因此,您需要传递对o2的引用。为此,您需要添加一个额外的间接级别,并传递指向o2的指针,如下所示:

if(msg.message=6) then begin
  FreeAndNil(PObject(msg.lParam)^);
  Exit;
end;

...

PostThreadMessage(l.ThreadID, 6, 0, LPARAM(@my.o2));

你不需要使用FreeMyObject,直接调用FreeAndNil即可。而且在消息中不需要传递实例。
希望你的真实代码没有这么奇怪!;-)

1
在PostThreadMessage中推荐的转换方式是反向和未来证明(64位),它是PostThreadMessage(l.ThreadID,6,0,LParam(@my.o2))。 - LU RD
@LU RD 谢谢,你说得很对,我会更新的。几个月前我的代码也是这么写的,我应该知道的! - David Heffernan
是的,我已经尝试过这个了,但失败了。问题出在别的地方。让 B=class(A)。在 A 中发送消息时,参数应该为 lparam=@Self - 这应该是有效的。但实际上不行。@Self 指向的地址并不是引用 A 的地址。很遗憾,我花了两天时间才发现这一点。感谢你的努力,虽然没用,还是接受了! - netboy
@netboy 是的,@Self 和你所问的问题有很大不同。但基本思想是相同的。你需要传递一个变量的引用,而不是它的值。Self 不是你应该传递的内容,而可能是某个对象的字段。 - David Heffernan
但是这条消息是从o2发送的(它是一个类,不仅仅是TOBject)。除非使用Self,否则无法获取其地址。或者我错了吗? - netboy
@netboy,我对这些评论有点困惑。但是,如果你需要在一个变量(局部变量、实例字段等)上调用 FreeAndNil(),那么你只需要传递该变量的地址。这就是所需的全部内容。 - David Heffernan

3
如果您想要FreeAndNil一个只发送对象引用的对象,Integer(my.o2)是不够的 - 您需要使用Integer(@my.o2)。您还应该对代码进行相应的更改。
由于您的代码很难调试,我已经编写了一个简单的演示以给出必要代码更改的想法:
type
  PObject = ^TObject;

procedure FreeObj(PObj: PObject);
var
  Temp: TObject;

begin
  Temp:= PObj^;
  PObj^:= nil;
  Temp.Free;
end;

procedure TForm17.Button1Click(Sender: TObject);
var
  Obj: TList;
  PObj: PObject;

begin
  Obj:= TList.Create;
  PObj:= @Obj;
  Assert(Obj <> nil);
  FreeObj(PObj);
  Assert(Obj = nil);
end;

1

这里是发生的情况:

程序开始。初始化运行并向线程发送消息,该线程在传递的引用上调用FreeAndNil。这将把传递的引用设置为nil,但它不会将保存o2的对象字段设置为nil。那是另一个引用。

然后在析构函数中,由于该字段不是nil,它再次尝试释放它,从而导致双重释放错误(无效指针操作异常)。由于您在析构函数中引发了异常,因此TMyClass永远不会被销毁,并且您会从中获得内存泄漏。

如果您想正确执行此操作,请将某种类型的标识符传递给FreeMyObject,而不是引用。例如整数2或字符串o2。然后,FreeMyObject使用此值查找应调用FreeAndNil的内容。(如果您使用Delphi 2010或更高版本,则可以使用RTTI轻松完成此操作。)这需要一些额外的工作,但它将修复您看到的错误。


@David:你回答中潜在的封装违规风险吓到我了... :P - Mason Wheeler
这不是真正的代码吧?传递一个字符串,由于它没有得到转换而无法起作用,然后使用 RTTI 类作为良好的封装?! - David Heffernan
你对消息循环的评论显然是不准确的。在此代码中,消息循环在线程中运行,因此在初始化运行时也会开始。 - David Heffernan
2
@David,哦,我明白了。 这是在谈论其他线程的消息传递。 那就算了吧。 将其删除... - Mason Wheeler

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接