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

4

3 回答 3

28

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

(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 中,并且调用是在运行时确定的。看看TObject 包含哪些数据?,并阅读参考书籍章节:Delphi 对象模型

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

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

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

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

var
  a: TClassA;
  b: TClassB;

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

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

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

安全行事

如果您希望(并且您肯定希望大部分时间)安全,请不要在代码中应用这样的硬转换。使用as运算符

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;
于 2013-01-31T13:26:10.357 回答
4
  1. Member2具有未由内存管理器分配的地址。写入的可能结果Member2是堆损坏,随后在程序的完全不同部分发生访问冲突。这是一个非常讨厌的错误,编译器在这里帮不了你。在进行不安全的类型转换时,您必须知道自己在做什么。

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

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

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

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

于 2013-01-31T12:53:21.657 回答
3

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

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

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

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

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

答案与(2)相同。

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

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

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

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

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

于 2013-01-31T12:31:42.470 回答