派生类的指针指向基类对象的问题 (100分)

  • 主题发起人 主题发起人 huaervvhuaer
  • 开始时间 开始时间
H

huaervvhuaer

Unregistered / Unconfirmed
GUEST, unregistred user!
请看代码
Tshape=class
public
ai:integer;
procedure draw();
end;

Tline=class(Tshape)
public
bi:integer;
bs:string;
bp:Tpoint;
procedure draw();overload;
end;

var
line:Tline;
begin
line:=Tline(Tshape.create);//1.只是转换了对象引用的类型??
line.bp:=point(2,3)
//2.ok,为什么?但下一条语句出错
line.bs:='aaaaaaaaaa'
//3.运行时出错,为什么?
line.draw();//4.调用的是派生类的对象的draw(),为什么?
end;
请回答上面四个问题。
*********************************************************

另外把例子稍做修改:
Tshape=class
public
ai:integer;
procedure draw()
virtual;
end;

TLine=class(Tshape)
public
bp:Tpoint;
str:string;
procedure draw()
override;
end;

var
Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var
line,line2:TLine;
begin
line:=TLine(Tshape.Create);
//转换的只是对象的引用的类型,从下面的测试结果可以得出实际的对象还是Tshape对象
line2:=TLine.Create;
showmessage('inst size:'+inttostr(line.InstanceSize)+':'+inttostr(line2.InstanceSize));

//但是--> 问题2. 下面又如何解释?
Line.bp:=point(2,3);
line.ai:=999;
showmessage(format('line.bp:%d,%d: line.ai is %d',[Line.bp.X,Line.bp.y,line.ai]));
// 输出的是line.bp:2,3 line ai is 999
// 说明ai,bp并不是占用相同的内存空间,这样的话与上面的自相矛盾呀!!!

//问题3, 下面的在delphi7中运行的时候会出错,但在delphi 6中正常!为什么?
//有兴趣的朋友可以分别在delphi 7和delphi 6中试试
line.str:='drived class string';
showmessage(line.str);

//新发现,用派生类的指针/引用指向基类的对象,同样可以实现多态!!!
line.draw;
//输出结果是invoked Tshape.draw(),即调用的基类Tshape的方法。

end;

{ Tshape }

procedure Tshape.draw;
begin
showmessage('invoked Tshape.draw()');
end;

{ TLine }

procedure TLine.draw;
begin
showmessage('invoked TLine.draw()');
end;
 
4.调用的是派生类的对象的draw(),为什么?
应该是
4.调用的是派生类的draw(),为什么?
 
有一点要说明:overload是不属于多态的范畴,它的作用仅仅是区别你的过程名而已。
可改成:
Tshape=class
public
ai:integer;
procedure draw();virtual;
end;

Tline=class(Tshape)
public
bi:integer;
bs:string;
bp:Tpoint;
procedure draw();override;
end;

line:=Tline.create;
 
对于1,4说说我个人的看法
1。应该只是转换了对象的引用的类型。
4。一个类的所有对象共享同一个方法的代码段,又delphi中方法是根据其对象引用的类型来确定是调用基类的方法还是派生类的方法。。
对于第4点,还有个问题,
我把基类Tshape 中draw设为virtual,再在派生类中override后,调用的却是基类的draw,这也是所谓‘多态性’,所以多态性的实质并不是只是指用基类的指针指向派生类的对象(Nicrosoft的‘再谈多态’中的说法有点欠妥)
 
to joioy,
在上面楼主的意思不是实现多态。
 
overload用的不对,你的函数参数又没有重载!
 
正如joioy所说“overload只是区别的过程名而已”
 
line:=Tline(Tshape.create);
这个创建了一个引用对象,这个对象指向的是一个TShape类的内存,所以你用下面
的操作肯定是会有问题的,但是具体你下面的问题的细节,我也想搞清楚

line.bp:=point(2,3)
//2.ok,为什么?但下一条语句出错
line.bs:='aaaaaaaaaa'
//3.运行时出错,为什么?
line.draw();//4.调用的是派生类的对象的draw(),为什么?

 
我来说说吧:

1:从代码上来看Line对象是TLine类的实例,但是事实上Line是Tshape类的实例,可以通过下面的方法确认:
ShowMessage(IntToStr(Line.InstanceSize));//显示出来的是8;
ShowMessage(IntToStr(TLine.InstanceSize));//显示出来的是24;
ShowMessage(IntToStr(Tshape.InstanceSize));//显示出来的是8;

2:line.bp:=point(2,3);
真的OK吗?
看看下面的代码:
var
line:Tline;
shape:Tshape;
begin
line:=Tline(Tshape.create);
shape:=Tshape.Create;
line.bp:=point(2,3);
ShowMessage(IntToStr(shape.ai));//这里显示出来的是多少?2,不对吧,应该是0才能呀.是因为Line在作怪呢.
shape.ai:=100;
ShowMessage(IntToStr(line.bp.x)+':'+IntToStr(line.bp.y));
//这里显示出来的x和y是什么?好象是100和18.为什么呢?
//上面说Line事实上是Tshape的实例,也就是说它在内存中只占有8个字节的空间,
//好了,光一个点就占8个字节,它放哪里?放后面,为什么不会告成非法内存访问,
//因为后面的内存也是本应用程序的.而且是Shape对象的内存,
//所以Shape对象的ai成员就和Line对象的bp成员的x字段重合了.
end;

//3.运行时出错,为什么?
有了上面的知识,应该不会问为什么出错了吧.
我们知道String事实上是一种特别的指针(有人说不对,它是一个动态数组,喝喝,那我告诉你动态数组也是一个特殊的指针),它已经由Delphi封装了很多指针操作.即然是指针,如果它指向一个未知完间会出什么问题?如果一个对象的String类型的成员是它自己的成员,它会在构造的时候把它指向一个有意义的内存,而如果本身就不是它自己的成员,喝喝.对不起,它管不着.

//4.调用的是派生类的对象的draw(),为什么?
上面已经说得很清楚了,对于编译器来说,Line是TLine对象,对于内存来说Line是TShape对象,你是用Overload,编译器就不会再认同名同参数的父类的方法,所以调用的是TLine类的方法,如果如joioy兄的方法那样修改一下,你再看看,它会调用TShape的方法.


说得不是很清楚(这玩意的确很难说清),还请见谅.
 
1.> overload
理解错误,这个就不用说了。
2.> 一个对象的创建,可以理解系统建立的一特定内存块,包含数据块和代码块,并且系统根据某种特定的方式(如调用该对象的Create方法)对其中的数据块进行初始化工作。
Tline(Tshape.create) ,实质是创建了一个Tshape类型对象,并没有创建Tline类型对象,
很明显,没有调用Tline.Create方法,而调用了Tshape.Create方法.
Tline(Tshape.create),仅仅是做一个强制类型转换,欺骗编译器而已,就像我完全可以把一
个毫无意义的数0强制转换为一个Tline,这样写Tline(0).Create,编译可以通过.
回到楼主的问题上:
var
line:Tline;
begin
line:=Tline(Tshape.create);//1.只是转换了对象引用的类型??
line.bp:=point(2,3)
//2.ok,为什么?但下一条语句出错
line.bs:='aaaaaaaaaa'
//3.运行时出错,为什么?
line.draw();//4.调用的是派生类的对象的draw(),为什么?
end;

Line虽然定义为Tline类型,但是我们知道所有的对象都是指针,line:=Tline(Tshape.create),使得Line指向一个Tshape确定的内存区域,这使得对对象Line的一切调用都是变得不确定,因为Line披着Tline的外衣,却是有着Tshape的内脏
这一切也许可以骗过编译器,骗过RTTLL,但是无法骗过操作系统,所以我猜测
line.bs:='aaaaaaaaaa';这种用法也许有的时候也不会抛Execption出来.
 
别看Jhdandcl专家分才区区2812,但是分析得入目三分,字字珠玑呀!俺佩服!相比之下,楼主自以为聪明,实则糊涂不堪。何必打Nicrosoft的招牌。俺是破例进了带ID的标题,只为结识Jhdandcl。
line:=Tline(Tshape.create);//1.只是转换了对象引用的类型??
利用编译器的宽容,犯下不法勾当的开始。事实上并没有什么类型上的转换,而是为后面的勾当撑起了一把保护伞。

line.bp:=point(2,3)
//2.ok,为什么?但下一条语句出错
正如Jhdandcle所说说不定啥时候也出报错。

line.bs:='aaaaaaaaaa'
//3.运行时出错,为什么?
TShape的实例是8字节长(查验InstanceSize),TLine的实例却是24字节长。也就是说,你申请了一个8字节长的内存空间,你却要把它当24个字节的空间来访问,后面的16个字节是否出现AV错误纯粹是运气了。

line.draw();//4.调用的是派生类的对象的draw(),为什么?
当然,到底运行哪个对象的方法,那不是运行期可以更改的,在编译期已经固定好了。你的语句分明是调用的Tline的draw。如果你的Tshape的Draw是virtual的,而tline又override了draw方法,你在draw方法中如果有调用inherited这样的语句,你将死得很难看。因为你的Tshape的VMT中draw指向0x00000000,也就是一个抽象方法的地址。
其实运行期是可以检查出这个错误的。你试试在运行期查验Self.ClassType或者Self.InstanceSize返回的是TShape和8。Delphi是为你提供一个灵活运用的机会,并不是鼓励你犯罪。
 
今天让我加深了对概念了解。

其实delphi不允许子类引用父类的。

procedure TForm1.Button4Click(Sender: TObject);
var
MyClass1: TMyClass1;
MyClass: TMyClass;
begin
MyClass1 := TMyClass.Create
<----报错,类型不匹配
MyClass := TMyClass1.Create
<----允许的。
end;
 
大家能不能对强制类型转换做一下介绍,什么时候应该用,什么时候不应该用,
如何合理的去用,不要造成滥用。
 
想想楼主也并不是自以为聪明,每个人在学习的过程中都会遇到各种各样的问题,自己想不通,当然求救能者了,地球人都知道,Nicrosoft对这方面有独到的见解,研究得非常,楼主希望他出手并非不可.

to:jacky_shen,说简单一点,强制类型转换的时候就是需要把对象打回原型的时候用到,比如:
procedure TForm1.Button1Click(Sender: TObject);
begin
if Sender is TButton then//判断Sender是不是TButton或者其派生类的对象.
TButton(Sender).Caption:='click';//如果是的话强制类型转换.
end;
 
首先感谢大家的参与,尤其是aizb,Jhdandcl,Carlsberg三位大侠,他们的分析很有道理,特别是aizb对我的理解,我感到很欣慰,我很希望能和你交个朋友。其实这个帖子是我问的,由于我的可用分很少(并不我不想回答别人问题,只是我的水平很菜,回了很多贴,我的专家分还是那么地可怜,5555),我借用我朋友huaervvhuaer的ID的,请不要见怪。
当初我也是看到“用派生类的指针指向基类的的对象时,有可能会导致灾难性的后果,即
有可能使用基类中对象并没有成员",我就写了这样一例子试试,并不是我自以为聪明‘故意犯罪’哦,不过在其中我发现了另一种“多态”,即“用派生类的指针/引用指向基类的对象,同样可以实现多态!”(可以看下面的程序),但我看到Nicrosoft的一篇文章Nicrosoft的‘再谈多态——向上映射及VMT/DMT’中的说法“多态的本质就是‘将子类类型的指针赋值给父类类型的指针’”有所偏妥(在delphi,C++我没有试过,我个人认为),其实“派生类的指针/引用指向基类的对象”也是可以的的,虽然这样不安全,但在程序员清楚他们的关系的情况下,这个安全问题可以避免,这样做也并不是没有价值,我觉得
多态的实质就是“使用动态帮定(后期帮定)技术根据对象实际的类型来确定要调用的方法”更确切些,当然从实现多态的方法的上来说,简单地说就是使用了VMT,在类的头8个字节含有一个指向VMT的指针,Nicrosoft(我很佩服他)能看到,所以并不是我故意打着'nicrosoft'的牌子哦,


 
但问题2,3还是没有解决,更其奇怪的是在delphi 7 和delphi 6中的结果截然不同,有兴趣的朋友可以试试。

Tshape=class
public
ai:integer;
procedure draw()
virtual;
end;

TLine=class(Tshape)
public
bp:Tpoint;
str:string;
procedure draw()
override;
end;

var
Form1: TForm1;

implementation

{$R *.dfm}

procedure TForm1.Button1Click(Sender: TObject);
var
line,line2:TLine;
begin
line:=TLine(Tshape.Create);
//转换的只是对象的引用的类型,从下面的测试结果可以得出实际的对象还是Tshape对象
line2:=TLine.Create;
showmessage('inst size:'+inttostr(line.InstanceSize)+':'+inttostr(line2.InstanceSize));

//但是--> 问题2. 下面又如何解释?
Line.bp:=point(2,3);
line.ai:=999;
showmessage(format('line.bp:%d,%d: line.ai is %d',[Line.bp.X,Line.bp.y,line.ai]));
// 输出的是line.bp:2,3 line ai is 999
// 说明ai,bp并不是占用相同的内存空间,这样的话与上面的自相矛盾呀!!!

//问题3, 下面的在delphi7中运行的时候会出错,但在delphi 6中正常!为什么?
//有兴趣的朋友可以分别在delphi 7和delphi 6中试试
line.str:='drived class string';
showmessage(line.str);

//新发现,用派生类的指针/引用指向基类的对象,同样可以实现多态!!!
line.draw;
//输出结果是invoked Tshape.draw(),即调用的基类Tshape的方法。

end;

{ Tshape }

procedure Tshape.draw;
begin
showmessage('invoked Tshape.draw()');
end;

{ TLine }

procedure TLine.draw;
begin
showmessage('invoked TLine.draw()');
end;
 
to Jhdandcl,
我对RTTL不太懂,请多多指教!!
 
line:=TLine(Tshape.Create);
//转换的只是对象的引用的类型,从下面的测试结果可以得出实际的对象还是Tshape对象
意料之中没什么奇怪.
//但是--> 问题2. 下面又如何解释?
Line.bp:=point(2,3);
line.ai:=999;
showmessage(format('line.bp:%d,%d: line.ai is %d',[Line.bp.X,Line.bp.y,line.ai]));
// 输出的是line.bp:2,3 line ai is 999
// 说明ai,bp并不是占用相同的内存空间,这样的话与上面的自相矛盾呀!!!

但是ai和bi肯定占用相同空间.

//问题3, 下面的在delphi7中运行的时候会出错,但在delphi 6中正常!为什么?

说明这是D6的bug,或者说某种程序上的宽容(在处理string数据方面)

//新发现,用派生类的指针/引用指向基类的对象,同样可以实现多态!!!
line.draw;
//输出结果是invoked Tshape.draw(),即调用的基类Tshape的方法。

这不算什么新发现。你的Draw方法是虚方法,类指针指向的是TShape,也就是说调用draw的时候是从TShape的VMT中找draw的入口地址当然是执行TShape的Draw了。
 
to Carlsberg,想不到你在线呀。
"但是ai和bi肯定占用相同空间."但同一个空间可以不可能同时存储两个不同的值!!
 
有没有msn,我在线讨教你,可以吗?
 
后退
顶部