讨论与学习:返回对象的函数的内存释放问题 (300分)

Y

ysai

Unregistered / Unconfirmed
GUEST, unregistred user!
//下面的程序只是个演示
//我写的类似的函数还有返回DataSet等对象的
//程序很简单,但可能问题没有好的答案
unit Unit1;

interface

uses
Windows, Messages, SysUtils, Classes, Forms, Controls, StdCtrls, Buttons;

type
TForm1 = class(TForm)
Button1: TButton;
Button2: TButton;
Button3: TButton;
Button4: TButton;
Button5: TButton;
lst: TListBox;
procedure Button1Click(Sender: TObject);
procedure Button2Click(Sender: TObject);
procedure Button3Click(Sender: TObject);
procedure Button4Click(Sender: TObject);
procedure Button5Click(Sender: TObject);
private
{ Private declarations }
public
{ Public declarations }
end;

var
Form1: TForm1;

implementation

{$R *.dfm}

//一个函数,返回有1000个字符串的字符串列表
function GetList:TStrings;
var
i:Integer;
begin
Result:=TStringList.Create;
for i:=1 to 1000 do
Result.Add(IntToStr(i));
end;

//第一种方法,过程1
procedure TForm1.Button1Click(Sender: TObject);
var
lt:TStrings;
begin
lst.Clear;
lt:=Getlist;
lst.Items.AddStrings(lt);
lt.Free;
//内存正常释放,标准的写法
end;

//第二种方法,过程2
procedure TForm1.Button2Click(Sender: TObject);
begin
lst.Clear;
lst.Items.AddStrings(GetList);
//偷懒的方法,但内存没有被释放,可以在任务管理器中看出
end;

//第三种方法,过程3
procedure TForm1.Button3Click(Sender: TObject);
var
lt:TStrings;
begin
lst.Clear;
lt:=Getlist;
lst.Items.AddStrings(lt);
//这里没有lt.Free
lst.Clear;
lt:=Getlist;
lst.Items.AddStrings(lt);
lt.Free;
//很容易出现的错误,虽然最后释放了lt
//但它第一次引用的对象丢失了,没有被释放
//内存像第二种一样增加
end;

//再试一个例子
//下面的过程多次调用也会增加使用内存
//过程4
procedure TForm1.Button4Click(Sender: TObject);
var
fm:TForm;
lst:TListBox;
begin
fm:=TForm.Create(self);
lst:=TListBox.Create(fm);
lst.Parent:=fm;
lst.Align:=alClient;
//直接用:=,没有用AddStrings
lst.Items:=GetList;
fm.ShowModal;
fm.Free;
end;

//而改成这样就不会了
//过程5
procedure TForm1.Button5Click(Sender: TObject);
var
fm:TForm;
lst:TListBox;
ls:TStrings;
begin
fm:=TForm.Create(self);
lst:=TListBox.Create(fm);
lst.Parent:=fm;
lst.Align:=alClient;
ls:=GetList;
lst.Items.AddStrings(ls);
ls.Free;
fm.ShowModal;
fm.Free;
end;

end.
//问题1:(过程1,2,3)
//有没有其它方法,可以让第二种方法(过程2)释放内存
//也就是说:
//不声明一个变量而直接使用函数返回的对象,要求对象自释放

//问题2:(过程4,5)
//照理说fm.Free也释放了lst,当然lst.Items也释放了,
//lst.Items指向的对象,也就是GetList函数取得的Strings也释放了,但......
//过程4造成内存增加,而5没有
//难道lst.Items:=GetList;也会造成对象丢失吗?(不好怎么称呼这种现象)
//是不是lst.Items原来指向的对象没有释放而且没有了管理者???
//如果是的,直接用lst.Items.Free是会出错的,那么这种情况怎么避免?
 
我来说一种方法,
建立一个全局变量Objs:TList;
在函数内部创建的对象,并登记到此链表内
然后用定时器,定时检测并释放其中的对象
由于当前过程退出前一般不会执行其他的过程,
所以定时器的代码也不会执行,等到过程退出的时候,
在很短的时间内,定时器的代码就会执行,
来完成垃圾收集的任务

这种方法比你的所有方法都要好
 
To:ysai:
这种问题在目前没什么理想的解决方法,你想省事不手动释放内存
只有等Delphi.net出来了。
To LiChaoHui:
hehe,一种模拟GC?
这可够浪费资源的,一般的GC在资源耗尽的时候开始执行.
 
//要求对象自释放
哈哈,我不是刚提过这个问题吗,不过你要重载那个类,也比较麻烦:(
参见:http://www.delphibbs.com/delphibbs/dispq.asp?lid=1556758

探讨一下,说不定能想出更好的办法:)

 
我一般遵循3个原则:
1、最好不要把临时创建的对象作为函数返回值。
2、把对象作为参数传递给函数;
3、对象的create和free最好/尽量在一个代码块。

基于这个原则,如果我写你的代码如下:

function GetList(lst:TStrings):boolean
//对象作为参数传递
var
i:Integer;
begin
for i:=1 to 1000 do
lst.Add(IntToStr(i));
result:=true;
end;

procedure TForm1.Button1Click(Sender: TObject)

var
lt:TStrings;
begin
lt:=tstringlist.create
//create/free在一个代码块
if Getlist(lt) then
lst.Items.AddStrings(lt);
lt.Free;
end;
 
To beta:
跟你的情况有所不同,你的问题是要避免有悬空指针(指向失效地址的指针);
yasi是要自动销毁不用的对象。
一个过客的方法有道理,把Create和Free放在不同的过程肯定降低了程序
的可读性,增加了出错的概率。

解决这个问题的根本方法还是Java和.net里的GC机制,但这个手动管理的
执行效率有天壤之别.....
 
to xeen: 这我知道,我是认为那个思路可以借鉴:)
但是用那个思路必须要人家肯重载那个类才行啊:)
我再考虑一下。。。

to 一个过客: 我觉得楼主的意思好像不是这个,他就是想省下那个变量声明。
换句话说,他不是怕那个对象 Free 不调,而是不想显示的把它 Free 掉,
也许我理解错了:)

 
可惜 Delphi 没有静态类成员,否则可以置一个计数器,什么都搞定了:(

 
to all:
呵,高手都来了呀,其实我只是想偷懒少写几句代码而已~~~~
to LiChaoHui:
有点意思,不过相对来说我还是愿意自己释放.
to xeen:
对,我就是想自动Free,等.net那我现在玩什么?说过这个问题可能没有好答案,讨论讨论吧
to 一个过客:
有道理,虽然可读性差点,但安全
to bata:
久闻大名,您的文章我也看过,很好,但还没机会用上:)
 
Delphi目前采用的自动管理内存的方法主要有两种:
1.Owner,在Create的时候指定Owner,等Owner释放的释放的时候释放所有的Components,
要求必须从TComponent派生的类才行。
2.接口,以接口变量来引用对象实例,用引用计数的方式。当所有引用对象的接口变量
销毁后释放对象。这要求类必须实现了一个接口。

看看能不能用上。
 
to xeen:
像TStrings这种类好象没办法用第一种方法
用接口实现可以返回像TClientDataSet这样的对象吗?如果可以,怎么写?

To All:
题目有改动.改为5个过程,帮忙看看后两个
 
高手不敢当,瞎琢莫而已:)

你的第四个的确有问题!!

lst.Items:=GetList;

问题就出在这句话,Items 是 lst 的一个私有成员,通过 public 属性公布出来,
本在 lst 创建的时候一起创建了(或这个过程在其基类,意思你明白)。
可是你这样一行代码执行了过后,GetList 新创建了一个 TStringList 的实例,且
将该实例指针赋给了 Items,也就是说 Items 原来指的实例现在已经无人认领了:)
所以,在 lst 释放的时候(不管是否随 fm 一起),它所释放的 Items 已经不是
原来哪个 Items 了,而是你新生成的实例。这个新生成的实例的确释放了,可是呢,
“原配”就······ :)

 
To ysai:
没戏,所以说现在自动内存管理机制有很大的限制,还得等.net的自动垃圾收集.
To beta:
真不幸你又错了.
Items是TListBox的一个属性,对属性的访问实际意味着对一个方法SetxxxValues(写
操作),GetxxxValues(读操作)的调用.类似于一种特殊的操作符重载。
List.Items := GetList;
编译器会把这条语句换成:
List.SetItems(GetList);
看看TCustomListBox.SetItems的源码你只知道了,它知己调用了TStrings的Assign过程,
TStrings.Assign会清除TStrings对象里的数据并调用TStrings.AddStrings将GetList
的数据复制过来.
所以 List.Items := GetList;并没有把List.Items的指向没有变,只不过复制了
GetList对象数据而已,由于两者间没有联系,当List对象释放的时候,GetList返回
的指针指向的对象也不会释放.
 
create 了對象 就必須 free
你可能會說 tform 會自動 Free tedit
你可以看下面的代碼

constructor TComponent.Create(AOwner: TComponent);
begin
FComponentStyle := [csInheritable];
if AOwner <> nil then AOwner.InsertComponent(Self);
end;

tComponent中有個 FComponents: TList
用來保存它的下屬對象列表
tComponent 對象free時會調用 DestroyComponents;來释放所有下屬對象
所以說Tedit也不是自動释放的
比如你用 edt1:=TEdit.create(nil);
來 create 一個 edit也必須自己释放,沒人會幫你释放
更不用說TStringList這種對象了

保存在 StringList.items[0].data 中的對象更是要在 StringList.free前 
全部释放掉不然這塊內存就沒了。
//4
lst.Items:=GetList;
你每調用一次 GetList 就會 Create一個 TStringlist
調用時對象的指针保存在 result 中,也就是保存在一個寄存器中
你不及時释放的話,這對象找都找不到了,內存當然也沒了
//5 中
ls:=GetList;
lst.Items.AddStrings(ls);
ls.Free;
把 ls 等於返回的對象,然後释放,當然時沒有問題了
 
to xeen: 多谢指出,的确是我的错。的确是新的实例没有被释放,“原配”无恙:)
呵呵,当时没有想得很仔细,就贴上来了,失败。

至少有一点是可以肯定的,这段代码只能释放一个 Strings :)

不过我觉得,如果反过来想,恐怕更好一些,你原来是:
lst.Items.AddStrings(Getlist);
把取得的列表加给 lst。反过来,直接把 lst 传进去进行修改,就不需要新建一个
实例,于是就不存在释放的问题了:
GetList(lst.Items);
接下来你应该知道怎么做了:)

不过遗憾的是,我觉得这恐怕不是你的初衷,你是想构造一个自动释放的类/对象。
暂时还没有想出来:(

唉,没有时间了,马上得把电脑装箱,明天回家了:)
祝大家好运。
 
看到这么多意见确实学了不少东西,长了见识,大家讨论的这么热闹,我也说两句。
其实上面的问题关键之处是在于object pascal和vcl的内存分配管理机制问题,具体到本问题就是
在变量赋值时是使用的指针赋值还是复制赋值。对于一般变量在delphi中一般使用的是复制赋值,
也就是说在赋值发生时,系统内存管理自动申请一块新内存来存放数据,赋值完成后就与原数据不
再有任何联系;而在对象间赋值时一般使用的是指针赋值,也就是说当赋值发生是系统只是单单的
将原变量的地址传给赋值变量,这时这两个变量其实使用的是同一块内存,当其中一个释放时,此
块共有的内存就会被系统回收,这时就会发生对象丢失,如果再次引用对象就会出现“存取地址非
法”的异常。另外如果在系统建立一个对象后,指向此对象的指针(此对象的实例)因故丢失,此
时就会发生内存泄漏,也就是说此对象实例的内存不能再为系统所使用。
在上面楼主的例子中,函数getlist返回的就是一个对象(tstrings):
//第一种方法,过程1
procedure TForm1.Button1Click(Sender: TObject);
var
lt:TStrings;
begin
lst.Clear;
lt:=Getlist
//此时getlist 的返回的对象由lt接管,它们实际指向同一内存
lst.Items.AddStrings(lt)
//这里使用的是复制赋值
lt.Free;          //lt释放,由于lt和getlist返回的对象指向同一内存,所以不会产生垃圾
//内存正常释放,标准的写法
end;

//第二种方法,过程2
procedure TForm1.Button2Click(Sender: TObject);
begin
lst.Clear;
lst.Items.AddStrings(GetList);  //由于使用了复制赋值,但getlist返回的对象却没有被任何变
                //量捕获,并且它不在本过程中定义,不会自动释放,所以产生了垃圾
                //如果将getlist的定义改在本过程内,就不会产生内存垃圾了  
//偷懒的方法,但内存没有被释放,可以在任务管理器中看出
end;

//第三种方法,过程3
procedure TForm1.Button3Click(Sender: TObject);
var
lt:TStrings;
begin
lst.Clear;
lt:=Getlist;   //这里使用了指针赋值,
lst.Items.AddStrings(lt);  //这里是复制赋值
//这里没有lt.Free
lst.Clear;
lt:=Getlist;  //再次使用getlist时,它又产生了一个对象,当将该新对象赋值给lt时,
        //指向原来对象的指针就丢失了,系统已经没有办法再来管理,产生了垃圾
lst.Items.AddStrings(lt);
lt.Free;  //这里只是释放的第二次产生的对象,而第一次产后的对象已经丢失。
 //同样,在本过程退出时系统不会自动释放由getlist函数产生的对象,
 
//很容易出现的错误,虽然最后释放了lt
//但它第一次引用的对象丢失了,没有被释放
//内存像第二种一样增加
end;

//再试一个例子
//下面的过程多次调用也会增加使用内存
//过程4
procedure TForm1.Button4Click(Sender: TObject);
var
fm:TForm;
lst:TListBox;
begin
fm:=TForm.Create(self);
lst:=TListBox.Create(fm);
lst.Parent:=fm;
lst.Align:=alClient;
//直接用:=,没有用AddStrings
lst.Items:=GetList;  //这里和上面的一样,是复制赋值,虽然lst是由fm来释放了,但getlist却是在本过程外
            //定义,它产生的对象不会被系统自动释放
fm.ShowModal;
fm.Free;
end;

//而改成这样就不会了
//过程5
procedure TForm1.Button5Click(Sender: TObject);
var
fm:TForm;
lst:TListBox;
ls:TStrings;
begin
fm:=TForm.Create(self);
lst:=TListBox.Create(fm);
lst.Parent:=fm;
lst.Align:=alClient;
ls:=GetList;
lst.Items.AddStrings(ls)
//这个就不要分析了,一样的结果
ls.Free;   
fm.ShowModal;
fm.Free;
end;
 
要求对象自释放就不可能了.这是机制的问题.

但是不定义变量也是可以的,用with语句嘛:

Function GetList:TStrings;
Var i:Integer;
begin
Result:=TStringList.Create;
for i:=0 to 1000 do
Result.Add(IntToStr(i));
end;

procedure TForm1.Button1Click(Sender: TObject);
begin
with GetList do
begin
try
ListBox1.Items.Text:=Text;//当然,没有变量名了,所以不能用AddStrings方法了.
finally
Free;
end;
end;
end;
 
我不太喜欢用with语句,感觉对程序的可读性没什么好处。
一个局部指针变量才4个字节,费不了多少空间.
 
最主要的是保证资源的正确和有效的释放,
我的方法还是比较可行的
 

fstringlist:tstringlist写在类中的private段。
create 一个form or class 时:
fstringlist:=tstringlist.create;
在自己的程序段中这样:
function sss(val:string):Tstringlist;
begin
不用result:=tstringlist.create!!!这样就不用result.free了。
直接操作fstringlist,
....
最后result:=fstringlist;
end;

form or class的destroy中。
fstringlist.free;
这样就可以不用管释放内存了,form or class destroy时,就自动释放tstringlist的内存了!
这样应该可以吧!
 

Similar threads

S
回复
0
查看
3K
SUNSTONE的Delphi笔记
S
S
回复
0
查看
2K
SUNSTONE的Delphi笔记
S
I
回复
0
查看
668
import
I
顶部