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