Delphi中String类型原理介绍
Delphi中字符串的操作很简单,但幕后情况却相当复杂。Pascal传统的字符串操作⽅法与Windows不同,Windows吸取了C语⾔的字符串操作⽅法。32位Delphi中增加了长字符串类型,该类型功能强⼤,是Delphi缺省的字符串类型。
字符串类型在Borland公司的TurboPascal和16位Delphi中,传统的字符串类型是⼀个字符序列,序列的头部是⼀个长度字节,指⽰当前字符串的长度。由于只⽤⼀个字节来表⽰字符串的长度,所以字符串不能超过255个字符。这⼀长度限制为字符串操作带来不便,因为每个字符串必须定长(确省最⼤值为255),当然你也可以声明更短的字符串以节约存储空间。
字符串类型与数组类型相似。实际上⼀个字符串差不多就是⼀个字符类型的数组,因此⽤[]符号,你就能访问字符串中的字符,这⼀事实充分说明了上述观点。
为克服传统Pascal字符串的局限性,32位Delphi增加了对长字符串的⽀持。这样共有三种字符串类型:
ShortString  短字符串类型也就是前⾯所述的传统Pascal字符串类型。这类字符串最多只能有255个字符,与16位Delphi中的字符串相同。短字符串中的每个字符都属于
ANSIChar类型(标准字符类型)。字符串常量在内存中的存放位置
ANSIString  长字符串类型就是新增的可变长字符串类型。这类字符串由内存动态分配,引⽤计数,并使⽤了更新前拷贝(copy--on-write)技术。这类字符串长度没有限制(可以存储多达20亿个字符!),其字符类型也是ANSIChar类型。
WideString  长字符串类型与ANSIString 类型相似,只是它基于WideChar字符类型,WideChar字符为双字节Unicode字符。
使⽤长字符串
如果只简单地⽤String定义字符串,那么该字符串可能是短字符串也可能是ANSI长字符串,这取决于$H编译指令的值,$H+(确省)代表长字符串(ANSIString类型)。长字符串是Delphi库中控件使⽤的字符串。
Delphi长字符串基于引⽤计数机制,通过引⽤计数追踪内存中引⽤同⼀字符串的字符串变量,当字符串不再使⽤时,也就是说引⽤计数为零时,释放内存。
如果你要增加字符串的长度,⽽该字符串邻近⼜没有空闲的内存,即在同⼀存储单元字符串已没有扩展的余地,这时字符串必须被完整地拷贝到另⼀个存储单元。当这种情况发⽣时,Delphi运⾏时间⽀持程序会以完全透明的⽅式为字符串重新分配内存。为了有效地分配所需的存储空间,你可以⽤SetLength过程设定字符串的最⼤长度值,如:
SetLength  (String1,  200);
SetLength过程只是完成⼀个内存请求,并没有实际分配内存。它只是把将来所需的内存预留出来,实际上并没有使⽤这段内存。这⼀技术源于Windows操作系统,现被
Delphi⽤来动态分配内存。例如,当你请求⼀个很⼤的数组时,系统会将数组内存预留出来,但并没有把内存分配给数组。
⼀般不需要设置字符串的长度,不过当需要把长字符串作为参数传递给API函数时(经过类型转换后),你必须⽤SetLength为该字符串预留内存空间,这⼀点我会在后⾯进⾏说明。
看⼀看内存中的字符串
为了帮你更好地理解字符串的内存管理细节,我写了⼀个简例StrRef。在程序中我声明了两个全程字符串:Str1和Str2,当按下第⼀个按钮时,程序把⼀个字符串常量赋给第⼀个变量,然后把第⼀个变量赋给第⼆个:
Str1  :=  'Hello';
Str2  :=  Str1;
除了字符串操作外,程序还⽤下⾯的StringStatus函数在⼀个列表框中显⽰字符串的内部状态:
function  StringStatus  (const  Str:  string):  string;
begin
Result  :=  'Address:  '  +  IntToStr  (Integer  (Str))  +
',  Length:  '  +  IntToStr  (Length  (Str))  +
',  References:  '  +  IntToStr  (PInteger  (Integer  (Str)  -  8)^)  +
',  Value:  '  +  Str;
end;
在StringStatus函数中,⽤常量参数传递字符串⾄关重要。⽤拷贝⽅式(值参)传递会引起副作⽤,因为函数执⾏过程中会产⽣⼀个对字符串的额外引⽤;与此相反,通过引⽤(var)或常量(const)参数传递不会产⽣这种情况。由于本例不希望字符串被修改,因此选⽤常量参数。为获取字符串内存地址(有利于识别串的实际内容也有助于观察两个不同的串变量是否引⽤了同⼀内存区),我通过类型映射把字符
串类型强⾏转换为整型。字符串实际上是引⽤,也就是指针:字符串变量保存的是字符串的实际内存地址。
为了提取引⽤计数信息,我利⽤了⼀个鲜为⼈知的事实:即字符串长度和引⽤计数信息实际上保存在字符串中,位于实际内容和字符串变量所指的内存位置之前,其负偏移量对字符串长度来说是-4(⽤Length函数很容易得到这个值),对引⽤记数来说是-8。
不过必须记住,以上关于偏移量的内部信息在未来的Delphi版本中可能会变,没有写⼊正式Delphi⽂档的特性很难保证将来不变。
通过运⾏这个例⼦,你会看到两个串内容相同、内存位置相同、引⽤记数为2,如图7.1中列表框上部所⽰。现在,如果你改变其中⼀个字符串的值,那么更新后字符串的内存地址将会改变。这是copy-on-write技术的结果。
第⼆个按钮(Change)的OnClick事件代码如下,结果如图7.1列表框第⼆部分所⽰:
procedure  TFormStrRef.BtnChangeClick(Sender:  TObject);
begin
Str1  [2]  :=  'a';
ListBox1.Items.Add  ('Str1  [2]  :=  ''a''');
ListBox1.Items.Add  ('Str1  -  '  +  StringStatus  (Str1));
ListBox1.Items.Add  ('Str2  -  '  +  StringStatus  (Str2));
end;
注意,BtnChangeClick只能在执⾏完BtnAssignClick后才能执⾏。为此,程序启动后第⼆个按钮不能⽤(按钮的Enabled属性设成False);第⼀个⽅法结束后激活第⼆个按钮。你可以⾃由地扩展这个例⼦,⽤StringStatus函数探究其它情况下长字符串的特性。
动态分配可以⽤任意⼀个分配内存的函数, 其实系统最终调⽤的都是GetMem, 其它的New、AllocMem、SetLength等等只不过除了调⽤GetMem外还做了⼀些初始化处理⽐如把内存清零。释放可以⽤Dispose或者FreeMem, 系统最终都是调⽤FreeMem的, Dispose相当于
Finalize(p);  FreeMem(p);
Finalize的作⽤简单说就是⾃动释放结构或者数组中的string和动态数组,  FreeMem则是直接释放指针所指向的内存,例如:
type
TMyRec  =  record
Name:  string;
X,  Y:  Integer;
end;
PMyRec  =  ^TMyRec;
var
MyRec  :  PMyRec;
begin
New(MyRec);          //  编译器会根据MyRec的⼤⼩⾃动计算需要分配的内存数量然后⽣成代码调⽤GetMem并将其中的Name字段清零
MyRec.Name  :=  str1  +  str2;
Dispose(MyRec);    //  除了调⽤FreeMem释放MyRec这个结构的内存外还会⾃动清除其中的Name所⽤到的内存(如果Name指向的string 引⽤计数=1时);
//  FreeMem(MyRec);  <--  如果直接调⽤FreeMem释放MyRec,  则会造成内存泄露,  因为MyRec.Name指向的字符串没有释放(引⽤计数-1)
end;
由于delphi关于string的内存管理的特殊性, 可以有很多技巧充分利⽤其优点⽣成⾮常⾼效的代码, ⽐如要⽤TList来保存string(不是TStringList),  ⼀般的做法是TList.Items[i]中保存⼀个PString指针, 这样就需要重新分配⼀块内存并复制原串, ⼤数据量的情况下效率很低, 但是如果充分利⽤string的引⽤计数和强制类型转换技巧, 可以直接将string作为指针保存在TList.Items[i]中:  ⽐如:
var
List:  TList;
GlobalString1,  GlobalString2:  string;
...
procedure  Test;
var
tmp:  string;
begin
tmp  :=  GlobalString1+GlobalString2;
List.Add(Pointer(tmp));    //  将tmp作为指针保存进List
{  由于Test过程结束时会⾃动释放掉tmp, 如果直接退出的话List中就保存了⼀个⽆效的指针了, 所以这⾥要欺骗编译器, 让它认为tmp已经被释放掉了, 等于在不改动tmp引⽤计数(当前是1)的情况下执⾏相当于tmp  :=  ''的语句, 由于直接tmp  := ''会修改引⽤计数并可能释放掉内存, 所以⽤强制类型转换将tmp转成⼀个Integer并将这个Integer设置成0(也就是nil), 此语句完全等价于pointer(tmp) :=  nil; 只是个⼈喜好我喜欢⽤Integer(tmp) := 0⽽已.
}
Integer(tmp)  :=  0;
end;
1. string是Delphi编译器内在⽀持的(predefined or built-in),是Delphi的⼀个基本数据类型,⽽PChar只是⼀个指向零终⽌字符串的指针;
2. String 所存字符串是在堆分配内存的,String变量实际上是指向零终⽌字符串的指针,与此同时它还具有引⽤计数(reference count)功能,并且⾃⾝保存字符串长度,当引⽤计数为零时,⾃动释放所占⽤的空间。
3.将string赋值给另⼀个string,只是⼀个简单的指针赋值,不产⽣copy动作,只是增加string的引⽤计数;
4.将⼀个PChar变量类型赋值给⼀个string  变量类型会产⽣真正的Copy动作,即将PChar所指向的字符串整个copy到为string分配的内存中;
5.将string赋值给⼀个PChar变量类型,只是简单地将string的指针值赋值给PChar变量类型,⽽string的引⽤计数并不因此操作⽽发⽣变化,因为这种情况PChar会对string产⽣依赖,当string的引⽤计数为零⾃动释放内存空间后,PChar很可能指向⼀个⽆效的内存地址,在你的程序你必须⼩⼼对付这种情况。
6.对PChar的操作速度要远远⾼于对string操作的速度,但PChar是⼀种落后的管理字符串的⽅式,⽽stri
ng则以⾼效的管理⽽胜出,PChar 它的存在只是为了兼容早期的类型和操作系统(调⽤Windows API时会经常⽤到),建议平常使⽤string。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。