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。

http://www.cnblogs.com/zhengjuzhuan/archive/2010/03/15/1686257.html


免責聲明!

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



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