Object Pascal Free方法的奇怪现象,涉及到面向对象理论,请高手指点。(200分)

  • 主题发起人 ZXW49362727
  • 开始时间
P

Pipi.

Unregistered / Unconfirmed
GUEST, unregistred user!
有用,
TEdit.Create 相当于 TEdit::Create 是指定了某个类的方法
而:
var
c,b:TComponent;

c:=Edit1;
b:=c.Create(nil);
虽然c是声明为TComponent,但是实际上是调用了 TEdit.Create ,就起作用了
 
O

oskiller

Unregistered / Unconfirmed
GUEST, unregistred user!
还是老夫来kill这篇帖子吧,呵呵,本人连OS一天也能kill它两三回,还怕kill不了这篇
帖子?:)
纵观这篇帖子,回复的高手不少,本人也从中学到很多,只是话题的中心好像东拉西扯,
先有两个'静态方法'搅局(C++和OP),后有两个Self指针蛮缠(指向对象的指针和指向类
的指针),事实上同志们已经把问题讨论正确了,我现在只是来做番整理。
首先还是回到ZXW49362727兄的问题上来吧,
(以下是ZXW49362727兄的原文,带"/*"的行是我得注释,最后有一点对delphi对象模型
的探讨)

请看以下代码:
var
Strings: TStringList;
<1>: Strings:= nil;
Strings.Free

//为何以上代码运行不会出错?如果Free方法确实调用了,那么它保存在内存哪里呢?

/*答:1.首先当你第一句声明了一个"var Strings"后,你在栈空间得到一个4字节的内存,
/*这块内存由符号"Strings"对应(就像你声明一个"var a:Longint;"后'a'就对应于
/*栈中一个4字节(Longint大小)的内存一样)。而这段内存中此刻存放的内容是什么呢?
/*它是不确定的,(更准确的说是上一次系统对这块内存写后的值,不过这里我们不管它)
/* 2.你做的非常好,你把这块内存赋值为nil,在c++中这是标准做法,否则你后来可能
/*直接使用它的值作为指针,也就是前面说得不确定的值作为指针,导致非法内存访问。
/* 3.关键的来了!你调用了"String.Free;"准确的说吧,你调用的是这个:
/* Free(Self)
//这个Self哪里来的?就是Strings对应内存的值啊,你
/* //把它赋值为nil了,这个self就是调用所有普通成员方法
/* //都会用到的"指向对象"的指针,但这里它是nil,没有指
/* //向任何对象。 请注意别和"指向类"的self指针混淆。
/* 再准确点说你调用的这个:
/* Free(nil);
/*在Free中就简单了,它会判断传给它的self指针的值,是nil就什么也不做,直接返回
/*所以这段代码没有出错。你问Free方法保存在内存的哪里?它当然是保存在只读的内存
/*页中,或者说代码页。我想你的真正想问的是Free过程是怎么被调用的?很简单,Free
/*是静态绑定的普通成员方法,它的入口地址是编译时就能确定的,调用的时候把参数压
/*入栈,一个call调用指令就完事。你或许会疑惑的是Destroy是虚拟方法啊,不错,只是
/*这对于我们使用Free来说毫无关系,因为怎样找到Destroy方法并调用它是Free的责任,
/*我们不用管,关于这点后文有详述。
/*好了,第一个问题搞定。

<2>: Strings:= TStringsList.Create;
Strings.Free;
Strings.Free;
//出错。如果上面那个问题解决的话,这个问题也解决了。

/*答:1.先看你的第一句,你调用TStringsList类的Create方法,这个方法在堆中创建一个
/*TStringsList对象并返回一个指向这个对象的指针,返回的这个指针赋值给了Strings。
/*也就是说Strings所对应的那块4字节大小的栈内存中此刻存放的值是Create方法创建的
/*对象的地址.
/* 2.第二句你调用Free释放这个对象,你实际上是这样调用的:
/* Free(Self)
//注意这里的self就是Strings所对应那块4个字节大小栈内存
/* //的值,其值是上一句Create方法创建的对象的地址。
/*这里Free首先看Self是否为nil,这里不是,所以它调用Destroy摧毁对象(至于Free方法
/*怎么找到虚拟方法Destroy见后文)。摧毁对象后Free过程返回。
/* 3.第三句你再次调用Free过程,也就是再次调用:
/* Free(Self)
//特别注意,你这里的self的值还是第一句Create方法所创建
/* //对象的地址,因为Self就是Strings所对应的4字节大小栈内
/* //存的值,而你并没有对这4字节内存作任何操作,它当然还保
/* //持着原来的值,也就是已经被销毁的对象原来的地址。
/*现在就简单了,Free方法先检查Self是否为nil,基于前述的理由它当然不是,好紧接着
/*Free方法调用Destroy摧毁对象,夷????这个对象不是已经摧毁了嘛?它所占用的内存
/*已经回收了阿,而Destroy再次对已经回收的对象所占资源进行回收当然就会出错了。

Strings:= TStringsList.Create;
Strings.Free;
Strings:= nil;
Strings.Free;
Free的VCL源码如下:
if Self<>nil then
Destory;
确实,如果String=nil 时,不会调用Destory所以不出错。
但问题是Free不是一个静态方法,在没在调用Create的时候,Free方法的代码保存在哪呢?
请高手指教。

/*答:首先你第一句话就错了,Free正是一个静态方法,这里请注意,千万别和C++中的静
/*态方法混淆,同一个名词在不同的语言中对应不同的概念这是常有的事,delphi中的
/*静态方法强调的是静态绑定的意思。Free是静态绑定的,这意味着编译时已确定地址
/*了,所以一切明了。
-------------------------------------------------------------------------------
最后,顺便探讨一下delphi的对象模型,本人接触Delphi也不到一个月,以前是c++程
序员,也是用C++来理解delphi,呵呵所以刚开始时也犯了Pipi兄一样的错误,把delphi的
class method等同于c++的static member function.这个问题也是前几天抄vcl源码时才搞
明白它们概念上是类似但实现有区别。vcl源码那几个主要构件源码现在还没抄完呜呜:~(
呵呵,忽然发现DFW上许多兄弟同时精通delphi和c++,果然是高手如云。
先回复几个DFWer:

to sadj:
Free过程一点也不特殊,你说得:"一个对象没有建立之前,除了静态方法和变量,对象的方
法是没有分配地址的",这句话我不太明白你的意思,你这里的静态方法是指的class Method
还是普通成员方法?什么叫没有"分配"地址?没有"确定"地址吧?作为oo语言,OP和C++等
原生编译的语言都只是在语法语义层面将数据和代码绑定起来,而编译底层是将代码和数据
分开的,因此我们不难理解所谓的"普通成员方法"其实和原来老PASCAL的函数过程是一回事
只是每个函数都有一个隐含的参数Self,它指向一个堆中的地址,以这个地址开始的这个区
域就是对象的茜身之所,因此我们可以毫不犹豫的确定普通成员方法是静态绑定,编译时就
可确定其入口地址,就像原来的老pascal语言的函数过程一样。
而类方法其实和普通成员方法的实现完全一样,只是传给它的Self参数指向的是类而不是
对象更清楚些是指向的只读的代码区中的一张表。呵呵,这里最容易糊涂。这张表里记录了
所有的虚拟函数地址以及一些类型信息比如父类所对应的表的地址,千万注意,在delphi中
类就等同于一张表,所以类也有地址,比如类引用就是指向这张表的指针,你给它赋值为不
同的类,它就指向不同类所对应的表,而这也是C++程序员最难理解的地方。因此,现在我
们也可以确定类方法和普通成员过程一样也是静态绑定的,编译时便可确定入口地址。
现在来看虚拟方法,当我们定义了一个类,编译器便完全确定了所有的方法入口地址,
无论是普通成员方法还是类方法还是虚拟/动态方法,(当然了,所有的方法都是由它编译
的它还不知道啊?:) 但是或许会有人说虚拟/动态方法不是晚绑定么?
**千万注意:晚绑定是针对对象而不是针对类,在所有OO语言中都是如此。
看看我们怎样使用类吧,我们用类来定义对象,用对象来使用类。一个对象比如猫从
概念上来说当然是一个动物(is a的关系),而一只狗也是一个动物,注意我为什么不说
"猫类是一种动物类?" 前者是对象之间的关系,它正是后者类之间关系的体现,
现在我们让一只动物"叫",很显然当我们把这个命令发给猫,它当然是"喵喵"叫,发给狗
它当然应该是"汪汪"叫,这种现实世界中广泛存在的"**一种动作不同实现!**"的关系就
叫多态,多态并不是程序世界的术语而是现实世界中的术语,为什么会引入到程序世界中
呢?就是因为为了让我们的程序编写方式更接近于真实世界,因此也就更为易写易读易理
解。这就是多态技术乃至面向对象的技术的由来。扯远了哈,那么现在我们的问题就是怎
样以高效易用的方式在程序世界中实现现实世界中的多态关系。
前面说到当我们定义了一个类所有的方法就已经确定,它们的代码也都存放在只读的代码
页中,但是编译器另外还会构造一张表,这张表中记录了这个类所有的虚拟方法。这样子
我们程序中用到的每一个类都会有一张记录了他们各自虚拟方法的表,当我们使用对象的
时候,首先是申明一个对象,其实此时我们得到的只是一个指针,并且这个指针的值是不确
定的,注意我们申明时申明的对象所属类型,这一点也就决定了所有的普通成员函数怎样与
我们这个对象联系起来。紧接着我们创建对象,这一步决定了我们的对象怎样与具体的类所
对应的那张表联系起来。这个对象是由创建对象实体时调用的那个类所对应的create函数创建的
它当然知道自己的类对象实体有多大,总而言之它只返回一个地址赋值给申明对象时得到
那个指针。注意这一步中的Create方法,它创建对象实体也初始化这个对象实体的所有域
其中隐含的第一个域就是指向用来调用create那个类的指针,它指向那个类所对应那张表
也就将所有的虚拟方法与对象联系起来。因此当我们调用一个虚拟方法时,实际上是先根据
对象实体的第一个域找到类对应的表再查表中虚拟函数的地址,再调用它。
***综上所述,类方法和普通成员方法都是在我们申明一个对象时就确定了,而虚拟方法
和动态方法都是在创建对象时才确定的。****

to 教父:
你有一句话没说完整:"所以既使没有创建实例,但还是可以访问它的静态方法"
可以访问是一回事,程序崩溃通常也是同一回事,呵呵,是的,只要你声明了一个对象,
不必创建对象实体就可以访问它的静态方法(普通成员方法),但是注意:当你访问一个静态
方法时是以你声明的这个对象(引用)的值作为self指针传给静态方法的,而此时它的值是
不确定的,紧接着有两种可能,1.你调用的这个静态方法压根儿没用这个self指针,比如
你可以写一个只输出'HELLO!'的静态函数试试,绝对没问题。
2.你调用的这个静态方法会检查这个self指针是否为空,
如果为nil则什么也不做,返回。这种情况下只要你保持一个声明一个对象马上赋值为nil
的习惯那么程序也不会崩溃。(对于c++程序员这是入门守则呵呵)

to savenight:
你可真有些冤枉,你说得对你没错。看到sadj对你义愤填膺的样子我可真有些同情你了,
自己认为对的可一定要坚持。即使错也的错个明明白白。你很谦虚,交我这个朋友好嘛?
我的qq:65326804

to sadj :
本来我想你们讨论出任何结果我都不会惊讶,但,老天,你以什么作为根据说op/vcl不符
合OOP的理念?OOP的理念与任何底层机制/计算策略有关马?比如你完全可以用双表格模型
实现OOP。。。

最后,本来还想探讨一下比如VMT中有些什么还有这几天研究VCL的心得(抄源码抄出来的苯
鸟先飞么)比如消息机制等等,一是天快亮了,手累了得睡觉,二是一些思考还不成熟,等
有空了再贴出来大家探讨一下,我有不少疑惑盼望大家指正,学了一个月DELPHI觉得蛮好
到DFW不久,大家多多指教。还有我看了关于用OP实现GP的帖子,有些想法,感觉照搬C++
的模式是不行的,它们的底层机制区别极大,最有希望的是向泛型JAVA的方案靠齐,感觉
DELPHI和JAVA和.NET实在是极为神似,出来这么久的DELPHI还与当今领先技术合拍实在是
BORLAND的技术实力。哎,睡了睡了
 

吴明星

Unregistered / Unconfirmed
GUEST, unregistred user!
受益非浅!期待oskiller的另一个贴。
 
D

darkiss

Unregistered / Unconfirmed
GUEST, unregistred user!
技术是拿来给人用的,不是拿来炫耀的,大家还是把精力放在怎样使用技术上面吧,没有必要
一定要把什么底层的东西搞清楚。我想重要的是碰到一个好的技术,首先应该想到怎么利用它,
而不是怎样把它研究透,技术日新月异,哪里有时间一项一项全搞的很清楚?这个帖子到现在我
认为已经够了,该结束之。
 
S

savenight

Unregistered / Unconfirmed
GUEST, unregistred user!
oskiller:
兄弟,多谢了(大富翁里不愁找不到知己,呵呵呵~~~)。我的qq: 27722635.,一会儿我加你。昨天感染了‘求职信’,
格式化了两遍c盘都没用,什么都不做时,硬盘都一个劲的响,内存不断上涨。用毒霸网站的方法根本就不行。我也是今天早晨6:00才回去,
急死了,还请各位兄弟多帮帮忙。

〉〉,感觉DELPHI和JAVA和.NET实在是极为神似,。。。
对呀,这也是我看了Csharp,.net后的感觉,甚至感觉有点像大学里完成作业,只有一两个母板,其他
全是拷贝。borland的大师真是厉害。
 
S

sadj

Unregistered / Unconfirmed
GUEST, unregistred user!
谢谢oskiller的回复,讨论这个问题我澄清了很多似是而非的概念。
to savenight
我为我愚蠢的话感到惭愧,希望你不要介意。
感谢大家的帮助,在这里不光可以学到技术,还可以学到做人的道理[:D]。
 
S

savenight

Unregistered / Unconfirmed
GUEST, unregistred user!
sadj: 你都道歉过了,再这样我都不好意思了。

兄弟们救救我吧:
http://www.delphibbs.com/delphibbs/dispq.asp?lid=1057929
 

吴明星

Unregistered / Unconfirmed
GUEST, unregistred user!
对了类方法不是可以虚拟的吗?那又是怎么一回事呢?
 
L

lance2000

Unregistered / Unconfirmed
GUEST, unregistred user!
oskiller的帖子有重大错误!!
吴明星已经指出来了.delphi的类方法与c++的静态函数有很大的不同.
首先delphi的类方法有隐含的参数self,其次delphi的类方法可以是虚的.

>>***综上所述,类方法和普通成员方法都是在我们申明一个对象时就确定了,而虚拟方法
>>和动态方法都是在创建对象时才确定的。****
是不正确的.应该为没有声明为虚拟的类方法在我们申明一个对象时就确定了.
最后,关于虚的构造函数.
当由对象调用构造函数时,不能实现多态.
当由类调用构造函数时,同样不能实现多态
但当由类引用调用构造函数时,就可以实现多态了.
borland真伟大!


 
L

lance2000

Unregistered / Unconfirmed
GUEST, unregistred user!
在delphi中并没有等同于c++中静态函数的概念,说类方法相当于c++中静态函数
有点牵强.
 
L

lance2000

Unregistered / Unconfirmed
GUEST, unregistred user!
大家测试一下下面的代码,说不创建对象就不能访问成员变量,似乎也不正确.
type
TTest=class
private
i:integer;
public
procedure Show;
procedure show2;
end;

procedure TTest.show;
begin
i:=87;
showmessage(inttostr(i))
//87
end;
procedure TTest.show2;
begin
showmessage(inttostr(i))
//87
end;


t1:TTest
t1.show;

t2:TTest
t2.show2;
t1,t2指向同一地址,而该地址不是随机的.
 
P

Pipi.

Unregistered / Unconfirmed
GUEST, unregistred user!
你放在局部变量,局部变量的值时不确定的,不过,你如果你的是在button.OnClick运行的
你ShowMessage(t1.Classame)看看,似乎是TButton?那是你运气好,t1、t2的值不确定的设置成了
一个可以访问的地址,你这样很危险,可能改了某些重要的数据,当然也可能改了无关紧要的数据
你要是给他设置nil,或者放到全局变量去,就没那么好彩了
 
O

oskiller

Unregistered / Unconfirmed
GUEST, unregistred user!
首先对大家说声对不起,真的。我的帖子有个重大失误,感谢吴明星和lance2000给我指出来
是的类方法可以是虚拟的,这种情况下也是迟绑定的。我当时回帖的时候没有考虑到这一点实
在惭愧,说明我的思维还很不严密,以后我回帖会再三检查的,检讨中........

关于虚拟create的问题,lance2000总结的非常好:

当由对象调用构造函数时,不能实现多态.
当由类调用构造函数时,同样不能实现多态
但当由类引用调用构造函数时,就可以实现多态了

但是为什么????我觉得知其然还得知其所以然.我举个列子说明一下我的想法,因为我不
是很确定,所以这里仔细探讨一下,大家看看我的理解是否正确:
以下有两个类TA和TB。其中TA类定义了一个虚拟Create方法,TB继承于TA类并且
定义了自己的Create方法覆盖基类TA的Create方法:

Type
TA=class //编译器遇到这段代码时,发现Create是个虚拟方法
constructor Create;virtual
//于是把Create放到TA对应的虚拟方法表中,我们这
...... //里把这个表称为"表TA",仅仅为了称呼方便
end;

TB=class(A) //编译到此处,发现TB继承于TA,那么TB就应该是一种
constructor Create;override
//TA,TA有的TB也应该有,所以编译器复制一份TA的
...... //虚拟方法表放到TB的虚拟方法表中,紧接着它发现TB
end
//覆盖了Create,所以它用TB的Create方法的地址替换
//继承下来的TA的方法表中TA的Create方法的地址,就
//构成了"表TB",这里没考虑其他的虚拟方法

TAClass=class of TA
//这里声明了一个类引用型别,实际上声明的是个指针
//型别,它可以指向类TA或其派生类所对应的表,如前述
//的"表TA"、"表TB"等
var a1,a2:TA;
aC:TAClass;
begin

{这里是由类调用构造方法} //这种形式的Create调用必然不会是动态的,首先可以
a1:=TB.Create
//明白语义是调用TB的Create,那么类TB确定了,就意味
//着TB所对应的表"表TB"确定了,一查表就查出了Create
//对应的地址,然后调用之。"表TB"中的Create是什么?
//当然是TB自己定义的Create,如前所述
//注意:"TB.Create"这个单一指令唤起的是确定的动作
//所以是静态调用的
//注意得到的结果:a1此刻是一个TB型别的对象

{这里是由一个已存在的} //这段语句才是重中之重,这种形式是动态调用的马?
{对象调用它的虚拟方法Create} //先看其流程,先调用a1这个已经存在的对象的方法Create,
a2:=a1.Create
//再把这个方法的返回值赋值给a2。怎么确定a1的Creat方
a1.Free
//法呢?a1这个已经创建的对象的第一个域指向这个对象所
a2.Free
//属的类,即"表TB",理由同由类调用create一样,
//"a1.Create"这个单一指令唤起的也是确定的动作
//所以这种方式也是静态调用的

{这里由一个类引用调用Create} //首先aC:=TA使aC指向"表TA",紧接着的"aC.Create"指令
aC:=TA
//是通过aC找到它所指向的"表TA"再找到"表TA"中的Create
a1:=aC.Create
//方法,a1此刻是个TA类对象
aC:=TB
//接下来aC指向"表TB",紧接着的"aC.Create"指令通过
a2:=aC.Create
//"表TB"找到的Create方法,因此a2现在是个TB类对象
//在这段代码中同样的指令"aC.Create"唤起的是不同
.... //的Create方法,不同的动作,因此这种方式正是一种
end
//多态。
*************************************************************************************
to lance2000:
对你的看法我的意见和pipi的回复一样,就不废话了
to sadj:
呵呵,感觉你满可爱,敢于表现自己也勇于承认错误,因此也勇于犯错误,这不是坏事,真
的,不是讽刺你。美国人教育他们的孩子就是鼓励他们犯错误但是不许犯同样的错误,我想
你的进步一定会很快,共勉。


to darkiss:
我想你大概对我有些误会,我的上一个回复也许确实有点罗索,语气有些说教,但我的真正
目的仅仅是为了"尽量清晰"的表达我的意思,以免导致象本帖对于静态函数的讨论一样,两
个观点都是对的但又恰好矛盾,为什么,因为一个说得张飞一个说得岳飞,都是阿飞:)
没有充分理解对方的观点就匆忙反驳。因此我要尽量清晰的表达我的意思。还有我在写那个
回复的时候自己也是受益匪浅让我自己对这段时间的学习有了个总结,越写越顺到后来严重
超长也非我的本意。并不是要卖弄。因为开篇我就说了:
"事实上同志们已经把问题讨论正确了,我现在只是来做番整理。"
所以实际上也没有卖弄的本钱。我只是个事后诸葛亮做番整理。
你的这句话我很赞同:
"技术日新月异,哪里有时间一项一项全搞的很清楚"
说得对,我们没有时间也没有精力也没有必要把每一项层出不穷的技术搞清楚,但是请问一
个对op底层模模糊糊的人怎么编写高质量的控件,怎么拥有对自己编写的代码的完全控制?
连自己写的究竟是什么都搞不清楚有资格称自己是程序员么?我们的客户能够相信这样的软
件么?更何况占用软件生命周期80%的调试和维护?
我想技术是要分"{轻、重、缓、急、}"的,
比如Html/css/javascript等等与我们没有太多联系的技术我们完全可以只做大体了解没有必要
深入其底层实现,而象数据结构/算法/模式/建模方法论/语言的底层机制/常用类库的架构等等
基础的东西我觉得是学的越深入越好,因为它们往往也是最不会过时的东西,而且它们之间往往
是触类旁通的,换句话说就是找到学习的最佳投入/回报比,最佳平衡点,把有限的时间精力投
入到回报价值最大的方面学习。比如读vcl的源码,何其庞大,我们真正学的是它的架构/规范,
这只牵涉有限的类,小规模的代码,但它的回报有多大!那是"胸中自有丘壑"的一种笃定。
在工作中并不核心的部分自然是什么简单用什么什么快用什么
有前辈说过:用往易处用,学往深处学

不废话了,最后我想说一点与技术无关的话。
首先谢谢你对我在dfw的第一篇帖子中给我的回复建议推荐的delphi in a nutsBell.
很不错。这里是我的第二篇回复帖子。
其次,给你一个忠告,不要随便对一个人的品行下结论,真正了解一个人需要十年?二十年
够么?我看未必够!
我以后也将继续奉行"每大事必作于细"的理念,大家奉行的理念原则作风有所不同,很
正常,可以求同存异嘛
oskiller以己度人,愿和darkiss兄在激烈的技术讨论中共同进步,其间高潮迭起。。。
 

莫知

Unregistered / Unconfirmed
GUEST, unregistred user!
讨论得真是精彩,在下受益非浅深为各位高手的技术所折服,但更佩服各位的人品和严谨的治学态度!
〉〉,感觉DELPHI和JAVA和.NET实在是极为神似,。。。
近时读李维文章发现,C#和Object Pascal几乎都是由Anders Hejlsberg亲手打造,而他同时也是
.Net的主要架构设计师,所以对于它们之间极为神似也就不为奇了!
感觉Borland真是出人才的地方,不但有Anders Hejlsberg,还有如Soft-ICE的作者也是从中出去的,
他是Turbo Debuger的主要开发者!
 
Z

ZXW49362727

Unregistered / Unconfirmed
GUEST, unregistred user!
谢谢大家。
想不到我的一个小问题能得到这么多大师的指点,真是很荣兴。
 
N

net_donkey

Unregistered / Unconfirmed
GUEST, unregistred user!
不知长了什么运气把这个帖子找到了:)
学习!
 

那一刀

Unregistered / Unconfirmed
GUEST, unregistred user!
D

de_wu

Unregistered / Unconfirmed
GUEST, unregistred user!
我的理解:一般情况下,同一个类的所有对象共享同样的类方法。即使
没有创建对象,类的方法也已被分配了内存。所有VCL组件都是从TObject
继承下来的。因此,free方法的代码在内存中是存在的。而且,所有
组件都可以调用这一方法。
 
Z

ZXW49362727

Unregistered / Unconfirmed
GUEST, unregistred user!
[:D][:D]这是我在大富翁中第一个回复如此之多的帖子。再次谢谢大家。
 
顶部