====================================
帮我解决一下这个问题吧:在dll代码AllocMem的内存,在exe代码中FreeMem就报错,内存实际是没有释放的。
====================================
常识性错误,这种内存分配行为和释放行为不在同一模块里面的程序设计风格是被明令禁止的。建议楼主去看看Delphi的帮助,搜索一下“Libraries and packages”这个主题,那里面对此有详细的说明。
DLL函数返回指针和函数动态分配/释放内存是两码事,不能混为一谈。不可否认有的DLL函数的确会返回指针,但并不表示该函数里面一定会动态分配内存。事实上,DLL函数只是负责数据处理(比如转换、复制、统计),但待处理数据具体是存储在哪里的,是怎么存储进去的,它是不需要关心的。待处理数据的准备(存储空间的分配、数据载入)和清理(存储空间释放)这些工作都是由调用DLL的那个程序负责的。DLL函数内部当然也可以动态分配内存,但这个动态分配的内存一定要由这个函数或同一个DLL里面的其他函数释放掉,不能让调用DLL的程序来释放。这是DLL编程的一个通用的铁律和常识,并不是Delphi所独有的,所有编程语言在编写DLL时都要遵守这个守则,因为这涉及到的是Windows内存共享的问题。楼主既然知道“谁调用谁释放”的道理,怎么还会有“但是作为需要返回指针的函数,怎么释放指针返回值(返回值不能在外部创建)?如果必须是函数,必须要返回pchar,该怎么做?”这样的疑问,你这种做法是明显违法上述DLL编程守则的--因为DLL和调用它的程序是两个不同的进程,一个进程(DLL)分配的内存怎么能让另一个进程(主程序)帮你释放?
返回值怎么会不能在外部创建?这只是你没有转变好思想罢了,如果用编写常规的同一模块内的函数的思想来编写DLL函数的话,当然会觉得把函数处理结果所需的存储空间交由调用这个函数的外部程序来分配是“不可理喻”的一件事情--但事实上,这就是DLL函数的“标准行为方式”。我举个简单的例子,假设要在DLL里面写一个过滤掉字符串里面所有重复字符的函数(为了简化问题,字符仅限于ASCII集合),函数的输入是一个待处理的源字符串,可能包含重复的字符;输出是一个处理结果字符串,包含源字符串中所有出现过的字符;源串中重复出现过的字符在目标串中只出现一次。
按照楼主的理解,函数处理结果(即目标串)所需的存储空间应该是由DLL函数分配的,然后函数在处理完成后把指向目标串的指针作为函数返回值返回给调用程序。如果你写的是和主程序在同一模块的非DLL函数的话,这么理解是没有任何问题的,但写在DLL里面的话就不能这么来理解了。因为DLL和主程序不在同一模块,把DLL分配的内存交由主程序去释放是铁定会造成“非法内存访问”错误的。所以必须对函数的设计作一定的修改,以符合“谁分配谁释放”的原则。下面是我根据上面假设的这个处理需求所写的一个DLL形式的函数--
funciton DelReduChar(Source,Dest
Char):integer; stdcall;
var
i,j:integer;
ExistChars:set of char;
begin
if Source<>nil then
begin
ExistChars:=[];
j:=0;
for i:=0 to Length(Source)-1 do
if not (Source
in ExistChars) then
begin
Include(ExistChars,Source);
if Dest<>nil then
Dest[j]:=Source;
inc(j)
end;
Result:=j
end
else
Result:=0
end;
这就是数据处理类型DLL函数基本框架--即函数本身不进行任何为处理结果准备的存储空间分配动作,而是通过一个指针参数让主调程序自己提供存放处理结果的缓冲区的地址。函数根据这个地址把处理结果输出到主程序指定的缓冲区去,并通过函数返回值返回处理结果实际所需的存储空间的大小。也就是说,在DLL函数里面,如果处理结果所需的存储空间长度是不定的,那么就不能由函数自己分配内存,并把处理结果的地址返回给主程序的方式。而应该采用一边进行处理,一边根据主程序是否已经提供了结果缓冲区地址决定是否输出处理结果,处理完成后把存放最终结果所需的存储空间的容量返回给主程序的方式。
因此,主程序在调用上面这个实例函数时,就应该是这样的--
...
var
SourceStr,DestStr:string;
Size:integer;
...
Size:=DelReduChar(Pointer(SourceStr),nil);
// 第一步:用空目标指针第一次调用函数,探测处理结果所需的内存
if Size>0 then
begin
SetLength(DestStr,Size); // 第二步:为处理结果分配内存
// 第三步:用指向缓冲区的指针第二次调用函数,得到处理结果
if DelReduChar(Pointer(SourceStr),Pointer(DestStr))=Size then
showmessage(DestStr)
end;
...
也就是说,动态数据处理结果DLL函数的标准调用过程是这样的:首先用空目标指针探测到处理结果所需的内存大小,然后分配内存,最后用指向缓冲区的指针再次调用函数得到处理结果。就正如前面sed朋友所说的那样“需要调用两次”。用这种思维方式来编写和调用DLL函数,就不会出现在DLL里面分配,在DLL外面释放的这种“需要”了,完全符合“谁分配谁释放”的原则。顺带说一下,上面我举的这个例子程序没有给出释放内存的代码,原因是string的内存是动态管理的,当DestStr所在的那个模块结束的时候,内存管理模块会自动释放掉它占用的内存。所以我没有必要在自己的程序里面写上“SetLength(DestStr,0)”之类的“废话”。