Object Pascal中String型別的內幕探討 (轉)

gugu99發表於2008-04-27
Object Pascal中String型別的內幕探討 (轉)[@more@] 

在 Pascal中,String(準確的說是AnsiString)是一種可變長度的字串,透過PChar(AString)可以將其轉換為與 相相容的字元指標型別。事實上,String型別就是一個指標,你可以用Sizeof去讀取它的大小,不論字串的實際長度是多少,Sizeof(AString)永遠是4。String與一般的Null-Tenated字元指標不同的是,String還要保留另外的一部分空間,用於記錄字串長度和引用計數等資訊。String型別在中的確切格式如下::namespace prefix = o ns = "urn:schemas--com::office" />

 

(4位元組)分配大小+(4位元組)引用計數+(4位元組)字串長度+(不定長)字元陣列+(1位元組)$0結束字元

 

為了驗證這一點,我們可以在中新增一個作用域為private的String變數,在程式中動態改變它的長度和內容,同時觀察它的分配大小和長度發生了什麼變化。另外,為了觀察引用計數的變化,只有在兩個字串互相複製的時候才能體現出來,我們在程式中也要實現這一點。

 

請新建一個Application,在窗體上放置一個Edit,一個ListBox和三個Button。其中,Edit用來改變字串的內容;ListBox用來記錄跟蹤資訊;三個按鈕分別用於觀察字串的當前狀況,觀察字串的引用計數變化情況和清空列表內容。

 

在Form的宣告中新增一個變數:

type

  TForm1=class(TForm)

  ...

  private

s : string;

end;

 

新增三個按鈕的事件處理如下:

procedure TForm1.Button1Click(Sender: TObject);

var

  psz : PChar;

 pdw : PD;

  dw1, dw2, dw3 : DWord;

begin

  s := Edit1.Text;

  psz := PChar(s);

  pdw := PDWORD(psz);

  Dec(pdw);Dec(pdw);Dec(pdw);

  dw1 := pdw^;

  Inc(pdw);dw2 := pdw^;

  Inc(pdw);dw3 := pdw^;

  ListBox1.Items.Add( Format('[Current]Size:%d, Ref:%d, Len:%d',

  [dw1,dw2,dw3]) );

end;

 

procedure TForm1.Button2Click(Sender: TObject);

var

  psz : PChar;

  pdw : PDWORD;

  dw1, dw2, dw3 : DWord;

  s2 : string;

  p1, p2 : Pointer;

begin

  s := Edit1.Text;

  psz := PChar(s);

  pdw := PDWORD(psz);

  Dec(pdw);Dec(pdw);Dec(pdw);

  dw1 := pdw^;

  Inc(pdw);dw2 := pdw^;

  Inc(pdw);dw3 := pdw^;

  ListBox1.Items.Add( Format('[Before assign]Size:%d, Ref:%d, Len:%d',

  [dw1,dw2,dw3]) );

  s2 := s;

  p1 := Pointer(PChar(s));

  p2 := Pointer(PChar(s2));

  ShowMessage(Format('p1=%p,p2=%p',[p1,p2]));

  psz := pChar(s);

  pdw := PDWORD(psz);

  Dec(pdw);Dec(pdw);Dec(pdw);

  dw1 := pdw^;

  Inc(pdw); dw2 := pdw^;

  Inc(pdw); dw3 := pdw^;

  ListBox1.Items.Add( Format('[After assign]Size:%d, Ref:%d, Len:%d',

  [dw1,dw2,dw3]) );

  s2 := s2 + 'Another string';

  p1 := Pointer(PChar(s));

  p2 := Pointer(PChar(s2));

  ShowMessage(Format('p1=%p,p2=%p',[p1,p2]));

  psz := pChar(s);

  pdw := PDWORD(psz);

  Dec(pdw);Dec(pdw);Dec(pdw);

  dw1 := pdw^;

  Inc(pdw); dw2 := pdw^;

  Inc(pdw); dw3 := pdw^;

  ListBox1.Items.Add( Format('[After COW]Size:%d, Ref:%d, Len:%d',

  [dw1,dw2,dw3]) );

end;

 

 

procedure TForm1.Button3Click(Sender: TObject);

begin

  ListBox1.Items.Clear;

end;

 

如果你對指標的概念比較清楚的話,上面的程式碼是不難理解的。下面是該程式的輸出結果:

[Current]Size:22, Ref:5, Len:5

[Before Assign]Size:22, Ref:2, Len:5

[After Assign]Size:22, Ref:3, Len:5

[After COW]Size:22, Ref:2, Len:5

 

觀察上述結果,可以得出幾個結論:

1.“分配大小”和“字串長度”之間存在著一種固定的數量關係,即分配大小=字串長度+17。為什麼會有這種關係?請你再看一看String型別的記憶體分佈:(4位元組)分配大小+(4位元組)引用計數+(4位元組)字串長度+(不定長)字元陣列+(1位元組)$0結束字元,4+4+4+(strlen)+1,應該是13+(strlen)才對,也就是說應該還有4位元組的空間,其用途尚不清楚。值得一提的是,如果你將字串清空,那麼Len的結果可能不是你所想象的0,而是一個讓你大吃一驚的數字。

2.因為分配大小和字串長度都是用4位元組來表示的,而且String型別是動態分配記憶體,所以字串最大可能的長度應該是2^32-17個位元組。

3.在複製字串的時候,Object Pascal並不是把字串簡單的複製一份,而是採取了引用計數的方法,將兩個字串指向同一個記憶體空間,同時引用計數加1。當字串變數被清除的時候,引用計數減1,如果引用計數已經減為0,表明該字串可以真正被清除了。顯然,這種方法比複製整個字串的要高。

4.在給字串賦值的時候,Object Pascal首先會檢查字串的引用計數是否為1。如果是,按照一般的方法直接賦值即可;否則,就說明有兩個以上字串指向同一個地址,這種情況就複雜多了。Object Pascal使用的是Copy-on-Write機制(COW),為當前字串另外開闢一個緩衝區,將新內容拷入;同時,原來的字串引用計數要減一。

5.知道了String的記憶體佈局,我們也就知道了PChar(str)的意義了。不過,使用PChar的同時也就丟失了String的動態增長和引用計數的功能,所以一定要小心,另外要注意PChar長度的計算和字串長度一定要同步,否則會出問題。比如,下面的程式碼就不能正常工作:

var

  str : string;

begin

  SetLength(str,256);

  GetWindowDirectory(PChar(str),256);

  str := str + ‘win.ini’;

end;

這樣的結果是不正確的。之所以不正確,是因為SetLength將字串長度設成了256,而PChar計算的長度只到第一個$0為止。正確的方法應該是:

SetLength(str,256);

GetWindowsDirectory(PChar(str),256);

SetLength(str,StrLen(PChar(str)));

str := setr + ‘win.ini’;

 

 

說明:上面的程式是在 5下測試透過的。Borland並不保證String的記憶體結構在以後的Delphi版本中會保持不變,所以,上述例子只是作為測試用,實際的程式中不應該這樣使用String,謹此說明。


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10748419/viewspace-1003048/,如需轉載,請註明出處,否則將追究法律責任。

相關文章