在網(wǎng)上看到一些介紹delphi中String類型的文章,受益菲淺,,確定將其一一摘錄,,放在blog中,認(rèn)真學(xué)習(xí),、思考,!
[原文]
Delphi中字符串的操作很簡單,但幕后情況卻相當(dāng)復(fù)雜,。Pascal傳統(tǒng)的字符串操作方法與Windows不同,,Windows吸取了C語言的字符串操作方法。32位Delphi中增加了長字符串類型,,該類型功能強(qiáng)大,,是Delphi缺省的字符串類型,。
字符串類型在Borland公司的TurboPascal和16位Delphi中,,傳統(tǒng)的字符串類型是一個字符序列,序列的頭部是一個長度字節(jié),,指示當(dāng)前字符串的長度,。由于只用一個字節(jié)來表示字符串的長度,所以字符串不能超過255個字符,。這一長度限制為字符串操作帶來不便,,因為每個字符串必須定長(確省最大值為255),當(dāng)然你也可以聲明更短的字符串以節(jié)約存儲空間,。
字符串類型與數(shù)組類型相似,。實際上一個字符串差不多就是一個字符類型的數(shù)組,因此用[]符號,你就能訪問字符串中的字符,,這一事實充分說明了上述觀點,。
為克服傳統(tǒng)Pascal字符串的局限性,32位Delphi增加了對長字符串的支持,。這樣共有三種字符串類型:
ShortString 短字符串類型也就是前面所述的傳統(tǒng)Pascal字符串類型,。這類字符串最多只能有255個字符,與16位Delphi中的字符串相同,。短字符串中的每個字符都屬于
ANSIChar類型(標(biāo)準(zhǔn)字符類型),。
ANSIString 長字符串類型就是新增的可變長字符串類型。這類字符串由內(nèi)存動態(tài)分配,,引用計數(shù),,并使用了更新前拷貝(copy--on-write)技術(shù)。這類字符串長度沒有限制(可 以存儲多達(dá)20億個字符?。?,其字符類型也是ANSIChar類型。
WideString 長字符串類型與ANSIString 類型相似,,只是它基于WideChar字符類型,,WideChar字符為雙字節(jié)Unicode字符。
使用長字符串
如果只簡單地用String定義字符串,,那么該字符串可能是短字符串也可能是ANSI長字符串,,這取決于$H編譯指令的值,$H+(確?。┐黹L字符串(ANSIString類型),。長字符串是Delphi庫中控件使用的字符串。
Delphi長字符串基于引用計數(shù)機(jī)制,,通過引用計數(shù)追蹤內(nèi)存中引用同一字符串的字符串變量,,當(dāng)字符串不再使用時,也就是說引用計數(shù)為零時,,釋放內(nèi)存,。
如果你要增加字符串的長度,而該字符串鄰近又沒有空閑的內(nèi)存,,即在同一存儲單元字符串已沒有擴(kuò)展的余地,,這時字符串必須被完整地拷貝到另一個存儲單元。當(dāng)這種情況發(fā)生時,,Delphi運(yùn)行時間支持程序會以完全透明的方式為字符串重新分配內(nèi)存,。為了有效地分配所需的存儲空間,你可以用SetLength過程設(shè)定字符串的最大長度值,如:
SetLength (String1, 200);
SetLength過程只是完成一個內(nèi)存請求,,并沒有實際分配內(nèi)存,。它只是把將來所需的內(nèi)存預(yù)留出來,,實際上并沒有使用這段內(nèi)存。這一技術(shù)源于Windows操作系統(tǒng),,現(xiàn)被
Delphi用來動態(tài)分配內(nèi)存,。例如,當(dāng)你請求一個很大的數(shù)組時,,系統(tǒng)會將數(shù)組內(nèi)存預(yù)留出來,,但并沒有把內(nèi)存分配給數(shù)組。
一般不需要設(shè)置字符串的長度,,不過當(dāng)需要把長字符串作為參數(shù)傳遞給API函數(shù)時(經(jīng)過類型轉(zhuǎn)換后),,你必須用SetLength為該字符串預(yù)留內(nèi)存空間,這一點我會在后面進(jìn)行說明,。
看一看內(nèi)存中的字符串
為了幫你更好地理解字符串的內(nèi)存管理細(xì)節(jié),,我寫了一個簡例StrRef。在程序中我聲明了兩個全程字符串:Str1和Str2,,當(dāng)按下第一個按鈕時,,程序把一個字符串常量賦給第一個變量,然后把第一個變量賦給第二個:
Str1 := 'Hello';
Str2 := Str1;
除了字符串操作外,,程序還用下面的StringStatus函數(shù)在一個列表框中顯示字符串的內(nèi)部狀態(tài):
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函數(shù)中,,用常量參數(shù)傳遞字符串至關(guān)重要。用拷貝方式(值參)傳遞會引起副作用,,因為函數(shù)執(zhí)行過程中會產(chǎn)生一個對字符串的額外引用,;與此相反,通過引用(var)或常量(const)參數(shù)傳遞不會產(chǎn)生這種情況,。由于本例不希望字符串被修改,,因此選用常量參數(shù)。 為獲取字符串內(nèi)存地址(有利于識別串的實際內(nèi)容也有助于觀察兩個不同的串變量是否引用了同一內(nèi)存區(qū)),,我通過類型映射把字符串類型強(qiáng)行轉(zhuǎn)換為整型,。字符串實際上是引用,也就是指針:字符串變量保存的是字符串的實際內(nèi)存地址,。
為了提取引用計數(shù)信息,,我利用了一個鮮為人知的事實:即字符串長度和引用計數(shù)信息實際上保存在字符串中,位于實際內(nèi)容和字符串變量所指的內(nèi)存位置之前,其負(fù)偏移量對字符串長度來說是-4(用Length函數(shù)很容易得到這個值),,對引用記數(shù)來說是-8,。
不過必須記住,,以上關(guān)于偏移量的內(nèi)部信息在未來的Delphi版本中可能會變,,沒有寫入正式Delphi文檔的特性很難保證將來不變。
通過運(yùn)行這個例子,,你會看到兩個串內(nèi)容相同,、內(nèi)存位置相同,、引用記數(shù)為2,如圖7.1中列表框上部所示?,F(xiàn)在,,如果你改變其中一個字符串的值,那么更新后字符串的內(nèi)存地址將會改變,。這是copy-on-write技術(shù)的結(jié)果,。
第二個按鈕(Change)的OnClick事件代碼如下,結(jié)果如圖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只能在執(zhí)行完BtnAssignClick后才能執(zhí)行,。為此,程序啟動后第二個按鈕不能用(按鈕的Enabled屬性設(shè)成False),;第一個方法結(jié)束后激活第二個按鈕,。你可以自由地擴(kuò)展這個例子,用StringStatus函數(shù)探究其它情況下長字符串的特性,。
動態(tài)分配可以用任意一個分配內(nèi)存的函數(shù), 其實系統(tǒng)最終調(diào)用的都是GetMem, 其它的New,、AllocMem、SetLength等等只不過除了調(diào)用GetMem外還做了一些初始化處理比如把內(nèi)存清零,。釋放可以用Dispose或者FreeMem, 系統(tǒng)最終都是調(diào)用FreeMem的, Dispose相當(dāng)于Finalize(p); FreeMem(p);
Finalize的作用簡單說就是自動釋放結(jié)構(gòu)或者數(shù)組中的string和動態(tài)數(shù)組, FreeMem則是直接釋放指針?biāo)赶虻膬?nèi)存,例如:
type
TMyRec = record
Name: string;
X, Y: Integer;
end;
PMyRec = ^TMyRec;
var
MyRec : PMyRec;
begin
New(MyRec); // 編譯器會根據(jù)MyRec的大小自動計算需要分配的內(nèi)存數(shù)量然后生成代碼調(diào)用GetMem并將其中的Name字段清零
MyRec.Name := str1 + str2;
Dispose(MyRec); // 除了調(diào)用FreeMem釋放MyRec這個結(jié)構(gòu)的內(nèi)存外還會自動清除其中的Name所用到的內(nèi)存(如果Name指向的string引用計數(shù)=1時);
// FreeMem(MyRec); <-- 如果直接調(diào)用FreeMem釋放MyRec, 則會造成內(nèi)存泄露, 因為MyRec.Name指向的字符串沒有釋放(引用計數(shù)-1)
end;
由于delphi關(guān)于string的內(nèi)存管理的特殊性, 可以有很多技巧充分利用其優(yōu)點生成非常高效的代碼, 比如要用TList來保存string(不是TStringList), 一般的做法是TList.Items[i]中保存一個PString指針, 這樣就需要重新分配一塊內(nèi)存并復(fù)制原串, 大數(shù)據(jù)量的情況下效率很低, 但是如果充分利用string的引用計數(shù)和強(qiáng)制類型轉(zhuǎn)換技巧, 可以直接將string作為指針保存在TList.Items[i]中: 比如:
var
List: TList;
GlobalString1, GlobalString2: string;
...
procedure Test;
var
tmp: string;
begin
tmp := GlobalString1+GlobalString2;
List.Add(Pointer(tmp)); // 將tmp作為指針保存進(jìn)List
{ 由于Test過程結(jié)束時會自動釋放掉tmp, 如果直接退出的話List中就保存了一個無效的指針了, 所以這里要欺騙編譯器, 讓它認(rèn)為tmp已經(jīng)被釋放掉了, 等于在不改動tmp引用計數(shù)(當(dāng)前是1)的情況下執(zhí)行相當(dāng)于tmp := ''的語句, 由于直接tmp := ''會修改引用計數(shù)并可能釋放掉內(nèi)存, 所以用強(qiáng)制類型轉(zhuǎn)換將tmp轉(zhuǎn)成一個Integer并將這個Integer設(shè)置成0(也就是nil), 此語句完全等價于pointer(tmp) := nil; 只是個人喜好我喜歡用Integer(tmp) := 0而已.
}
Integer(tmp) := 0;
end;
1. string是Delphi編譯器內(nèi)在支持的(predefined or built-in),,是Delphi的一個基本數(shù)據(jù)類型,而PChar只是一個指向零終止字符串的指針,;
2. String 所存字符串是在堆分配內(nèi)存的,,String變量實際上是指向零終止字符串的指針,與此同時它還具有引用計數(shù)(reference count)功能,,并且自身保存字符串長度,,當(dāng)引用計數(shù)為零時,自動釋放所占用的空間,。
3.將string賦值給另一個string,,只是一個簡單的指針賦值,不產(chǎn)生copy動作,,只是增加string的引用計數(shù),;
4.將一個PChar變量類型賦值給一個string 變量類型會產(chǎn)生真正的Copy動作,即將PChar所指向的字符串整個copy到為string分配的內(nèi)存中,;
5.將string賦值給一個PChar變量類型,,只是簡單地將string的指針值賦值給PChar變量類型,而string的引用計數(shù)并不因此操作而發(fā)生變化,,因為這種情況PChar會對string產(chǎn)生依賴,,當(dāng)string的引用計數(shù)為零自動釋放內(nèi)存空間后,PChar很可能指向一個無效的內(nèi)存地址,,在你的程序你必須小心對付這種情況,。
6.對PChar的操作速度要遠(yuǎn)遠(yuǎn)高于對string操作的速度,,但PChar是一種落后的管理字符串的方式,而string則以高效的管理而勝出,,PChar它的存在只是為了兼容早期的類型和操作系統(tǒng)(調(diào)用Windows API時會經(jīng)常用到),,建議平常使用string。