【问题标题】:Delphi Type Casting德尔福型铸造
【发布时间】:2013-01-15 13:06:32
【问题描述】:

我需要澄清一下 Delphi 类型转换
我写了一个有两个类的例子:TClassA 和 TClassB,TClassB 派生自 TClassA。

代码如下:

program TEST;

{$APPTYPE CONSOLE}


uses
  System.SysUtils;

type
  TClassA = class(TObject)
  public
    Member1:Integer;
    constructor Create();
    function ToString():String; override;
  end;

type
  TClassB = class(TClassA)
  public
    Member2:Integer;
    constructor Create();
    function ToString():String; override;
    function MyToString():String;
  end;

{ TClassA }

constructor TClassA.Create;
begin
  Member1 := 0;
end;

function TClassA.ToString: String;
begin
  Result := IntToStr(Member1);
end;

{ TClassB }

constructor TClassB.Create;
begin
  Member1 := 0;
  Member2 := 10;
end;

function TClassB.MyToString: String;
begin
  Result := Format('My Values is: %u AND %u',[Member1,Member2]);
end;

function TClassB.ToString: String;
begin
  Result := IntToStr(Member1) + ' - ' + IntToStr(Member2);
end;


procedure ShowInstances();
var
  a: TClassA;
  b: TClassB;
begin
  a := TClassA.Create;
  b := TClassB(a); // Casting (B and A point to the same Memory Address)
  b.Member1 := 5;
  b.Member2 := 150; // why no error? (1)

  Writeln(Format('ToString: a = %s, a = %s',[a.ToString,b.ToString])); // (2)
  Writeln(Format('Class Name: a=%s, b=%s',[a.ClassName,b.ClassName])); // (3)
  Writeln(Format('Address: a=%p, b=%p',[@a,@b])); // (4)
  Writeln(b.MyToString); // why no error? (5)

  readln;
end;

begin
  try
    ShowInstances;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

程序输出为:

ToString: a = 5, a = 5
Class Name: a=TClassA, b=TClassA
Address: a=0012FF44, b=0012FF40
My Values is: 5 AND 150

(1) Member2 地址是什么?这可能是“访问冲突”吗?
(2) ok,ToString() 方法指向同一个地址
(3) 为什么 a 和 b 的 ClassName 相同?
(4) ok,a和b是两个不同的变量
(5)如果b是TClassA,为什么可以使用“MyToString”方法?

【问题讨论】:

  • 这些问题有点没有意义。你的演员是非法的。不要这样做。
  • 非法不代表没有意义。
  • 即使我误用了演员表,我认为编译器允许你这样做是有原因的。
  • @AndreaBoc - 通过进行不安全的类型转换,您对编译器说“闭嘴。我知道我在做什么”。编译器只是服从。
  • 你可以用这些信息做什么?你永远不会使用这个代码。一旦你打破了规则,你就无法以稳健的方式进行推理。编译器可能会以不可预知的方式运行。

标签: delphi types casting


【解决方案1】:

您正在对变量应用 hard 类型强制转换。当你这样做时,你是在告诉编译器你知道你在做什么并且编译器信任你。

(1) Member2 地址是什么?这可能是“访问冲突”吗?

当你为类的成员赋值时,编译器使用变量的类定义来计算该成员在内存空间中的偏移量,所以当你有这样的类声明时:

type
  TMyClass = class(TObject)
    Member1: Integer; //4 bytes
    Member2: Integer; //4 bytes
  end;

这个对象的内存表示如下:

reference (Pointer) to the object
|
|
--------> [VMT][Member1][Member 2][Monitor]
Offset     0    4        8         12

当你发出这样的声明时:

MyObject.Member2 := 20;

编译器只是使用该信息来计算要应用该分配的内存地址。在这种情况下,编译器可能会将赋值转换为

PInteger(NativeUInt(MyObject) + 8)^ := 20;

因此,您的分配成功只是因为(默认)内存管理器的工作方式。当您尝试访问不属于您的程序的内存地址时,操作系统会产生 AV。在这种情况下,您的程序从操作系统中占用的内存超过了所需的内存。恕我直言,当您没有获得 AV 时,实际上您很不走运,因为您的程序内存现在可能会默默地损坏。恰好位于该地址的任何其他变量都可能已更改其值(或元数据),这将导致未定义的行为。

(2) ToString() 方法指向同一个地址

由于 ToString() 方法是虚拟方法,因此该方法的地址存储在 VMT 中,调用是在运行时确定的。看看What data does a TObject contain?,并阅读参考书章节:The Delphi Object Model。

(3) 为什么 a 和 b 的 ClassName 相同?

类名也是对象运行时元数据的一部分。您在对象上应用了错误的模具这一事实并不会改变对象本身。

(4) a和b是两个不同的变量

当然,你声明了,看看你的代码:

var
  a: TClassA;
  b: TClassB;

嗯,两个不同的变量。在 Delphi 中,对象变量是引用,因此,在某些代码行之后,它们都引用了相同的地址,但这是另一回事。

(5)如果b是TClassA,为什么可以使用“MyToString”方法?

因为您告诉编译器没问题,并且如前所述,编译器信任您。它很 hacky,但 Delphi 也是一种低级语言,如果你愿意,你可以做很多疯狂的事情,但是:

注意安全

如果您希望(并且您肯定希望大部分时间)保持安全,请不要在代码中应用这样的强制转换。使用as operator:

as 运算符执行检查类型转换。表达式

对象 作为 类

返回对与 object 相同的对象的引用,但类型由 class 给出。在运行时,object 必须是由 class 或其后代之一表示的类的实例,或者为 nil;否则会引发异常。如果声明的对象类型与类无关——也就是说,如果类型不同并且一个不是另一个的祖先——则会导致编译错误。

因此,使用 as 运算符,您在编译时和运行时都是安全的。

将您的代码更改为:

procedure ShowInstance(A: TClassA);
var
  b: TClassB;
begin
  b := A as TClassB; //runtime exception, the rest of the compiled code 
                     //won't be executed if a is not TClassB
  b.Member1 := 5;
  b.Member2 := 150; 

  Writeln(Format('ToString: a = %s, a = %s',[a.ToString,b.ToString])); 
  Writeln(Format('Class Name: a=%s, b=%s',[a.ClassName,b.ClassName])); 
  Writeln(Format('Address: a=%p, b=%p',[@a,@b])); 
  Writeln(b.MyToString); 

  readln;
end;

procedure ShowInstances();
begin
  ShowInstance(TClassB.Create); //success
  ShowInstance(TClassA.Create); //runtime failure, no memory corrupted.
end;

【讨论】:

  • FWIW,内存布局有点不同,这些天:Member1 在偏移量 8 处,而在偏移量 4 处有一个 Monitor 字段。
  • @Rudy,Monitor 字段在对象的末尾,而不是开头,如答案所示。
  • 是的,你是对的。我仍然不明白他们为什么这样做,但是嘿,这是他们的问题。
  • “当您没有获得 AV 时,您实际上是不幸的” - 说得好!事实上,AV 是你的朋友!不要恨他们。爱他们(并修复他们)。
【解决方案2】:
  1. Member2 的地址不是由内存管理器分配的。写入Member2 的可能结果是堆损坏以及随后在程序的完全不同部分中的访问冲突。这是一个非常讨厌的错误,编译器在这里帮不了你。在进行不安全的类型转换时,您必须知道自己在做什么。

  2. 那是因为ToString方法是虚拟的,所以它的地址是由正在创建的类实例的实际类型决定的。如果你用静态替换虚拟方法(在你的情况下用reintroduce替换override指令)结果会有所不同。

  3. 因为ClassName 方法也有点虚拟(不是真正的 VMT 成员,但那是无关紧要的实现细节)。

  4. 是的,a 和 b 是对同一个实例的两个引用。

  5. 因为ToMyString 方法是静态的。实例的实际类型与静态方法无关。

【讨论】:

  • 实际上没有堆损坏,因为在这种情况下,他正在写入实例中的最后一个字段 TMonitor 字段。尝试调用 TMonitor.Enter(b) 以获取 AV(读取地址 000000A6)作为证据。
  • @StefanGlienke - Delphi 2009+ 确实如此。 AFAIK Monitor 字段是在 Delphi 2009 中引入的。
  • D2009+ 与否,这是堆损坏。您希望 TMonitor 的地址存在于堆上的某个位置,但它被覆盖了。
  • @SertacAkyuz - 不,Monitor 现在是任何对象的最后一个指针大小字段。所以在这种特殊情况下,堆没有损坏 - Monitor 字段被损坏了。
  • @SertacAkyuz - 我知道了。这是堆损坏,但它只影响这个特定的实例。 :)
【解决方案3】:

(1) Member2 地址是什么?这可能是“访问冲突”吗?

是的。 AV是可能的。在你的情况下,你很幸运:)

好的,ToString() 方法指向同一个地址

是的,因为 VTable 在创建时涉及。

(3) 为什么 a 和 b 的 ClassName 相同?

与 (2) 中的答案相同。

(4)好的,a和b是两个不同的变量

不是真的。你从堆栈打印地址:)

(5)如果b是TClassA,为什么可以使用“MyToString”方法?

b 是 TClassB 但错误地指向 TClassA 实例。

您应该使用 as 运算符进行此类转换。在这种情况下,它会失败。

【讨论】:

  • 其实,我不会称之为运气。他正在覆盖某些东西,但没有注意到它。我宁愿得到一个清晰的 AV。
猜你喜欢
  • 1970-01-01
  • 2014-07-08
  • 1970-01-01
  • 1970-01-01
  • 2010-10-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2011-07-02
相关资源
最近更新 更多