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是一种落后的管理字符串的方式,而string则以高效的管理而胜出,PChar它的存在只是为了兼容早期的类型和操作系统(调用Windows API时会经常用到),建议平常使用string。
Delphi中String类型原理介绍的更多相关文章
- DELPHI中枚举类型数据的介绍和使用方法
在看delphi程序的时候看到aa=(a,b,c,d);这样的东西,还以为是数组,同事说是函数,呵呵,当然这两个都不屑一击,原来这样式子是在声明并付值一个枚举类型的数据.下边写下来DELPHI中枚举类 ...
- Java中String类型细节
Java中String类型细节 一 . String两种初始化方式 1 . String str1= “abc”;//String类特有的创建字符对象的方式,更高效 在字符串缓冲区中检测”abc”是否 ...
- Java中String类型详解
这篇博客是我一直想总结的,这两天一直比较忙,先上传下照片吧,过后有时间再弄成正常的. 本文主要是对Java中String类型的总结,包括其在JVM中是怎么存储的...
- UWP中String类型如何转换为Windows.UI.Color
原文:UWP中String类型如何转换为Windows.UI.Color 我在学习过程中遇到的,我保存主题色为string,但在我想让StatusBar随着主题色变化时发现没法使用. ThemeCol ...
- Redis 中 String 类型的内存开销比较大
使用 String 类型内存开销大 1.简单动态字符串 2.RedisObject 3.全局哈希表 使用 Hash 来存储 总结 参考 使用 String 类型内存开销大 如果我们有大量的数据需要来保 ...
- Delphi中返回类型为string的函数的一个陷阱(不是很懂)
如果类的一个成员函数的返回值是string类型,需要注意一个问题 其返回值可能是错误的 例如函数的实现如下 function GetString( s: string ): string;begin ...
- C#中string类型是值类型还是引用类型?
.Net框架程序设计(修订版)中有这样一段描述:String类型直接继承自Object,这使得它成为一个引用类型,也就是说线程上的堆栈上不会驻留有任何字符串. string类型(引用类型) 名称 CT ...
- 【转载】 Java中String类型的两种创建方式
本文转载自 https://www.cnblogs.com/fguozhu/articles/2661055.html Java中String是一个特殊的包装类数据有两种创建形式: String s ...
- C#中string类型是值类型还是引用类型?(转)
出处:https://www.cnblogs.com/dxxzst/p/8488567.html .Net框架程序设计(修订版)中有这样一段描述:String类型直接继承自Object,这使得它成为一 ...
随机推荐
- hibernate里的generator中class =value介绍
在*.hbm.xml必须声明的<generator>子元素是一个Java类的名字,用来为该持久化类的实例生成唯一的标识.<generator class="sequence ...
- Sasha and Array
Sasha and Array time limit per test 5 seconds memory limit per test 256 megabytes input standard inp ...
- mr本地运行的几种模式
MR程序的几种提交运行模式 本地模型运行 1/在windows的eclipse里面直接运行main方法,就会将job提交给本地执行器localjobrunner执行 ----输入输出数据可以放在本地路 ...
- Hadoop集群搭建的密钥配置SSH实现机制的配置(2)
[hadoop@weekend110 ~]$ ssh-keygen -t rsa 用来生产密钥对 Generating public/private rsa key pair. Enter file ...
- PAT (Advanced Level) 1068. Find More Coins (30)
01背包路径输出. 保证字典序最小:从大到小做背包. #include<cstdio> #include<cstring> #include<cmath> #inc ...
- css中margin重叠和一些相关概念(包含块containing block、块级格式化上下文BFC、不可替换元素 non-replaced element、匿名盒Anonymous boxes )
平时在工作中,总是有一些元素之间的边距与设定的边距好像不一致的情况,一直没明白为什么,最近仔细研究了一下,发现里面有学问:垂直元素之间的margin有有互相重叠的情况:新建一个BFC后,会阻止元素与外 ...
- zepto学习之路--源代码提取
最近在看zepto的源代码,把一些有用的函数摘出来,看看zepto是怎么实现的,自己做的时候也可以用.说实话,zepto的实现有一些看起来还是很晦涩的,可能是自己的水平不够,看不透作者的真正的意图. ...
- Delphi XE7 Update1修正列表
Delphi XE7 Update1修正列表 官方下载地址:http://altd.embarcadero.com/download/radstudio/xe7/delphicbuilder_xe7_ ...
- SpringMVC轻松学习-其他常用(四)
Spring MVC 3.0 深入 核心原理 1. 用户发送请求给服务器.url:user.do 2. 服务器收到请求.发现DispatchServlet可以处理.于是调用Disp ...
- Cannot call sendError() after the response has been committed - baiyangliu - 博客频道 - CSDN.NET
body{ font-family: "Microsoft YaHei UI","Microsoft YaHei",SimSun,"Segoe UI& ...