(轉)Delphi 中的字符串


一、Delphi 2009 之前的字符串(不支持 Unicode):

  Delphi 2009 之前的字符串分為 3 種:ShortString、AnsiString、WideString。



【ShortString】

  ShortString 是一種比較古老的 Pascal 字符串格式,它最多只能容納 255 個字節的字符。當我們聲明一個 ShortString 類型的變量時,Delphi 會自動申請 256 個字節的內存空間給該變量,其中第一個字節用來存放字符串的長度,后面的 255 個字節用來存放字符串內容,如果字符串的長度不夠 255 個字節,則有多少字符就用多少內存,后面未用到的內存清零。

var
  SStr: ShortString;

  上面的聲明就使 SStr 擁有了 256 個字節的內存空間:

Sizeof(SStr); { = 256; }

  直到 SStr 不再被使用時,Delphi 會自動釋放 SStr 所占用的內存空間。

  我們還可以用下面的方式來聲明 ShortString 類型的變量:

var
  SStr: string[16];

  這樣,我們就聲明了一個只能容納 16 個字節內容的 ShortString 字符串,加上一個字節用來存放字符串的長度,SStr 一共占用 17 個字節的內存空間:

Sizeof(SStr); { = 17; }

  我們可以像使用字節數組(array of byte)那樣來使用 ShortString,比如我們可以用下標來訪問 ShortString 中的各個字符,可以用 High 和 Low 函數來獲取 ShortString 的上限位置和下限位置。由於字符串的第一個字節存放的是字符串的長度,所以 SStr[0] 存放的是字符串的長度,例如:

var
  SStr: string[16];
begin
  SStr := 'ABC';
{ 此時:
  Ord(SStr[0]);  // = 3   字符串長度為 3
  SStr[1];       // = 'A' 第一個字符為 A
  SStr[2];       // = 'B' 第二個字符為 B
  SStr[3];       // = 'C' 第三個字符為 C
  SStr[4];       // = #0  其余位置清零 #0
  ……           //       其余位置清零 #0
  SStr[255];     // = #0  其余位置清零 #0
  High(SStr);    // = 16  上限位置為 16
  Low(SStr);     // = 0   下限位置為 0 }
end;

  接下來,我們來看看 SStr 的指針情況。

----------

{ 在一個空白窗體上放置一個 TMemo 和一個 TButton }
procedure TForm1.Button1Click(Sender: TObject);
var
  SStr: ShortString;

  pS: Pointer;
  pS1: Pointer;
begin
  SStr := 'ABC';

  pS := Addr(SStr);        { 字符串變量 SStr 的地址 }
  pS1 := Addr(SStr[0]);    { 字符串的首地址 }

  Memo1.Clear;
  Memo1.Lines.Add(IntToStr(Integer(pS)));  { 在我的電腦中顯示為:1242240 }
  Memo1.Lines.Add(IntToStr(Integer(pS1))); { 在我的電腦中顯示為:1242240 }
end;

----------

  上面的代碼說明變量 SStr 的地址就是“存放字符串的內存塊”的地址。這和后面講到的 AnsiString 和 WideString 不同。



【AnsiString】

  AnsiString 是一種動態分配內存的字符串,也就是說當我們聲明一個 AnsiString 時,它並不占用內存空間,比如:

var
  AStr: AnsiString;

  此時字符串的長度為 0,不占用內存空間:

Length(AStr);  { = 0 }

  這里為什么不用 Sizeof(AStr); 而用 Length(AStr); 因為 AStr 和 ShortString 不同,變量 AStr 的地址並不是“存放字符串的內存塊”的地址,變量 AStr 中存放的只是一個指針,指向“存放字符串的內存塊”,我們通過變量 AStr 可以找到“存放字符串的內存塊”,請看下面的代碼:

----------

{ 在一個空白窗體上放置一個 TMemo 和一個 TButton }
procedure TForm1.Button1Click(Sender: TObject);
var
  AStr: AnsiString;

  pA: Pointer;
  pA1: Pointer;
begin
  AStr := 'ABC';

  pA := Addr(AStr);       { 字符串變量 AStr 的地址 }
  pA1 := Addr(AStr[1]);   { “存放字符串的內存塊”的地址。Delphi 不允許訪問 AStr[0] }

  Memo1.Clear;
  Memo1.Lines.Add(IntToStr(Integer(pA)));  { 在我的電腦中顯示為:1242504 }
  Memo1.Lines.Add(IntToStr(Integer(pA1))); { 在我的電腦中顯示為:17539780 }
end;

----------

  從上面的代碼中可以看出,字符串變量 AStr 的地址和“存放字符串的內存塊”的地址是不一樣的。所以用 Sizeof(AStr) 無法獲取字符串的大小,只能獲取一個指針的大小,永遠為 Sizeof(Pointer);

  剛才說了,AnsiString 變量在剛聲明的時候是不分配內存的,所以就不能使用 AStr[1],因為 AStr[1] 是指“被分配的內存塊”中的第一個字符,而“內存塊”根本就沒有分配,哪來的第一個字符?所以在使用 AStr[1] 之前首先要判斷 Length(AStr) 是否為 0,若為 0,就表示內存塊未分配,就不能使用 AStr[1]。

  那什么時候 AStr 才分配內存呢?當我們給 AStr 賦值的時候,Delphi 就給 AStr 分配內存,例如:

----------
var
  AStr: AnsiString;     { 此時 AStr 未分配內存 }
begin
  AStr := 'ABC';        { 此時 Delphi 給 AStr 分配了三個字節的內存用來存放 'ABC' }
  ShowMessage(AStr[1]); { 這時就可以使用 AStr[1] 了 }
  AStr := 'ABCDEF';     { 此時 Delphi 增大字符串的內存空間來存放更多字符 }
  AStr := '';           { 此時 AStr 將剛分配的內存全部釋放 }
  ShowMessage(AStr[1]); { 錯誤,因為此時未分配內存,所以不可以使用 AStr[1] }
end;

----------

  那么變量 AStr 和“存放字符串的內存塊”之間是什么關系呢?變量 AStr 中存放的其實是一個指針,指向“存放字符串的內存塊”。通過下面的代碼可以很好的理解這個問題:

----------

{ 在一個空白窗體上放置一個 TMemo 和一個 TButton }
procedure TForm1.Button1Click(Sender: TObject);
var
  AStr: AnsiString;

  pA: Pointer;      { 變量 AStr 的地址 }
  pA1: Pointer;     { 字符串中第一個字符的地址(即:內存塊的地址) }
  pAP: Pointer;     { 變量 AStr 中所存放的指針 }
begin
  AStr := 'ABC';    { 申請三個字節的內存塊用來存放 'ABC' }

  pA  := Addr(AStr);    { 獲取變量 AStr 的地址 }
  pA1 := Addr(AStr[1]); { 獲取內存塊的地址 }
  pAP := Pointer(AStr); { 獲取變量 AStr 中所存放的指針 }

  Memo1.Clear;
  Memo1.Lines.Add(IntToStr(Integer(pA)));   { 變量 AStr 的地址 }
  Memo1.Lines.Add(IntToStr(Integer(pA1)));  { 內存塊的地址 }
  Memo1.Lines.Add(IntToStr(Integer(pAP)));  { 變量 AStr 中所存放的指針 }
end;

----------

{ 運行結果 }
1242504      { 變量 AStr 的地址 }
17539780     { 內存塊的地址 }
17539780     { 變量 AStr 中所存放的指針 }

----------

  由此可見 AStr 中存放的只是一個指針,指向“存放字符串的內存塊”,我們可以通過 Pointer(AStr) 來得到這個內存塊的地址。

  上面的代碼有一個奇怪的地方,就是將 pA1 := Addr(AStr[1]); 和 pAP := Pointer(AStr); 兩行代碼的前后位置對調一下,運行結果就不同了(測試環境:Delphi XE2),下面是對調以后的運行結果:

----------

{ 對調以后的運行結果 }
1242504      { 變量 AStr 的地址 }
17539780     { 內存塊的地址 }
5327792      { 變量 AStr 中所存放的指針 }

----------

  這或許是 Delphi 對字符串優化所造成的結果(Delphi 的 copy-on-write 技術),當我們僅僅是讀取該字符串時,它的地址就是剛賦初始值時的地址(5327792),而當我們要修改字符串時( pA1 := Addr(AStr[1]) 也被認為是用戶將要通過指針修改字符串),Delphi 就會把該字符串復制到新的地方(17539780)給用戶修改並繼續使用。如果“舊地址中的字符串”還同時被其它變量引用,比如 AStr := 'ABC' 之后又 AStr2 := AStr,則保留舊地址,供 AStr2 繼續使用,如果舊地址不再被其它變量使用,則將舊地址中的字符串全部釋放。 )

  為了檢驗以上說法,我們用下面的代碼再測試一次,這次,我們將 AnsiString 定義為常量,不允許修改,看看結果如何:

----------

{ 在一個空白窗體上放置一個 TMemo 和一個 TButton }
procedure TForm1.Button1Click(Sender: TObject);
const
  AStr: AnsiString = 'ABC';
var
  pA: Pointer;      { 變量 AStr 的地址 }
  pA1: Pointer;     { 字符串中第一個字符的地址(即:內存塊的首地址) }
  pAP: Pointer;     { AStr 中所存放的指針 }
begin
  pA  := Addr(AStr);    { 獲取變量 AStr 的地址 }
  pAP := Pointer(AStr); { 獲取變量 AStr 中所存放的指針(放前面) }
  pA1 := Addr(AStr[1]); { 獲取內存塊的地址(放后面) }

  Memo1.Clear;
  Memo1.Lines.Add(IntToStr(Integer(pA)));   { 變量 AStr 的地址 }
  Memo1.Lines.Add(IntToStr(Integer(pA1)));  { 內存塊的地址 }
  Memo1.Lines.Add(IntToStr(Integer(pAP)));  { 變量 AStr 中所存放的指針 }
end;

----------

{ 此時的運行結果 }

5379472      { 變量 AStr 的地址 }
5327460      { 內存塊的地址 }
5327460      { 變量 AStr 中所存放的指針 }

----------

  字符串的地址沒有發生變化,這驗證了剛才我們說的 Delphi 的 copy-on-write 技術。這也解釋了為什么提倡大家在不修改字符串內容的情況下,盡量將字符串聲明為常量(比如函數的字符串參數),因為常量不會觸發 Delphi 的 copy-on-write 技術,增加代碼的執行效率。

  接下來有必要對 Length 函數做一下說明,Length 函數對於 ShortString 和 AnsiString 來說,都是指“字符串中的字節數(而不是字符數)”,請看下面的例子:

----------

{ 在一個空白窗體上放置一個 TMemo 和一個 TButton }
procedure TForm1.Button1Click(Sender: TObject);
var
  SStr: ShortString;
  AStr: AnsiString;
begin
  Memo1.Clear;

  SStr := '123你好嗎';  { 一共 6 個字符 }
  AStr := '123你好嗎';  { 一共 6 個字符 }

  Memo1.Lines.Add(IntToStr(Length(SStr)));    { 結果 9 }
  Memo1.Lines.Add(IntToStr(Length(AStr)));    { 結果 9 }
end;

----------

  因為一個英文字符只占用 1 個字節的內存空間,而一個漢字要占用 2 個字節的內存空間,所以 SStr 和 AStr 都用了 9 個字節的內存空間來存放字符串。如果此時你使用 AStr[4] 來獲取漢字“你”,那么只能獲取半個漢字,此時的 AStr[4] 是 AnsiChar 類型(單字節)。所以用 AnsiString 處理漢字是很麻煩的,我們一般用 WideString 來處理帶有漢字的字符串。

  關於 Delphi 不允許訪問 AStr[0],其實 AnsiString 和 ShortString 有着類似的結構,也就是說在 AnsiString 字符串的前面也有一個數據區用來保存 AnsiString 的長度(字節數),這個數據區就是字符串之前的 4 個字節,我們可以通過指針來訪問這個區域,而再之前的 4 個字節中還存放着字符串的引用計數,標識着這個字符串被幾個字符串變量所引用。在 Delphi 7 的 system 單元中可以找到字符串結構的定義:

StrRec = packed record
  refCnt: Longint;   { 引用計數 }
  length: Longint;   { 字符串長度 }
end;

請看下面的代碼:

----------

{ 在一個空白窗體上放置一個 TMemo 和一個 TButton }
procedure TForm1.Button1Click(Sender: TObject);
var
  AStr: AnsiString;
  P: PCardinal;    { Sizeof(Cardinal) = 4 字節 }
begin
  Memo1.Clear;

  SetLength(AStr, 65530);
  P := PCardinal(AStr);

  Dec(P); { 向前移動 4 個字節 }
  Memo1.Lines.Add(IntToStr(P^));    { 結果 65530   字符串長度 }
  Dec(P); { 再向前移動 4 個字節 }
  Memo1.Lines.Add(IntToStr(P^));    { 結果 1       引用計數 }
end;

----------

  從上面的結果可以看出,字符串的長度為 65530,引用計數為 1。

  關於 AnsiString 中可以存放的內容,有人說 AnsiString 中存放的是以 #0 結尾的字符串,和 C 語言的字符串類似,這樣理解不准確,因為 AnsiString 中可以存放多個 #0 字符,#0 並不一定代表字符串的結尾。例如:

----------

{ 在一個空白窗體上放置一個 TMemo 和一個 TButton }
procedure TForm1.Button1Click(Sender: TObject);
var
  AStr, AStr2: AnsiString;
  I: Integer;
begin
  Memo1.Clear;

  SetLength(AStr, 10);  { 在 AStr 中存放 10 個 #0 字符 }
  for I := 1 to 10 do
    AStr[I] := #0;

  AStr2 := AStr;        { 看看賦值的過程中會不會發生字符丟失 }
  AStr2[5] := 'A';
  Memo1.Lines.Add(IntToStr(Length(AStr2)));    { 結果 10,沒有丟失字符 }
end;

----------

  也就是說 AnsiString 可以直接當內存來使用,它不只可以存放字符,而是可以存放任何東西,你甚至可以將一個圖片的數據存入 AnsiString 的內存塊中。用 AnsiString 代替內存使用有一個好處,就是 Delpih 會幫你管理這個內存,在你不需要使用該內存塊的時候,Delphi 會自動幫你釋放。

  但是這樣的字符串不能和 PAnsiChar 類型一起使用,因為 PAnsiChar 才是真正的以 #0 結尾的字符串(我們剛才不是說了 AnsiString 其實就是一個指針嗎,PAnsiChar 也是一個指針,在用法上和 AnsiString 有很多相同的地方),當你把上面的字符串賦值給一個 PAnsiChar 變量時,PAnsiChar 只會讀取第一個 #0 之前的內容,而把第一個 #0 之后的所有內容都丟棄掉。請看下面的代碼:

----------

{ 在一個空白窗體上放置一個 TMemo 和一個 TButton }
procedure TForm1.Button1Click(Sender: TObject);
var
  AStr: AnsiString;
  pAStr: PAnsiChar;
  I: Integer;
begin
  Memo1.Clear;

  SetLength(AStr, 10);
  for I := 1 to 10 do
    AStr[I] := #0;

  pAStr := 'ABC';
  Memo1.Lines.Add(IntToStr(Length(pAStr)));    { 結果 3 }

  pAStr := PAnsiChar(AStr);
  Memo1.Lines.Add(IntToStr(Length(pAStr)));    { 結果 0 }

  pAStr := #0#0#0;
  Memo1.Lines.Add(IntToStr(Length(pAStr)));    { 結果 0 }
end;

----------

  好了,總結一下 AnsiString:

  AnsiString 變量只是一個指針,指向一個內存塊,這個內存塊用來存放實際的字符串。AnsiString 所指向的內存是動態分配的,如果 AnsiString 中未存放任何字符串,則 AnsiString 指向 nil。

  雖然 AnsiString 變量並不是真正的內存塊,只是一個指針,但是我們仍然可以通過下標的方式來訪問內存塊中的字符。下標必須從 1 開始,Delphi 不允許 AnsiString 和 WideString 使用下標 0。

  我們可以通過如下方式訪問到 AStr 所指向的內存塊地址:

@AStr[1]
Pointer(AStr)

  我們可以通過如下方式訪問到 AStr 所指向的內存塊中的第一個字符、第二個字符、第三個字符:

AStr[1]、AStr[2]、AStr[3]
PAnsiChar(AStr)^ 、(PAnsiChar(AStr)+1)^ 、(PAnsiChar(AStr)+2)^

  PAnsiChar 類型和 AnsiString 類型之間可以很容易的相互轉換。不過編譯器在編譯的時候會給出警告(不是錯誤)。例如:

----------

{ 在一個空白窗體上放置一個 TMemo 和一個 TButton }
procedure TForm1.Button1Click(Sender: TObject);
var
  AStr: AnsiString;
  pAStr: PAnsiChar;
begin
  Memo1.Clear;

  AStr := 'ABC';
  pAStr := PAnsiChar(AStr);
  Memo1.Lines.Add(AStr);
  Memo1.Lines.Add(pAStr);

  Memo1.Lines.Add('');

  pAStr := '123';

  AStr := pAStr;
  Memo1.Lines.Add(AStr);
  Memo1.Lines.Add(pAStr);
end;

----------

  關於 Length 函數,Length 函數對於 ShortString 和 AnsiString 來說返回的是它們所存放的字符串的字節數,而不是字符數。

  在 AnsiString 的字符串之前的 4 個字節中存放着字符串的總長度(總字節數),再之前的 4 個字節中存放着字符串的引用計數。

  AStr[n] 只能返回半個漢字,所以用 AnsiString 處理漢字是很麻煩的,我們一般用 WideString 來處理帶有漢字的字符串。

  Delphi 對 AnsiString 和 WideString 都使用了 copy-on-write 技術,所以當我們不准備修改字符串的內容時,最好將字符串聲明為常量,以提高程序的執行效率。

  Delphi 中的 AnsiString 可以直接當做內存來使用,如果你願意這么用的話,Delphi 會幫你管理這塊內存。申請內存很方便,直接 SetLength 就可以了。用完了 Delphi 會幫你釋放。但此時 AnsiString 不能和 PAnsiChar 一起使用。要得到內存的地址,可以使用 Pointer(AStr)。



【WideString】

  WideSting 和 AnsiString 的用法基本一樣,也是動態分配內存。所不同的是,WideString 所存儲的任何字符(不管是英文還是漢字)都是使用 2 個字節,但是 WideString 沒有引用計數。其實 WideString 是為了方便使用 COM 而產生的,也就是 BSTR 字符串。BSTR 沒有引用計數,效率較低。

  與 WideString 相對應的類型有 WideChar,用法與 AnsiChar 一樣。還有 PWideChar,用法與 PAnsiChar 一樣。

  Length 函數對於 WideString 來說,返回的就是字符數,而不是字節數,要獲取 WideString 的字節數,直接把 Length 的返回值乘以 2 就可以了。

  對於如下代碼而言:

----------

var
  WStr: WideString;
begin
  WStr := '123你好嗎'
end;

----------

  WStr[1] 就表示字符“1”,WStr[4] 就表示字符“你”,只不過此時的 WStr[1] 是 WideChar 類型(雙字節)。



二、Delphi 2009 之后的字符串(開始全面支持 Unicode):

  Delphi 2009 之后又加入了一種新的字符串:UniodeString,並且對字符串的結構也做了改動,增加了 codePage 和 elemSize 域。下面是 System 單元中定義的字符串結構:

PStrRec = ^StrRec;
StrRec = packed record
  codePage: Word;   // 代碼頁:Unicode、UTF-8、UTF-16、GB2312
  elemSize: Word;   // 元素大小:一字符占幾個字節
  refCnt: Longint;  // 引用計數:字符串被幾個字符串變量使用
  length: Longint;  // 字符串長度:字節數
end;

  這個結構只用於 AnsiString 和 UnicodeString,也就是說在 AnsiString 和 UnicodeString 的字符串之前的 4 個字節存放的是字符串長度,再之前的 4個字節存放的是字符串的引用計數,再之前的 2 個字節存放的是元素大小,再之前的 2 個字節存放的是字符串的代碼頁。而 WideString 依然是原來的樣子,沒有變化。所以我們以后可以直接使用 UnicodeString 來處理漢字。

  在 system 單元中還定義了 UTF8String 和 UCS4String 類型的字符串,定義如下:

UTF8String = type AnsiString(65001);
UCS4String = array of UCS4Char;  { UCS4Char = type LongWord; }

  除此之外,Delphi 還定義了 RawByteStrng 類型的字符串,定義如下:

RawByteString = type AnsiString($ffff);

  關於RawByteStrng 類型:在將 AnsiString 格式的字符串賦值給 UTF8String 格式的字符串時,Delphi 會自動進行格式轉換(還有其它格式的自動轉換),所以,如果我們有一個函數的參數需要接收各種類型的字符串時,那么就很難實現,因為在傳遞參數的時候,Delphi 就會自動進行格式轉換,所以,Delphi 定義了 RawByteString 類型,這種類型的變量在接收任何格式的字符串時,都會保持源字符串的內存格式,不做任何改動。



【string】

  我們平時經常使用的字符串類型是 string 類型,很少直接使用 AnsiString 或 UnicodeString,那么 string 是什么類型呢?

  string 類型根據編譯參數的不同,所代表的含義也不同,或者說根據 Delphi 版本的不同,所代表的含義也不同,在 Ansi 版本(Delphi 2009 之前)中,string 就代表 AnsiString,而在 Unicode 版本(Delphi 2009 之后)中,就代表 UnicodeString。

  同樣的 Char 在 Ansi 版本中就代表 AnsiChar,在 Unicode 版本中就代表 WideChar。PChar 在 Ansi 版本中就代表 PAnsiChar,在 Unicode 版本中就代表 PWideChar。

  在 Delphi 的 Unicode 版本中有一個新的函數 ByteLength 用來獲取字符串的字節數。但是這個函數只能用於 string 類型的變量,我們看一下它的源代碼就知道為什么了。

----------

function ByteLength(const S: string): Integer;
begin
  Result := Length(S) * SizeOf(Char);
end;

----------

  這個函數會根據不同的編譯條件產生不同的計算結果,所以只能配合 string 使用。如果你將它用於 AnsiSting 或 UnicodeString 型變量,則在不同的編譯條件下,有可能會出現錯誤。請看下面的代碼:

----------

{ 在一個空白窗體上放置一個 TMemo 和一個 TButton }
procedure TForm1.Button1Click(Sender: TObject);
var
  SStr: ShortString;
  AStr: AnsiString;
  UStr: UnicodeString;
  WStr: WideString;
  Str: string;
begin
  SStr := '123你好嗎';
  AStr := '123你好嗎';
  UStr := '123你好嗎';
  WStr := '123你好嗎';
  Str  := '123你好嗎';

  Memo1.Clear;
  Memo1.Lines.Add(IntToStr(Length(SStr)));      { 結果 9  }
  Memo1.Lines.Add(IntToStr(Length(AStr)));      { 結果 9  }
  Memo1.Lines.Add(IntToStr(Length(UStr)));      { 結果 6  }
  Memo1.Lines.Add(IntToStr(Length(WStr)));      { 結果 6  }
  Memo1.Lines.Add(IntToStr(Length(Str)));       { 結果 6  }

  Memo1.Lines.Add(IntToStr(ByteLength(SStr)));  { 結果 12 }
  Memo1.Lines.Add(IntToStr(ByteLength(AStr)));  { 結果 12 }
  Memo1.Lines.Add(IntToStr(ByteLength(UStr)));  { 結果 12 }
  Memo1.Lines.Add(IntToStr(ByteLength(WStr)));  { 結果 12 }
  Memo1.Lines.Add(IntToStr(ByteLength(Str)));   { 結果 12 }
end;

----------


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM