存储Delphi接口引用时出现奇怪的AV错误

6

在下面的代码中,我遇到了意外的访问冲突错误:

program Project65;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  SysUtils;

type
  ITest = interface
  end;

  TTest = class(TInterfacedObject, ITest)
  end;

var
  p: ^ITest;

begin
  GetMem(p, SizeOf(ITest)); 
  p^ := TTest.Create; // AV here
  try
  finally
    p^ := nil;
    FreeMem(p);
  end;
end.

我知道接口应该有不同的使用方式。然而,我正在处理一个使用这种方法的旧代码库。我很惊讶地发现,仅仅保留 SizeOf(ITest) 的内存空间并不能满足放置 ITest 的需求。

有趣的是,如果我将第一行修改为

GetMem(p, 21);

当AV(访问冲突)发生时,程序会崩溃。在这里,我们需要预留比20字节更多的空间才能避免发生AV(20字节或更少的空间将导致失败)。为什么要预留21个字节而不是ITest的大小(4个字节)?请注意,我使用Delphi XE2 Update 4 + HotFix。


你的代码看起来很奇怪。为什么需要"^ITest"和GetMem/FreeMem对?TTest是TInterfacedObject的后代,所以p应该只是一个ITest。它是引用计数的,所以当它超出范围时会自动销毁。没有必要使用GetMem/FreeMem。 - kol
1
这是处理接口的完全错误方式。你能解释一下你希望实现什么,这样也许有人可以指出更好的方向吗? - Ken White
  1. AV = 访问冲突
  2. 我知道这不应该这样使用。我正在尝试理解一份遗留代码,而这是一个常见的做法。可以请你回答问题,而不要评论这段代码有多糟糕吗?
- RM.
@Ken,我认为你的分析是错误的。有时候会将接口分配给指针以获得弱引用。在这个例子中,引用计数正常工作。当接口分配给p^时,它会增加,当p^设置为nil时,它会减少(此时对象被销毁)。 - RM.
我不同意,但我会把这个问题留给那些能比我更好地解释的人。 :-) 很高兴你在处理那个遗留代码而不是我。祝好运。 - Ken White
你好 Ken,你删掉了我要回答的评论! - RM.
1个回答

25

你实际编写的代码所执行的逻辑如下:

var
  p: ^ITest;
begin
  GetMem(p, SizeOf(ITest));
  if p^ <> nil then p^._Release; // <-- AV here
  PInteger(p)^ := ITest(TTest.Create);
  p^._AddRef;
  ...
  if p^ <> nil then p^._Release;
  PInteger(p)^ := 0;
  FreeMem(p);
end;

GetMem()不能保证分配的内存被清零。当你将新的对象实例赋给接口变量时,如果这些字节不是零,RTL会认为已经存在一个接口引用,并尝试调用它的_Release()方法,导致AV错误,因为它没有支持真正的对象实例。您需要事先将分配的字节清零,然后RTL将看到一个nil接口引用,不再尝试调用其_Release()方法:

program Project65;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  SysUtils;

type
  ITest = interface
  end;

  TTest = class(TInterfacedObject, ITest)
  end;

var              
  p: ^ITest;              

begin              
  GetMem(p, SizeOf(ITest));               
  try
    FillChar(p^, SizeOf(ITest), #0); // <-- add this!
    p^ := TTest.Create; // <-- no more AV
    try
      ...
    finally
      p^ := nil;
    end;
  finally
    FreeMem(p);
  end;
end.

6
可以使用AllocMem代替GetMem+FreeMem。 - Andreas Hausladen
更正:我的意思是GetMem+FillChar。FreeMem对于AllocMem仍然是必要的。 - Andreas Hausladen
@Craig:确实,它们应该是第一选择。并且在必要时它们会初始化为nil。 - Rudy Velthuis

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