Delphi 之 第七課 字符串操作

 

字符串類型

在Borland公司的Turbo Pascal和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技術的結果。

圖 7.1: 例StrRef顯示兩個串的內部狀態,包括當前引用計數 

第二個按鈕(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 函數探究其它情況下長字符串的特性。

Delphi 字符串與 Windows PChar字符串

長字符串爲零終止串,這意味着長字符串完全與Windows使用的C語言零終止串兼容,這給長字符串使用帶來了便利。一個零終止串是一個字符序列,該序列以一個零字節(或null)結尾。零終止串在Delphi中可用下標從零開始的字符數組表示,C語言就是用這種數組類型定義字符串,因此零終止字符數組在Windows API 函數(基於C語言)中很常見。由於Pascal長字符串與C語言的零終止字符串完全兼容,因此當需要把字符串傳遞給Windows API 函數時,你可以直接把長字符串映射爲PChar 類型。

下例把一個窗體的標題拷貝給PChar 字符串(用API 函數GetWindowText),然後再把它拷貝給按鈕的Caption 屬性,代碼如下:

procedure TForm1.Button1Click (Sender: TObject);
var
  S1: String;
begin
  SetLength (S1, 100);
  GetWindowText (Handle, PChar (S1), Length (S1));
  Button1.Caption := S1;
end;

你可以在例LongStr 中找到這段代碼。注意:代碼中用SetLength函數爲字符串分配內存,假如內存分配失敗,那麼程序就會崩潰;如果你直接用PChar 類型傳遞值(而不是象以以上代碼那樣接受一個值),那麼代碼會很簡單,因爲不需要定義臨時字符串,也不需要初始化串。下面代碼把一個Label(標籤)控件的Caption 屬性作爲參數傳遞給了API函數,只需要簡單地把屬性值映射爲PChar類型:

SetWindowText (Handle, PChar (Label1.Caption));

當需要把WideString 映射爲Windows兼容類型時,你必須用PWideChar 代替PChar進行轉換,WideString常用於OLE和 COM 程序。

剛纔展現了長字符串的優點,現在談談它的弊端。當你把長字符串轉換爲PChar 類型時可能會引發一些問題,問題根本在於:轉換以後字符串及其內容將由你來負責,Delphi 不再管了。現在把上面Button1Click代碼稍作修改:

procedure TForm1.Button2Click(Sender: TObject);
var
  S1: String;
begin
  SetLength (S1, 100);
  GetWindowText (Handle, PChar (S1), Length (S1));
  S1 := S1 + ' is the title'; // this won't work
  Button1.Caption := S1;
end;

程序編譯通過,但執行結果會令你驚訝,因爲按鈕的標題並沒變,所加的常量字符串沒有添加到按鈕標題中。問題原因是Windows寫字符串時(在GetWindowText API調用中),Windows 沒有正確設置Pascal 長字符串的長度。Delphi 仍可以輸出該字符串,並能通過零終止符判斷字符串何時結束,但是如果你在零終止符後添加更多的字符,那麼這些字符將被忽略。

怎麼解決這個問題呢?解決方法是告訴系統把GetWindowText API函數返回的字符串再轉換成Pascal字符串。然而,如果你用以下代碼:

S1 := String (S1);

Delphi 系統將不予理睬,因爲把一種類型轉換爲它自己的類型是無用的操作。爲獲得正確的Pascal 長字符串,需要你把字符串重新映射爲一個PChar 字符串,然後讓Delphi 再把它轉回到字符串:

S1 := String (PChar (S1));

實際上,你可以跳過字符串轉換(S1 := PChar (S1));, 因爲在Delphi中Pchar轉換到string是自動執行的,最終代碼如下:

procedure TForm1.Button3Click(Sender: TObject);
var
  S1: String;
begin
  SetLength (S1, 100);
  GetWindowText (Handle, PChar (S1), Length (S1));
  S1 := String (PChar (S1));
  S1 := S1 + ' is the title';
  Button3.Caption := S1;
end;

另一個辦法是用PChar 字符串的長度重新設定Delphi 字符串長度,可以這樣寫:

SetLength (S1, StrLen (PChar (S1)));

在例LongStr中你可以看到三種方法的結果,分別由三個按鈕執行。如果只想訪問窗體標題,僅需要用到窗體對象本身的Caption 屬性,沒有必要寫這段迷糊人的代碼,這段代碼只是用來說明字符串轉換問題。當調用Windows API 函數時會遇到這種實際問題,那時你就不得不考慮這一複雜情況了。

格式化字符串

使用加號(+)操作符和轉換函數(如IntToStr),你確實能把已有值組合成字符串,不過另有一種方法能格式化數字、貨幣值和其他字符串,這就是功能強大的Format 函數及其一族。

Format 函數參數包括:一個基本文本字符串、一些佔位符(通常由%符號標出)和一個數值數組,數組中每個值對應一個佔位符。例如,把兩個數字格式化爲字符串的代碼如下:

Format ('First %d, Second %d', [n1, n2]);

其中n1和n2是兩個整數值,第一個佔位符由第一個值替代,第二個佔位符由第二個值替代,以此類推。如果佔位符輸出類型(由%符號後面的字母表示)與對應的參數類型不匹配,將產生一個運行時間錯誤,因此設置編譯時間類型檢查會有利於Format 函數的使用。

除了%d外,Format 函數還定義了許多佔位符,見表7.1。這些佔位符定義了相應數據類型的默認輸出,你可以用更深一層的格式化約束改變默認輸出,例如一個寬度約束決定了輸出中的字符個數,而精度約束決定了小數點的位數。例如

Format ('%8d', [n1]);

該句把數字n1轉換成有8個字符的字符串,並通過填充空白使文本右對齊,左對齊用減號(-) 。

表 7.1: Format函數的佔位符 

 

佔位符 說明
d (decimal) 將整型值轉換爲十進制數字字符串
x (hexadecimal) 將整型值轉換爲十六進制數字字符串
p (pointer) 將指針值轉換爲十六進制數字字符串
s (string) 拷貝字符串、字符、或字符指針值到一個輸出字符串
e (exponential) 將浮點值轉換爲指數表示的字符串
f (floating point) 將浮點值轉換爲浮點表示的字符串
g (general) 使用浮點或指數將浮點值轉換爲最短的十進制字符串
n (number) 將浮點值轉換爲帶千位分隔符的浮點值
m (money) 將浮點值轉換爲現金數量表示的字符串,轉換結果取決於地域設置,詳見Delphi幫助文件的Currency and date/time formatting variables主題

領會以上內容最好的辦法是你親自進行字符串格式化試驗。爲了簡便起見,我寫了FmtTest 程序,它能將整數和浮點數轉換爲格式化字符串。從圖7.2可見,程序窗體分爲左右兩部分,左邊對應整型數字轉換,右邊對應浮點數轉換。

各部分的第一個編輯框顯示需要格式化爲字符串的數值。第一個編輯框下方有一個按鈕,用來執行格式化操作並在消息框中顯示結果;緊接着第二個編輯框用於輸入格式化類型串。你也可以單擊ListBox 控件中的任一行,選擇預定義的格式化類型串,也可以自行輸入,每輸入一個新的格式化類型串,該類型串就會被添加到列表框中(注意,關閉程序就失去了添加的類型)。

圖 7.2: 程序 FmtTest 的浮點值輸出 

本例只簡單使用了不同的控制文本來產生輸出,下面列出了其中一個Show 按鈕事件代碼:

procedure TFormFmtTest.BtnIntClick(Sender: TObject);
begin
  ShowMessage (Format (EditFmtInt.Text,
    [StrToInt (EditInt.Text)]));
  // if the item is not there, add it
  if ListBoxInt.Items.IndexOf (EditFmtInt.Text) < 0 then
    ListBoxInt.Items.Add (EditFmtInt.Text);
end;

這段代碼主要用EditFmtInt 編輯框的文本和EditInt 控件的值進行了格式化操作。如果格式化類型串沒有在列表框中列出,那麼輸入的串會被添加到列表框中;如果用戶在列表框中進行點擊,代碼會把點擊的串移到編輯框中:

procedure TFormFmtTest.ListBoxIntClick(Sender: TObject);
begin
  EditFmtInt.Text := ListBoxInt.Items [
    ListBoxInt.ItemIndex];
end;

結束語

字符串是一種很常用的數據類型,儘管在很多情況下不理解字符串怎樣工作也能安全使用它們,不過通過本章,瞭解了字符串的內部運行機制之後,你就能更充分地利用字符串類型的強大功能。

Delphi用特殊的動態方式處理字符串內存,正如動態數組一樣,這將在下一章進行討論。

下一頁: 動態數組

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章