【编译器优化】且看对象空指针调用对象方法

rainychan2009 2012-05-16 07:42:01
加精

unit main;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs, StdCtrls;

type
TSimpleTest = class
private
public
procedure PrintClassName;
function DoubleValue(AValue: Double): Double;
end;

TForm1 = class(TForm)
btn1: TButton;
procedure btn1Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

var
Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.btn1Click(Sender: TObject);
var
SimpleTest: TSimpleTest;
i: Double;
begin
//这里置为nil
SimpleTest := nil;
SimpleTest.PrintClassName;
i := 5;
i := SimpleTest.DoubleValue(I);
ShowMessage(FloatToStr(I));
if not Assigned(SimpleTest) then
ShowMessage('Object is nil!');
end;

{ TSimpleTest }

function TSimpleTest.DoubleValue(AValue: Double): Double;
begin
Result := AValue * 2;
end;

procedure TSimpleTest.PrintClassName;
begin
//这里必须用类方法,否则就报错
ShowMessage(TSimpleTest.ClassName);
end;

end.


各位,上面对象的指针是空的,但是我依然可以调用对象的方法。方法的规律是没有用到对象的数据部分,其实可以写成类方法,但是这里却没有加class标志,但是又可以调用。为什么可以调用呢?我怀疑是被编译器优化了,其实是按照类方法来处理的。这些方法本来就用VMT来管理的。
...全文
1435 29 打赏 收藏 转发到动态 举报
写回复
用AI写文章
29 条回复
切换为时间正序
请发表友善的回复…
发表回复
liangpei2008 2012-05-22
  • 打赏
  • 举报
回复
Delphi的方法默认为静态的,即在编译期已经确定其地址。故不需要创建对象。
LAONINGA098 2012-05-19
  • 打赏
  • 举报
回复
不错。。。。支持!!!!
lijing106 2012-05-18
  • 打赏
  • 举报
回复
归根结底,涉及数据域,掉操作的时候首先会考虑对象指针的合法性, mov byte ptr [eax+$04],$66 这个移动4个字节,是因为前面四个字节存放一个地址,指向VMT,显然移动4字节地址段是受系统保护的,就会出问题,而方法是在编译的时候就确定了地址的,因此,方法就直接通过地址来调用了。
seraphczh 2012-05-18
  • 打赏
  • 举报
回复
不错支持支持。
rainychan2009 2012-05-17
  • 打赏
  • 举报
回复
s11ss
看了你写的例子我终于明白了,也理解了你开始的说页权限,非常感谢。
一千句解释不如一段代码,一段代码不如一段汇编啊,汇编真是个好东西。
procedure TForm1.btn1Click(Sender: TObject);
var
p: Pointer;
begin
p := @TSimpleTest.PrintClassName; //这个就是通过VMT结构获取的。
asm
MOV EAX, 0
CALL p
end;
end;
所以,归根结底,涉及数据域,掉操作的时候首先会考虑对象指针的合法性, mov byte ptr [eax+$04],$66 这个移动4个字节,是因为前面四个字节存放一个地址,指向VMT,显然移动4字节地址段是受系统保护的,就会出问题,而方法是在编译的时候就确定了地址的,因此,方法就直接通过地址来调用了。
蓝色光芒 2012-05-17
  • 打赏
  • 举报
回复

Type
TTestClass = class(TComponent)
public
Function GetThisClassName : AnsiString;
end;

function TTestClass.GetThisClassName: AnsiString;
begin
Result := Self.ClassName;
//Result := ClassName; 这和上一句就本次访问的内容来说是一回事
//ClassName好象特殊了一点,反正就是为了说明这事
end;

procedure TForm1.FormCreate(Sender: TObject);
var
Obj : TTestClass;
begin
Obj := TTestClass(TObject.Create);
//这样调用,相当于让类中引用Self的地方都变成引用这个Obj对象,即TObject的实例
//注意类方法/函数中引用类自身的或者父类的数据定义,都是隐含的Self引用
if Obj.GetThisClassName = 'TObject' then
ShowMessage('相等');
end;
蓝色光芒 2012-05-17
  • 打赏
  • 举报
回复
这个VMT没多大关系,public中的方法,是不会记录"名称"的,只有地址,任何地方都是CALL这个地址来调用该方法,

类就是record + procedure/function,只不过复杂一点,但最终就是这样
而对象就是个指针 指向new(this record)的内存的指针,

你的例子来说:
function TSimpleTest.DoubleValue(AValue: Double): Double;
begin
Result := AValue * 2;
end;

procedure TSimpleTest.PrintClassName;
begin
//这里必须用类方法,否则就报错
ShowMessage(TSimpleTest.ClassName);
end;

没有用到Self,因此,对NIL对象的调用也没问题,一旦你用self,必然出错,注意直接引用类定义中的数据都是隐含的self调用,等会举个例子,看看就更明白了。
xiying12571 2012-05-17
  • 打赏
  • 举报
回复
支持下!虽然看不懂
rainychan2009 2012-05-17
  • 打赏
  • 举报
回复
我想说的是,由于方法都被管理在VMT中,VMT是伴随着类的,并不一定要创建类的实例。我例子里面确实没有创建类的实例,对象的指针为nil,但是通过访问一个为nil的指针,调用一个方法,这个不符合常理?我怀疑的优化是,对象的指针为nil,调用的是方法,那么直接从VMT里面去找了,方法的地址肯定已经存在了,这个在编译的时候已经确定了,确实是一个静态方法,但是动态不行?我中午再试试?个人感觉也是可以的,因为这个毕竟只是在访问VMT。。。这个我还真有点疑惑,汇编还怎么怎么搞过,谢谢大家的回复
s11ss 2012-05-17
  • 打赏
  • 举报
回复
我的意思是说,正常情况下eax应该是对象实例的地址;权限问题,是指地址所在页的访问权限。[Quote=引用 2 楼 的回复:]
引用 1 楼 的回复:
不关优化的事!

方法的规律是没有用到对象的数据部分——用到数据部分会出错是因为,会取eax表示的地址的附近的值。而对象的指针是空,意味着eax为0,而不是对象实例的地址,因为权限问题就会报错。

你讲的我觉得还是不对。这里面根本没有创建对象的实例,哪里还有对象实例的地址?权限问题?由于没有对象实例,因此只能用类方法。
[/Quote]
zwjchina 2012-05-17
  • 打赏
  • 举报
回复

procedure TForm1.btn1Click(Sender: TObject);
var
p: Pointer;
begin
p := @TSimpleTest.PrintClassName;
asm
MOV EAX, 0
CALL p
end;
end;
BambooCaep 2012-05-17
  • 打赏
  • 举报
回复
调用类方法其实就是call一个静态地址,同时隐式传入对象的指针。so,出现了你说的现象。跟优化没有关系。你可以跟踪汇编代码来证实这一点。
liuzenchao 2012-05-17
  • 打赏
  • 举报
回复
不错。。。。支持!!!!
s11ss 2012-05-17
  • 打赏
  • 举报
回复
错了,是12楼的程序
s11ss 2012-05-17
  • 打赏
  • 举报
回复
13L运行到o.a时,会报错:
'Access violation at address 0045C028 in module 'Project4.exe'. Write of address 00000004'.

其中0045C028正是f := 'f'的地址!其汇编代码为mov byte ptr [eax+$04],$66,而
00000004正是eax+$04!
kaikai_kk 2012-05-17
  • 打赏
  • 举报
回复
強勢圍觀兼學習
s11ss 2012-05-17
  • 打赏
  • 举报
回复
给你写了个例子!
unit Unit3;

interface

uses
Windows, Messages, SysUtils, Variants, Classes, Graphics, Controls, Forms,
Dialogs;

type
TForm3 = class(TForm)
procedure FormCreate(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

var
Form3: TForm3;

implementation

{$R *.dfm}

type
Ta = class
f: char;
procedure a;
procedure b;
end;

{ Ta }

procedure Ta.a;
begin
f := 'f';//访问数据成员
ShowMessage('a');
end;
{procedure Ta.a对应的汇编代码:
Unit3.pas.36: f := 'f';
0045C028 C6400466 mov byte ptr [eax+$04],$66
//eax+$04是数据成员f的地址,此时因为eax=0,所以地址为4,显然是个非法的地址,会报错
Unit3.pas.37: ShowMessage('a');
0045C02C B840C04500 mov eax,$0045c040//字符串'a'所在的地址
0045C031 E82611FDFF call ShowMessage//调用ShowMessage
Unit3.pas.38: end;
0045C036 C3 ret//返回
}

procedure Ta.b;
begin
ShowMessage('b');
end;
{procedure Ta.b对应的汇编代码:
Unit3.pas.42: ShowMessage('b');
0045C044 B858C04500 mov eax,$0045c058//字符串'b'所在的地址
0045C049 E80E11FDFF call ShowMessage//调用ShowMessage
Unit3.pas.43: end;
0045C04E C3 ret//返回
}


procedure TForm3.FormCreate(Sender: TObject);
var
o: Ta;
begin
o := nil;
o.b;//此行正常执行
o.a;//此行报错
end;
{procedure TForm3.FormCreate对应的汇编代码:
Unit3.pas.48: begin
0045C05C 53 push ebx//保存ebx
Unit3.pas.49: o := nil;
0045C05D 33DB xor ebx,ebx//清空ebx,这时ebx等于0
Unit3.pas.50: o.b;
0045C05F 8BC3 mov eax,ebx//将ebx的值赋予eax,这时eax也等于0
0045C061 E8DEFFFFFF call Ta.b//调用类Ta的方法b
Unit3.pas.51: o.a;
0045C066 8BC3 mov eax,ebx//eax等于0
0045C068 E8BBFFFFFF call Ta.a//调用类Ta的方法a
Unit3.pas.52: end;
0045C06D 5B pop ebx//恢复ebx
0045C06E C3 ret//返回
}

end.
dayofnight 2012-05-17
  • 打赏
  • 举报
回复
补充一下,C++这个技巧早已不新鲜.static只是协助编译器检查防止方法体内使用成员.编译成机器码后本质都是一样的.
dayofnight 2012-05-17
  • 打赏
  • 举报
回复
很简单的原理,调用方法都是压入this指针和方法参数(如果有的话),但是你这个对象方法没有用到this指针包括间接引用.所以可以直接(object)0->xxx(xx).注意这里的0(null)强制转化成类,这样编译器就能找到方法的地址(静态段上).然后根据Call协议压入xx和this(这里是0),然后调用.
unituniverse2 2012-05-17
  • 打赏
  • 举报
回复
c++有同样的问题:

#include <iostream>

class A
{
public:
void foo(void) const { std::cout << "called function 'foo'." << std::endl; };
};

int main(void)
{
A * p = nullptr;
p->foo(); //p为空指针,本来是不允许的。这样调用居然没有错。
return(0);
}

加载更多回复(2)

16,748

社区成员

发帖
与我相关
我的任务
社区描述
Delphi 语言基础/算法/系统设计
社区管理员
  • 语言基础/算法/系统设计社区
加入社区
  • 近7日
  • 近30日
  • 至今
社区公告
暂无公告

试试用AI创作助手写篇文章吧