Delphi类型转换

11

我需要关于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的地址是什么?这是否可能导致"访问冲突(“Access Violation”)"?
(2) 好的,ToString()方法指向同一地址
(3) 为什么a和b有相同的类名?
(4) 好的,a和b是两个不同的变量
(5) 如果b是TClassA,为什么可以使用"MyToString" 方法?


5
这些问题有点毫无意义。你的阵容是非法的。不要这样做。 - David Heffernan
7
非法并不意味着毫无意义。 - kludg
即使我错误地使用了转换,我认为编译器允许以这种方式进行转换的原因。 - AndreaBoc
4
通过进行不安全的类型转换,你在告诉编译器:“闭嘴,我知道自己在干什么。”编译器只会听从你的指示。 - kludg
@Serg 好的,我明白了。我的评论只是因为我找不到我的问题毫无意义。 - AndreaBoc
1
你可以用这些信息做什么?你永远不会使用这段代码。一旦你打破规则,你就无法以强大的方式推理。编译器可能会表现出不可预测的行为。 - David Heffernan
3个回答

30
你正在对变量进行强制类型转换。这样做是告诉编译器你知道自己在做什么,而编译器会相信你。
(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 运算符

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

object as class

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

因此,使用 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;

顺便提一下,现在内存布局有点不同:Member1位于偏移量8处,而偏移量4处有一个Monitor字段。 - Rudy Velthuis
@Rudy,监视器字段位于对象末尾,而不是答案中所示的开头。 - jachguate
是的,你说得对。我仍然不明白为什么他们要那样做,但是嘿,那是他们的问题。 - Rudy Velthuis
1
“当你没有收到 AV 时,实际上是不幸的。” - 说得好!确实,AV 是你的朋友!不要讨厌它们。爱它们(并修复它们)。 - Gabriel

4
  1. Member2拥有一个未被内存管理器分配的地址。对Member2进行写操作可能导致堆破坏,进而在程序的完全不同部分出现访问冲突。这是一个非常严重的错误,编译器无法帮助你解决。当进行不安全类型转换时,你必须知道自己在做什么。

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

  3. 因为ClassName方法也是一种虚方法(并不是真正的VMT成员,但这个实现细节不重要)。

  4. 是的,ab是对同一实例的两个引用。

  5. 因为ToMyString方法是静态的。对于静态方法,实例的实际类型并不重要。


1
实际上,这里没有堆损坏,因为在这种情况下,他正在写入实例中的最后一个字段TMonitor。尝试调用TMonitor.Enter(b),这将导致AV(读取地址000000A6)以进行证明。 - Stefan Glienke
D2009+与否,这是堆破坏。您期望TMonitor的地址存在于堆上的某个位置,但它被覆盖了。 - Sertac Akyuz
@SertacAkyuz - 这是对象实例的一部分。它与对象实例一起分配。 - kludg
@Serg - 我的观点是,由于对象实例内存是从堆中分配的,监视器字段也存在于堆中。但无论如何,我没有成功地表达我的观点。 - Sertac Akyuz
1
@SertacAkyuz - 我明白了。这是堆栈损坏,但它只影响到这个特定的实例。 :) - kludg
显示剩余3条评论

3

(1) Member2地址是什么?这会导致"访问冲突"吗?

是的,可能会出现访问冲突。但在您的情况下,您很幸运 :)

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

是的,在创建时与VTable有关。

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

和问题(2)的答案一样。

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

并不完全是这样。你从堆栈中打印了地址 :)

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

b是TClassB,但由于错误指向TClassA实例。

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


1
实际上,我不会称之为运气。他正在覆盖某些东西,但没有注意到。我宁愿得到一个清晰的 AV。 - Rudy Velthuis

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