1. 程式人生 > >Delphi的指針(有圖,很清楚)

Delphi的指針(有圖,很清楚)

obj 問題 isa 方式 變量 nil expec 緩沖 mina

Pointers are like jumps, leading wildly from one part of the data structure to another. Their introduction into high-level languages has been a step backwards from which we may never recover. — Anthony Hoare

對指針可能有最讓人誤解和懼怕的數據類型,因此很多程序員喜歡躲避他們.

但是指針很重要.即使不顯式支持指針或使其很難應用的語言,在其背後指針也起到非常重要的作用.因此理解指針是很重要的.有幾個不同的途徑理解指針.

本文針對理解指針和使用指針有問題的讀者.在Win32的Delphi環境下進行討論.可能不會涉及到所有方面(例如,一個應用程序的內存不是一個大的連續區域),但出於實用目的是有所幫助的.我認為指針是很容易理解的.

內存

You probably already know what I write in this paragraph, but it is probably good to read it anyway, since it shows my view on things, which may differ a bit from your own.

指針是一個指向其他變量的變量.為解釋他們,有必要先理解內存地址和變量的概念.首先先簡要的解釋一下計算機內存.

計算機內存簡化的可以看做很長的字節行.字節是最小存儲單元,包括256種不同的值(0到255).在當前32為Delphi中,內存可以看做是最大有2G字節長度的數組.字節中存儲的內容與內容的解讀方式有關,例如,使用方式.值97可以看做是一個byte類型的97,可以看做字符a.如果包含多個字節,可以存儲更多的值.兩個字節可以表示256*256中不同的值.

內存中的字節可以按編號進行訪問,從0開始直到2147483647(假如有2G字節,即使你不知道這些,Windows也會試圖虛擬這種效果).在這個巨大的數組中字節索引叫做地址.

也可以說:字節是內存中最小的可用地址表示的塊.

事實上,內存非常復雜.有些計算機的字節不是8位,其能表示或大於或小於256種值,但Win32的Delphi不會遇到這樣的計算機.內存由軟件和硬件共同管理,全部的內存並非真正存在(內存管理程序處理同樣與硬盤交換空間來解決這些問題),但本文中,將內存看做是字節組成的大塊並在多個程序間共用有助於理解問題.

變量

變量是巨大數組中的一個或多個字節組成的存儲單元,可供程序讀寫.有名字,類型,值及其地址進行標示.

如果聲明了一個變量,編輯器將保留一塊適當大小的內存區域.變量存儲的具體地址由編譯器及運行時代碼決定.不能對變量具體存放地址做任何假設.

變量類型定義了如何使用內存存儲單元.例如其定義了size (尺寸)決定占用多少字節,以及其structure(結構).例如,下圖是一個內存片段.顯示了起始地址在$00012344的4個字節.字節值分別為$4D, $65, $6D和$00.

技術分享圖片

註意上圖雖然使用了$00012344作為起始地址,但這是杜撰的,只是為了區別其他內存位置.並不是真實反映的內存地址,其依賴於很多事情,不可預測.

數據類型決定了如何使用這些字節.例如Integer類型其值為7169357 ($006D654D),或一個array[0..3] of Char類型,表示C風格的字符串‘Mem‘,或其他內容,如集合變量,幾個單字節變量,一個小結構體, SingleDouble類型的一部分等等.換句話說,在不知道存儲的變量類型前,內存中存儲的值的意義是無法推測的.

變量的地址是其第一個字節的地址.上圖中,假設是一個Integer,其地址為 $00012344.

未初始化變量

內存對於變量來說是可以重用的.通常內存為變量預留的時間與變量的生命周期一樣長.例如,函數或過程(兩者總稱例程)中的局部變量僅在例程運行期間可用.對象的域(也是一個變量)在對象存在期間可用.

如果聲明一個變量,編譯器預留出變量需要的字節數.但其中的內容是以前函數或過程使用的時候在字節中存放的.換句話說,未初始化的變量值是未定義的(但不是未定的).例如在如下簡單的控制臺程序中:

program uninitializedVar;

{$APPTYPE CONSOLE}

procedure Test;

var

A: Integer;

begin

Writeln(A); // uninitialized yet

A := 12345;

Writeln(A); // initialized: 12345

end;

begin

Test;

Readln;

end.

第一個顯示的值(未初始化的變量A)依賴於變量A存儲的地址中以前的值.本例中,顯示為2147319808 ($7FFD8000) ,但在其他計算機上會顯示不同的值.值是未定義的,因為其未初始化.在復雜的程序中, 尤其就指針而言這經常會導致程序癱瘓或意想不到的結果.賦值語句將變量A初始化為12345 ($00003039), 第二個值顯示正常.

指針

指針也是變量.但其中不存儲數值或字符,而是一個內存存儲單元的地址.如果將內存看做是一個大數組,指針可以看做是這個數組中的一個入口,指向數組中另一個數組的入口索引.( If you see memory as an array,a pointer can be seen as an entry in the array which contains the index of another entry in the array.)

假設有如下的聲明和初始化過程:

var

I: Integer;

J: Integer;

C: Char;

begin

I := 4222;

J := 1357;

C := ‘A‘;

並假如有如下的內存布局:

技術分享圖片

執行完這些代碼後,假如P是一個指針,

P := @I;

既有如下情形:

技術分享圖片

上圖中,顯示出了每個字節.通常是不必要的,因此可簡化為:

技術分享圖片

這個圖不能在反映出真實的內存占用大小(C看起來可I或J一樣大小),但對理解指針來說足夠了.

Nil

Thou shalt not follow the NULL pointer, for chaos and madness await thee at its end. — Henry Spencer

Nil 是一個特殊的指針值.可以賦值給任意類型的指針.其表示空指針(nil在拉丁語中是nihil,表示啥也沒有或零;或者說NIL表示Not In List).表示指針有定義的狀態,但不能訪問任何值(C語言中nil表示為NULL--見上面的引用).

Nil 不執行可用的內存,但作為一個預定義的值,很多例程可以對其進行檢查 (例如使用Assigned()函數).賦予了一個有效值後就無法檢測了,舊指針或未初始化指針與正常的指針沒有什麽不同. 沒有方法區別他們.程序邏輯必須確保指針或是有效的或是nil.

Delphi中, nil 的值是0,指向內存區域中的第一個字節.很明顯這個字節Delphi代碼是無法訪問到的.除非對後臺原理非常理解,通常不必關心nil等於0.nil 的值可能會在以後版本中出於某種目的進行修改.

類型指針

上例中P是Pointer類型的.意味著P包含一個地址,但不知道地址處的保存的變量信息. 指針之所以是通用的類型,指針可以解讀指向的內存區域中存儲的特定類型的內容.

假設有另一個指針, Q:

var

Q: ^Integer;

Q 的類型是^Integer,可以讀作”指向Integer” (^Integer相當於↑Integer).即這不是一個整數,而是指向一個常用的內存存儲單元.要將變量J的地址賦予Q,可以使用@ 地址操作符或定價的偽函數Addr,

Q := @J; // Q := Addr(J);

技術分享圖片

Q指向了局部地址$00012348 (指向了有變量J標識的內存存儲單元).但由於Q是一個類型化指針,編譯器將Q指向的內存單元看做是一個整數.Integer是Q的基本類型.

雖然很少看到使用偽函數Addr的代碼,其等價於@.對於復雜的表達式@有時很難看出是作用於那個部分.而Addr使用的是函數語法,配合小括號減少了混淆:

P := @PMyRec^.Integers^[6];

Q := Addr(PMyRec^.Integers^[6]);

通過指針賦值與直接使用變量有少許不同.通常只能通過指針進行操作.如果對普通變量賦值,形式如下:

J := 98765;

將整數值98765 (十六進制$000181CD)賦予內存存儲單元.使用指針Q來存取內存存儲單元,必須通過間接方式,使用^操作符:

Q^ := 98765;

這叫做降低引用(dereferencing). 假想虛擬箭頭指向Q中的地址(這裏是$00012348)並將對其賦值.

對於結構體,如果沒有異議,語法上可以省去^操作符.為了清晰,建議保留..

通常將常用的類型指針預先定義好.例如,^Integer不能用於參數傳遞,需要預先定義一個類型:

type

PInteger = ^Integer;

procedure Abracadabra(I: PInteger);

事實上, Pinteger類型和其他常用指針類型已經在Delphi的運行時庫 (例如SystemSysUtils單元)中預先定義.通常命名為P加上指向的類型名稱.如果基本類型名稱以T作為前綴,則忽略T.例如:

type

PByte = ^Byte;

PDouble = ^Double;

PRect = ^TRect;

PPoint = ^TPoint;

匿名變量

上例中按需定義了變量.有時無法確認是否需要一個變量,以及多少個變量.通過指針就可以使用匿名變量. 可在運行時預留內存,並返回一個指針.使用偽函數New():

var

PI: PInteger;

begin

New(PI);

New() 是一個編譯器偽函數.其為PI預留基本類型大小的內存,並使PI指向這塊內存區域(其中存儲了內存區域的地址).變量沒有名稱,因此是匿名的.只能提供指針間接訪問.現在可對其賦值,在函數間傳遞,在不需要的時候調用Dispose(PI)釋放:

PI^ := 12345;

ListBox1.Add(IntToStr(PI^));

// lots of code

Dispose(PI);

end;

除了New和Dispose,也可調用更低級別的函數GetMem和FreeMem.但New 和Dispose有幾個優點.他們已經知道指針的基本類型,並對內存存儲單元進行必要的初始化和釋放.因此無論何時都優先使用New和Dispose替代GetMem和FreeMem.

確保每個New()都有一個對應的使用相同類型和指針的Dispose()調用,否則變量就不會被正確的釋放.

可能相對於直接使用變量的優點不是很明顯,但對於不知道需要多少個變量的情形很有用.假如一個鏈接列表(see below),或一個TList. TList存儲指針,如果想在List中存儲Double值,可以簡單的對每個值調用New()並將指針存儲在Tlist中:

var

P: PDouble;

begin

while HasValues(SomeThing) do

begin

New(P);

P^ := ReadValue(SomeThing);

MyList.Add(P);

// etc...

當然在列表不再使用的階段要對每個值調用Dispose().

使用匿名變量,可以容易的闡述通過類型指針操作內存.兩個指針類型不同,執行同一塊內存,可以顯示出不同的值:

program InterpretMem;

{$APPTYPE CONSOLE}

var

PI: PInteger;

PC: PAnsiChar;

begin

New(PI);

PI^ := $006D654D; // Bytes $4D $65 $6D $00

PC := PAnsiChar(PI); // Both point to same address now.

Writeln(PI^); // Write integer.

Writeln(PC^); // Write one character ($4D).

Writeln(PC); // Interpret $4D $65 $6D $00 as C-style string.

Dispose(PI);

Readln;

end.

PI 將內存存儲單元填充為$006D654D (7169357).下圖(註意地址純粹是虛構的):

技術分享圖片

PC 指向了同一塊內存(由於基本類型不同,不能直接進行賦值,必須進行類型轉換).但PC 指向一個AnsiChar,因此如果調用PC^, 獲取到的是一個AnsiChar, ASCII字符值為$4D或‘M‘.

PC 是特殊情況,盡管是PAnsiChar類型, 由於實際是指向AnsiChar的指針,處理起來和其他類型指向稍有不同.在其他文章中解釋another article.PC,如果沒有降低引用,通常看做是指向以#0結尾的字符串,調用Writeln(PC) 將$4D $65 $6D $00字節顯示為‘Mem‘.

當思考指針問題,尤其是復雜指針,我都準備一頁紙和鋼筆或鉛筆,繪制如本文所見的圖.變量的地址也是偽造的(並不是32位的,而是像30000,40000, 40004, 4000850000便於理解就好).

壞指針

適當使用指針是很有用而且靈活的.但一旦出錯將是一個大問題.這也是很多人盡量避免使用指針的原因.這裏描述常見錯誤.

未初始化指針

指針是變量,可其他變量一樣需要初始化,或賦予其他指針值,或使用NewGetMem 等等:

var

P1: PInteger;

P2: PInteger;

P3: PInteger;

I: Integer;

begin

I := 0;

P1 := @I; // OK: using @ operator

P2 := P1; // OK: assign other pointer

New(P3); // OK: New

Dispose(P3);

end;

例如如果簡單的聲明PInteger,但沒有初始化,指針包含了一個隨機字節值,其指向了隨機的內存區域.

如果訪問這個隨機的內存地址,會發生令人厭惡的事情.如果這個內存地址不在當前應用程序預留範圍內,獲取一個非法訪問(Access Violation)錯誤,程序崩潰.但如果內存在應用程序預留範圍,並寫了數據,可能修改了不應修改的數據.如果數據在程序的其他部分稍後使用,會導致程序的數據錯誤.這樣的錯誤很難查找.

事實上,如果獲取到AV或其他類型的明顯錯誤,是值得高興的(除了硬盤損壞).應用程序崩潰不好,但問題很容易定位和修改.但對於錯誤數據和結果,問題更加糟糕,可能沒有註意到或很久才爆發.因此使用指針必須及其謹慎.要細致的檢查未初始化的指針.

舊指針

舊指針是以前有效的指針,但後來過時了.經常因為指針指向的內存區域被釋放和重用.

經常發生舊指針的情況是內存被釋放,但指針仍舊被使用.為避免這種情況,有些程序員在釋放內存後總將指針設置為nil.並在訪問內存前檢查其是否為nil.換句話說,nil是指針不可用的標誌.這是一種途徑,但並不總有效.

另一種常見情況是多個指針執行同一個內存區域,然後用其中的一個指針釋放內存.即使將那個指針置為nil,其他指針還是指向了這塊被釋放的內存.如果幸運報非法指針錯誤,但具體會發生什麽事情是不確定的.

第三,相似的問題是指向不穩定的數據,例如,數據會隨時消失.最大的錯誤是在函數中返回一個指向局部數據的指針.一旦例程結束,數據消失,局部變量不再存在.典型(愚蠢)的範例:

function VersionData: PChar;

var

V: array[0..11] of Char;

begin

CalculateVersion(V);

Result := V;

end;

V位於過程棧.這是每個運行時函數用來存放局部變量和參數,以及敏感的函數返回地址的地方.結果值指向V (PChar 可以直接指向數組,見提及的article).VersionData 結束後,棧被另一個運行的例程改變,無論CalculateVersion計算的結果是什麽都以過時,指針指向了同一棧位置的新內容.

同樣的問題還有Pchar指向一個字符串,請見article about PChars.指針指向動態數組元素也是一個經典問題,因為動態數組變小或調用SetLength後會被移動.

使用錯誤的基本類型

事實上指針可以指向任意內存存儲單元,兩個不同類型的指針可以指向同一個區域,意味著可以按不同方式存取同一個內存區域.使用指向Byte(^Byte)的指針,可以按字節修改整數或其他類型的內容.

但也可能會寫覆蓋(overwrite)或讀覆蓋(overread).例如,如果用整數指針去訪問存儲一個字節的內存區域,將會寫4個字節,而不僅僅是Byte類型的單字節,還有其後的3個字節,因為編譯器將這連續的4個字節看做是一個整數.同樣讀取這個字節,也會多讀3個字節:

var

PI: PInteger;

I, J: Integer;

B: Byte;

begin

PI := PInteger(@B);

I := PI^;

J := B;

end;

J 有正確的值,因為編譯器會填充0將單字節擴展成為一個整數(4字節).但變量I則不同,其包括一個字節,以及其後的3個字節,組成了未定義的值.

指針允許不通過變量本身來設置變量的值.這可能在調試過程中很疑惑.知道一個變量的值錯誤,但不知道哪裏的代碼修改了變量,因為其是通過指針設置的.

所有者和孤兒

指針不但有不同的基本類型,還有不同的所有者語義.如果使用New或GetMem,或其他特定例程申請內存,你就是內存的所有者.很好,如果要持有這塊內存,需要將指針保存到一個安全的地方.這個指針是唯一可訪問這塊內存的途徑,如果其中的地址丟失,就無法在訪問或釋放這塊內存了.一個規則是申請的內存必須被釋放,因此你有責任照顧好他.設計良好的程序必須考慮到這些.

理解所有者是很重要的.擁有內存的人必須負責釋放內存.可以找代理來執行這個任務,但必須確保執行正確..

一個常見的錯誤是使用指針指向一塊分配的內存,而後又將這個指針指向另外一個內存塊.指針指向第一個內存塊又再次指向了另外一個內存塊,原來的內存塊丟失.已經沒有辦法找回第一個申請到的內存塊.內存塊成為了孤兒.已無法再次訪問並處理他.這也叫做內存泄露.

這是摘自Borland的新聞組中的範例代碼:

var

bitdata: array of Byte;

pbBitmap: Pointer;

begin

SetLength(bitdata, nBufSize);

GetMem(pbBitmap, nBufSize);

pbBitmap := Addr(bitdata);

VbMediaGetCurrentFrame(VBDev, @bmpinfo.bmiHeader, @pbBitmap, nBufSize);

事實上,這個代碼有幾個沖突的地方. SetLengthbitdata分配字節.出於某種原因程序員使用GetMempbBitmap分配了同樣數量的字節.而後將pbBitmap指向了另外一個內存地址,導致由分配GetMem的內存無法被訪問.( pbBitmap是唯一訪問的途徑,但現在不在指向他了).換句話說,內存泄露了.

事實上,還有幾個錯誤. bitdata 是一個動態數組,獲取bitdata的地址只是得到了一個指針的地址,而不是緩沖區中第一個字節的地址(更多信息參見下面的動態數組).而且,由於pbBitmap也是指針,在函數調用時使用@操作符傳遞參數是錯誤的.

正確的代碼如下:

var

bitdata: array of Byte;

pbBitmap: Pointer;

begin

if nBufSize > 0 then

begin

SetLength(bitdata, nBufSize);

pbBitmap := Addr(bitdata[0]);

VbMediaGetCurrentFrame(VBDev, @bmpinfo.bmiHeader, pbBitmap, nBufSize);

end;

或者:

var

bitdata: array of Byte;

begin

if nBufSize > 0 then

begin

SetLength(bitdata, nBufSize);

VbMediaGetCurrentFrame(VBDev, @bmpinfo.bmiHeader, @bitdata[0], nBufSize);

end;

看起來是很嚴重的問題,但是在復雜代碼中很容易出現.

註意指針不必一定執行自己的內存塊.指針通常用來遍歷數組(如下),或操作結構體中的成員.如果沒有為其分配內存,就不必負責對內存塊進行控制.可以看做是用完就失效的臨時變量.

指針運算和數組

You can either have software quality or you can have pointer arithmetic, but you cannot have both at the same time. — Bertrand Meyer

Delphi allows some simple manipulations of a pointer. Of course you can assign to them, and compare them for equality (if P1 = P2 then) or inequality, but you can also increment and decrement them, usingInc and Dec. The neat thing is that these increments and decrements arescaled by the size of the base type of the pointer. An example (note that I set the pointer to a fake address. As long as I don‘t access anything with it, nothing bad will happen):

program PointerArithmetic;

{$APPTYPE CONSOLE}

uses

SysUtils;

procedureWritePointer(P: PDouble);

begin

Writeln(Format(‘%8p‘, [P]));

end;

var

P: PDouble;

begin

P := Pointer($50000);

WritePointer(P);

Inc(P); // 00050008 = 00050000 + 1*SizeOf(Double)

WritePointer(P);

Inc(P, 6); // 00050038 = 00050000 + 7*Sizeof(Double)

WritePointer(P);

Dec(P, 4); // 00050018 = 00050000 + 3*Sizeof(Double)

WritePointer(P);

Readln;

end.

The output is:

00050000

00050008

00050038

00050018

The utility of this is to provide sequential access to arrays of such types. Since (one-dimensional) arrays contain consecutive items of the same type — i.e. if one element is at addressN, then the next element is at address N+SizeOf(element) —, it makes sense to use this to access items of an array in a loop. You start with the base address of the array, at which you can access the first element. In the next iteration of the loop, you increment the pointer to access the next element of the array, and so on, and so forth:

program IterateArray;

{$APPTYPE CONSOLE}

var

Fractions: array[1..8] of Double;

I: Integer;

PD: ^Double;

begin

// Fill the array with random values.

Randomize;

for I := Low(Fractions) to High(Fractions) do

Fractions[I] := 100.0 * Random;

// Access using pointer.

PD := @Fractions[Low(Fractions)];

for I := Low(Fractions) to High(Fractions) do

begin

Write(PD^:9:5);

Inc(PD); // Point to next item

end;

Writeln;

// Conventional access, using index.

for I := Low(Fractions) to High(Fractions) do

Write(Fractions[I]:9:5);

Writeln;

Readln;

end.

Incrementing a pointer is, at least on older processors, probably slightly faster than multiplying the index with the size of the base type and adding that to the base address of the array for each iteration.

In reality, the effect of doing it this way is not nearly as big as you might expect. First, modern processors have special ways of addressing the most common cases using an index, so there is no need to update the pointer too. Second, the compiler will generally optimize indexed access into the pointer using version anyway, if this is more beneficial. And in the above, the gain found by using a slightly more optimized access is largely overshadowed by the time it takes to perform the Write().

As you can see in the program above, you can easily forget to increment the pointer inside the loop. And you must either usefor-to-do anyway, or use another way or counter to terminate the loop (which you must then also decrement and compare manually). IOW, the code using the pointer is generally much harder to maintain. Since it is not faster anyway, except perhaps in a very tight loop, I would be very wary of using that kind of access in Delphi. Only do this if you have profiled your code and found pointer access to be beneficial and necessary.

Pointers to arrays

But sometimes you only have a pointer to access memory. Windows API functions often return data in buffers, which then contain arrays of a certain size. Even then, it is probably easier to cast the buffer to a pointer to an array than to use Inc or Dec. An example:

type

PIntegerArray = ^TIntegerArray;

TIntegerArray = array[0..65535] of Integer;

var

Buffer: array of Integer;

PInt: PInteger;

PArr: PIntegerArray;

...

// Using pointer arithmetic:

PInt := @Buffer[0];

for I := 0 to Count - 1 do

begin

Writeln(PInt^);

Inc(PInt);

end;

// Using array pointer and indexing:

PArr := PIntegerArray(@Buffer[0]);

for I := 0 to Count - 1 do

Writeln(PArr^[I]);

...

end;

Delphi 2009

In Delphi 2009, pointer arithmetic, as usable for the PChar type (andPAnsiChar and PWideChar), is now also possible for other pointer types. When and where this is possible is governed by the new$POINTERMATH compiler directive.

Pointer arithmetic is generally switched off, but it can be switched on for a piece of code using {$POINTERMATH ON}, and off again using {$POINTERMATH OFF}. For pointer types compiled with pointer arithmetic (pointer math) turned on, pointer arithmetic is generally possible.

Currently, besides PChar, PAnsiChar and PWideChar, the only other type for which pointer arithmetic is enabled by default is thePByte type. But switching it on for, say, PInteger would simplify the code above considerably:

{$POINTERMATH ON}

var

Buffer: array of Integer;

PInt: PInteger;

...

// Using new pointer arithmetic:

PInt := @Buffer[0];

for I := 0 to Count - 1 do

Writeln(PInt[I]);

...

end;

{$POINTERMATH OFF}

So there is no need for the declaration of special TIntegerArray andPIntegerArray types to be able to access the type as an array anymore. Alternatively, instead of PInt[I], the (PInt + I)^ syntax could have been used, with the same result.

Apparently, in Delphi 2009, the new pointer arithmetic doesn‘t work as intended for pointers togeneric types yet. Whatever type the parametric type is instantiated as, indices are not scaled bySizeOf(T), as expected.

References

Many types in Delphi are in fact pointers, but pretend not to be. I like to call these typesreferences. Examples are dynamic arrays, strings, objects and interfaces. These types are all pointers behind the scenes, but with some extra semantics and often also some hidden content.

Dynamic arrays

Multi-dimensional dynamic arrays

Strings

Objects

Interfaces

Reference parameters

Untyped parameters

What distinguishes references from pointers is:

技術分享圖片 References are immutable. You can not increment or decrement a reference. References point to certain structures, but never into them, like for instance the pointers that point into an array, in the examples above.

技術分享圖片 References do not use pointer syntax. This hides that they are in fact pointers, and makes them hard to understand for many, who do not know this, and therefore do things with them they would better not do.

Do not confuse such references with C++‘s reference types. These are different in many ways.

動態數組

在Delphi4之前,動態數組還不是語言的一個特性,但存在這個概念.動態數組是運行時分配的內存塊,並通過指針進行管理.動態數組可以增長或壓縮.這意味著需要重新分配指定大小的內存塊,原來內存塊中的內容需要拷貝到新的內存塊中,原來內存塊被釋放掉,指針指向新的內存塊.

Delphi中的動態數組(如array of Integer)類型也是這樣的.但由運行時附件的代碼來管理內存的讀取和分配.如下的內存存儲單元指針指向的地址有兩個附加的域:分配的元素數量和引用數量.

技術分享圖片

如果如上圖所示,N是動態數組變量的地址,那麽引用數量(reference count)的地址就是N-8,分配的元素數量(length指示器)是N-4.第一個元素的地址是N.

每增加一個引用(如賦值,參數傳遞等),引用計數就加一,每次解除引用(如離開變量的作用域,或包含動態數組成員的對象被釋放,或指向動態數組的變量指向了其他動態數組或nil)引用計數就減一.

使用低層次例程MoveFillChar或其他例程如TStream.Write存取動態數組經常出錯.對於一個正常的數組(為區別於動態數組,將其叫做靜態數組),變量與內存塊等價的.而對於動態數組,情況就不是這樣的了.因此如果一個例程想要按內存塊的方式存取數組中的元素,就不能引用動態數組變量,而是需要使用動態數組中的第一個元素.

var

Items: array of Integer;

...

// Wrong: address of Items variable is passed

MyStream.Write(Items, Length(Items) * SizeOf(Integer));

...

// Correct: address of first element is passed

MyStream.Write(Items[0], Length(Items) * SizeOf(Integer));

註意上面代碼中, Stream.Write 使用無類型的var 參數,也是引用傳值.下面會進行討論.

多維動態數組

上面討論的是一維動態數組.動態數組也可以是多維的.但只是語法層面上的,事實上不是的.多維動態數組實際上是一個指向另外一維數組的一維數組.

假如有如下聲明:

type

TMultiIntegerArray = array of array of Integer;

var

MyIntegers: TMultiIntegerArray;

現在看起來聲明了一個多維數組,而且可以通過MyIntegers[0, 3]的方式存取其中的元素.但是聲明的類型應該這樣讀(語法層面上):

type

TMultiIntegerArray = array of (array of Integer);

或者更加明確的描述為:

type

TSingleIntegerArray = array of Integer;

TMultiIntegerArray = array of TSingleIntegerArray;

可見, TMultiIntegerArray 事實上是一個指向TSingleIntegerArray的一維數組.這樣TMultiIntegerArray存儲區域就不是按行和列排列的連續內存塊,實際上是不定長的數組,如每個元素都指向了另一個數組,每個子數組都有不同的大小.因此對於

SetLength(MyIntegers, 10, 20);

(將分配10個TSingleIntegerArrays ,每個子數組有20個整數,表面上是一個矩形數組), 可以存取和修改每個子數組:

SetLength(MyIntegers, 10);

SetLength(MyIntegers[0], 40);

SetLength(MyIntegers[1], 31);

// etc...

字符串

Should array indices start at 0 or 1? My compromise of 0.5 was rejected without, I thought, proper consideration. — Stan Kelly-Bootle

字符串在很多方面都與動態數組相同.也有引用計數,有相同的內部結構,一個引用計數和指示其中存儲的字符串數據的長度(在同樣的偏移量上).

不同之處在語法和語義上.不能將字符串設置為nil,可以設置為‘‘ (空字符串)來清空他.字符串也可是一個常量(引用計數是-1,運行時例程中將其作為一個特殊值對其進行增減或釋放字符串).第一個元素的索引是1,而動態數組的第一個元素索引是0.

字符串更多信息見article about PChars and strings.

對象

對象— 更確切的說是類的實例,編譯器不會管理其生命周期.其內部結構很簡單.每個類實例的0偏移量處(相對於每個引用的指針執行的地址)有一個指針指向VMT表.其中包含指向類中的每個虛方法的指針.這個表的負偏移量中是關於類的其他信息.對此不作過多介紹.每個類只有一個VMT表(而不是每個對象).

實現了接口的類也有一個指向含有接口中被實現的方法表的指針,每個被實現的接口對應一個.這個表在負偏移量中也有一些額外的信息.這些偏移量處的對象指針指向與基類相關域的信息.編譯器知道具體細節.

在VMT指針和接口表指針後面,是對象的域,與結構體相似.

對象的RTTI數據和其他類的信息從對象的這些引用中獲取,如VMT指針指向的VMT表中等.編譯器知道如何獲取其余的數據,通常通過包含其他結構體指針的復雜結構體,甚至會循環引用來獲得.

下例中,假設如下聲明:

type

TWhatsit = class(TAncestor, IPrintable, IEditable, IComparable)

// other field and method declarations

procedure Notify(Aspect: TAspect); override;

procedure Clear; override;

procedure Edit;

procedure ClearLine(Line: Integer);

function Update(Region: Integer): Boolean; virtual;

// etc...

end;

var

Whatsit: TWhatsit;

begin

Whatsit := TWhatsit.Create;

對象布局如下圖:

技術分享圖片

接口

接口實際上上方法的集合.在內部,他們是指向一個指向了代碼的指針數組.假如有如下聲明:

type

IEditable = interface

procedure Edit;

procedure ClearLine(Line: Integer);

function Update(Region: Integer): Boolean;

end;

TWhatsit = class(TAncestor, IPrintable, IEditable, IComparable)

public

procedure Notify(Aspect: TAspect); override;

procedure Clear; override;

procedure Edit;

procedure ClearLine(Line: Integer);

function Update(Region: Integer): Boolean; virtual;

// etc...

end;

var

MyEditable: IEditable;

begin

MyEditable := TWhatsit.Create;

接口,實現對象,實現類和方法的關系如下:

技術分享圖片

MyEditable 指向了由TMyClass.Create創建對象中的IEditable指針.註意MyEditable 不是指向對象的起始地址,而是有一個偏移量.MyEditable指向對象中的一個指向指針列表的指針,其中包括接口中的每個方法實現.代碼會調整Self 指針(事實上指向了MyEditable) 指向一個對象的起始地址(通過在傳遞的指針中減去對象中IEditable的偏移量,然後調用真正的方法).這是類實現的接口中方法的存根.

例如,假設實例的地址是50000, TWhatsit實現的IEditable接口在實例中指針偏移量是16.那麽MyEditable指向50016. 50016處的IEditable 指針指向了在類中實現的接口方法表(例如在30000),而後指向方法的存根(如在60000).存根知道由Self傳遞的值在50016,減去16得到50000.只是實現接口的對象地址.存根而後通過將50000作為Self的地址調用真實的函數.

上圖為簡化忽略的QueryInterface, _AddRef和_Release的存根.

知道為什麽需要用鉛筆盒紙張了吧? ;-)

引用參數

引用參數通常叫做var 參數,但out 參數也是引用參數.

引用參數在實際傳參時並不會將真正的值傳遞給例程,而是傳遞參數地址.例如:

procedure SetBit(var Int: Integer; Bit: Integer);

begin

Int := Int or (1 shl Bit);

end;

或多或少等價於下面的代碼:

procedure SetBit(Int: PInteger; Bit: Integer);

begin

Int^ := Int^ or (1 shl Bit);

end;

不同之處:

技術分享圖片 沒有使用指針語法.使用參數名稱自動進行對參數解引用,即使用參數名稱就可操作目標變量,而不是指針.

技術分享圖片 引用參數不能被修改.使用參數名稱來執行目標變量,不能將其指向其他地址,或對其增減操作.

技術分享圖片 必須傳遞有地址的變量,例如,一個實際的內存存儲單元需要做一些變換.因此對於整型的引用參數,不能傳遞為17, 98765,或 Abs(MyInteger). 必須是一個變量(包括數組中的元素,對象或結構體中的成員等).

技術分享圖片 實參必須與聲明的參數類型相同,如,對聲明為TObject的參數不能傳遞為TEdit.為避免這個問題需要聲明無類型引用參數(untyped reference parameters).

語法上使用引用參數要比使用指針參數簡單.但需要註意一些潛規則.傳遞指針,增加了一級間接引用.換句話說如果使用指針P指向一個整數,要傳遞參數必須轉換為P^:

var

Int: Integer;

Ptr: PInteger;

Arr: array of Integer;

begin

// Initialisation of Int, Ptr and Arr not shown...

SetBit(Ptr^, 3); // Ptr is passed

SetBit(Arr[2], 11); // @Arr[2] is passed

SetBit(Int, 7); // @Int is passed

無類型參數

無類型參數也是引用參數,但可以是var, constout.可以傳遞任意類型的參數,簡化了可以接受任意大小和類型參數的例程編寫,但是這也需要有機制傳遞參數的類型信息,或作為類型無關的例程.訪問參數時必須進行類型轉換.

內部無類型參數也是作為指針傳遞的.如下兩個範例中,第一個通用例程可以填充任意大小的緩沖區,而參數Buffer的類型並不重要:

// Example of routine where type doesn‘t matter

procedure FillBytes(var Buffer; Count: Integer;

Values: array of Byte);

var

P: PByte;

I: Integer;

LenValues: Integer;

begin

LenValues := Length(Values);

if LenValues > 0 then

begin

P := @Buffer; // Treat buffer as array of byte.

I := 0;

while Count > 0 do

begin

P^ := Values[I];

I := (I + 1) mod LenValues;

Inc(P);

Dec(Count);

end;

end;

end;

第二個TTypedList 的子類TIntegerList中的方法:

function TIntegerList.Add(const Value): Integer;

begin

Grow(1);

Result := Count - 1;

// FInternalArray: array of Integer;

FInternalArray[Result] := Integer(Value);

end;

可見,使用指針必須傳遞一個實參地址,即使參數已經是需要的指針了.而且,間接引用級別增加.

要存取引用目標,可以簡單的作為正常的引用參數使用,但必須進行類型轉換,編譯器指定如何對指針進行解引用.

註意間接引用級別.如果需要使用FillBytes函數初始化動態數組,不能傳遞變量,而是需要傳遞數組中第一個元素.事實上,也可傳遞靜態數組的首地址.因此如果要將數組作為無類型引用參數的實參進行傳遞,最後傳遞第一個元素而不是數組本身,除非要故意的訪問錯誤的動態數組.

數據結構

指針很廣泛的用於數據結構中,如鏈接列表,樹和層等.在此不作討論.需要說明的是高級的結構沒有指針和引用是無法實現的,雖然很多語言(Java)官方說不使用指針.要了解更多結構的信息請閱讀相關專題的文檔.

如下是簡單的數據結構圖表,非常依賴於指針的鏈接列表:

技術分享圖片

如果使用這樣的結構,通常都是在類內部進行了封裝,使用指針可以減少類內部的實現難度,但不會將指針暴露在公共接口中.指針很強大,但很難駕馭,盡量避免使用.

結論

這裏給出了很多指針的知識面.另一方面,使用帶箭頭的圖表對理解復雜指針或接口變量,對象,類和代碼間的關系很有幫助.繪制的圖都是用於理解非常復雜的指針.

本文要闡述的是指針無處不在,即使沒有看到他們.但不能濫用,理解指針可以更好的理解下層機制,可以避免很多錯誤.

希望對於大家有所幫助.本文一定不是很全面,需要改進之處請e-mail.

Rudy Velthuis

https://blog.csdn.net/henreash/article/details/7368088

Delphi的指針(有圖,很清楚)