delphi.指針.應用,delphi指針應用
注:初稿...有點亂,可能增刪改...
因為指針應用,感覺不好寫,請大家指出錯誤,謝謝。
注意: 本文著重點講的是指針的各類型的應用或使用,而不是說這種方法不應該+不安全+危險+不提倡使用。
其它:本文說的是x86環境,x64會有變化,且,只是講述一些方法,細節部分,如果涉及到不同平台問題,勿太深究。:)
指針:按正規解釋是:“指向另一內存塊地址的變量”,它是一個變量值,只有4字節(x86=>sizeof(Pointer)=4, x64=8,以下都以x86為准)。
所以,它與內存其實息息相關,所以講述前,我們要懂一個道理,指針,其實就是一個內存塊地址的“代號”。
指針應用: 常用操作就是:New/GetMem後進行操作,然後Dispose/FreeMem,估計大伙都用的多了,這個不用多說了。
所想寫的,是自己看到過的,寫過的,遇到的及其延伸,總結一下,希望對大家有幫助。
指針方法一:強制轉換
<警告:這種操作也最危險,不安全,容易造成越界形為,且難以發現問題>。
先將警告放上面,這個表示:要慎重對待些操作,原因:
a: 容易越界,且無錯誤提示。
越界即在超出規定安全范圍內,引申在此,則是說操作:安全內存塊范圍外的內存塊
有點繞口,不過很容易理解,安全內存塊4字節,如果操作了4字節外的內存外就是越界。
越界的危害是嚴重級別,且難找的,如果細說,能說一堆,這裡略過,因為著重點不在於此。
b:數據不正確
強制轉換,對於非恰當的數據時,它直接更改是數據值,在安全操作(不超出內存邊界情況下),無任何提示,事後難以查覺。
數據的正確性給破壞,且無錯誤,對一個程序的危害是不言而喻了。
好了,危害說完,我得說:強制轉換操作,確實很好用,且高效。
下面開始列舉我經常使用的操作:
1:Pointer 與 TObject及子類實例的轉換
Pointer與對象實例的轉換可以互換的,且沒有編繹提示。
因為對象實例其實說白了就是一個指針,只不過編繹器進行檢查,來個編繹錯誤,不讓你轉換。
其它:Pointer對其它數據類型的指針,也不需要:v := PDataType(p)這樣寫,直接: v := p; 反過來亦然。
注意:僅僅是Pointer類型, 所以,Pointer類型是強大的。
UI相關:在UI操作,很多組件是帶有用戶自定義的屬性,用於用戶擴展屬性的關聯,如:
TStrings.Objects (TCombobox.Items/TListBox.Items/TMemo.Lines...)
TListItem.Data/TListNode.Data
TComponent.Tag: Integer --(只針對x86,不知道x64改為NativeInt沒)
這類相關的擴展屬性,要麼是Pointer,要麼是TObject,如果自己需要與之上下文相關的擴展數據,最方便使用了。

![]()
1 type
2 PMyData = ^TMyData;
3 TMyData = record
4 v1: Integer;
5 v2: string;
6 end;
7
8 // 對擴展屬性設置
9 var
10 entry: PMyData;
11 begin
12 new(entry);
13 entry.v1 := xx;
14 entry.v2 := yy;
15 Combobox1.Lines.AddObject('test', Pointer(entry));
16 Listbox1.Items.AddObject('test', Pointer(entry));
17 with Listview1.Items.Add do
18 begin
19 caption := 'test';
20 subItems.Add('sub-item');
21 data := entry;
22 end;
23 end;
24
25 // 從擴展屬性中讀取
26 var
27 index: Integer;
28 entry: PMyData;
29 begin
30 index := Combobox1.ItemIndex;
31 if index <> -1 then
32 begin
33 entry := Pointer(Combobox1.Lines.Objects[index]);
34 ShowMessage(entry.v2);
35 end;
36
37 // listbox1如上使用
38 // ListItem取的是data屬性
39 end;
View Code
其它轉換:
sizeof(Pointer) = sizeof(Cardinal) = sizeof(Integer) = ... = 4 (x86)
所以,更多的時候,這類轉換也是常用的,如:指針前進X字節:Pointer(Cardinal(p) + x); (x86)
還有Pointer與Cardinal/Integer相互轉換,p = Pointer(v); v := Cardinal(p); ...
Pointer類型轉換很方便,所以,寫組件時,為需要的類增加一個CustomData: Pointer,會是一種常態的寫法:)
2: buffer/Pointer與各類數據轉換,及相關操作
更多的時候,我們需要與各種數據類型打交道,進行數據操作,協議封包(數據打包)
示例:發送一個數據包:格式:前4字節為長度,後4命令字,再根據命令字,進行跟隨X字節。
通常的做法是:TMemoryStream,然後不斷按協議進行Stream.Read/Write?經常能見到此似代碼。
還有種做法:用string(ansi版本下)來代替TMemoryStream,因為Pos+Delete是相當方便,不過對於裡面的代碼,只能表示呵呵。
如果現在再寫,會是寫成如下:

![]()
1 // 首先, 先定義好數據格式
2 const
3 CMD_01 = $0001;
4 CMD_02 = $0002;
5
6 type
7 PProtocolData = ^TProtocolData;
8 TProtocolData = packed record
9 len: int32;
10 cmd: int32;
11 case Integer of
12 CMD_01: ( cmd_01: TProtocolCmd01; );
13 CMD_02: ( cmd_02: TProtocolCmd02; );
14 end;
15
16 // 打包/封包,直接利用格式,進行轉換+寫入
17 var
18 data: PProtocolData;
19 buffer: array [0..MAX_SIZE - 1] of Byte;
20 begin
21 data := @buffer[0];
22 data.len := xx;
23 data.cmd := CMD_01;
24 data.cmd_01 := cmd_01_data;
25 send(data, data.len);
26 end;
27
28 // 解包,直接用數據格式指針轉換buffer
29 procedure do_some(buffer: Pointer; size: int32);
30 var
31 data: PProtocolData;
32 begin
33 data := buffer;
34 if size < data.len then
35 errormsg_and_exit('data packet is invalid.');
36
37 case data.cmd of
38 CMD_01: do_cmd_01(data.cmd_01);
39 CMD_02: do_cmd_02(data.cmd_02);
40 end;
41 end;
View Code
額,得注意:只適合定長的類型,如果有不定長的格式,buffer無法確認最大長度的,就得GetMem出場了(或SendBuffer+SendBufLen)
這寫法的好處:
其一:數據類型更改好動手,比如協議版本升級,在cmd後面要加個seq: int32字段,按Stream的作法,你得先找到cmd寫入的地方,
後面加句:Stream.Write(seq...),位置順序不能變,如果位置不對,你就得抓瞎,抓協議數據包來找問題了。
如按上面,只要在定義中,cmd字段後加:seq: int32,然後,找個地方賦值就好了。
且重要的是:可以通過record定義,來知道協議的那幾個字節都是做什麼的,啥意思,這給後來開發人員減少出錯的機會。
其二:解析(解包)簡單
收到協議包的buffer後,判斷一下包長度是否正常,正常就直接轉換為對應指針類型,然後就p.xx就讀取操作了。
如果按stream的方法,你得不停的Stream.Read(xxx)...
好了,你還用Stream的方法做協議打包解包嗎?:D
其它定長轉換,還有種典型就是:如果是固定長度格式的字符串解析,可以使用先定義,再轉換,如時間:

![]()
1 // s = '2014-10-01 08:09:10'
2 function ToDateTime(const s: string): TDatetime;
3 type
4 PMyDateString = ^TMyDateString;
5 TMyDateString = packed record
6 YY: array [0..3] of Char;
7 S1: Char;
8 MM: array [0..1] of Char;
9 S2: Char;
10 DD: array [0..1] of Char;
11 S3: Char;
12 HH: array [0..1] of Char;
13 S4: Char;
14 NN: array [0..1] of Char;
15 S5: char;
16 SS: array [0..1] of Char;
17 end;
18 var
19 p: PMyDateString;
20 yy, mm, dd, hh, nn, ss: Word;
21 begin
22 if Length(s) < sizeof(TMyDateString) then
23 error_and_exit('invalid date string');
24
25 p := Pointer(s);
26 yy := ToWord(p.YY, sizeof(p.YY));
27 mm := ToWord(p.MM, sizeof(p.MM));
28 ...
29 result := EncodeDate(yy, mm, dd) + EncodeTime(hh, nn, ss, 0);
30 end;
View Code
注:此法,只合適那種有固定格式的情況。
這裡只是舉例,是種思路,不是建議。一時半會的想不到更好的示例,就想到這個(時間一堆的函數可以轉換,不用這樣寫)
3:Pointer與var的轉換
這個,不知怎麼說了,所以寫成var了,這個不好解釋,請查示例:

![]()
1 Delphi定義
2 function ReadFile(hFile: THandle; var Buffer; nNumberOfBytesToRead: DWORD;
3 var lpNumberOfBytesRead: DWORD; lpOverlapped: POverlapped): BOOL; stdcall;
4 C++定義
5 BOOL ReadFile(
6 HANDLE hFile, // handle of file to read
7 LPVOID lpBuffer, // address of buffer that receives data
8 DWORD nNumberOfBytesToRead, // number of bytes to read
9 LPDWORD lpNumberOfBytesRead, // address of number of bytes read
10 LPOVERLAPPED lpOverlapped // address of structure for data
11 );
12
13 var Buffer ==> LPVOID lpBuffer
14 var lpNumberOfBytesRead: DWORD; ==> LPDWORD lpNumberOfBytesRead
View Code
上面只是一個函數聲明,其中第二參數與第四參數很有意思。
var buf; const buf; 在System.Move/Stream.Read/Write中很常見這類參數,具體名稱,這個還真沒注意,無類型參數(暫且這樣叫)
如果對應於C/C++來說,它應該是LPVOID,這是D自有的數據類型,它是需要傳遞數據內存塊起始位置。如string[1], Integer/及sizeof可計算長度的,直接傳遞。
扯遠了,繼續看第四參數,才是我要表達的重點,var X: DWORD 等價於 X: PWORD; 這是D中最自由的部分。
然後延伸:我定義一函數/方法或回調指針,如:

![]()
1 type
2 TMyEvent = procedure(AParam: Integer; Custom: Pointer);
3
4 procedure DoJob(event: TMyEvent; custom: Pointer);
5 begin
6 if assigned(@event) then
7 event(10, custom);
8 end;
9
10
11 // 上面是最簡單的,也是經典回調或事件寫法吧,經常寫事件或接口API,都明白怎麼回事。
12
13 // 然後調用,對於custom: Pointer就自由了,請看:
14 type
15 PMyData = ^TMyData;
16 TMyData = record
17 x, y: Integer;
18 end;
19
20 procedure OnEvent1(param: Integer; var data: TMyData);
21 begin
22 data.x := param * 2;
23 data.y := param * 3;
24 end;
25
26 procedure TForm1.Button1Click(Sender: TObject);
27 var
28 data: TMyData;
29 begin
30 data.x := 0;
31 data.y := 0;
32 DoJob(@OnEvent1, @data);
33 ShowMessageFmt('x: %d, y: %d', [data.x, data.y]));
34 end;
View Code
看明白了沒?OnEvent1對應的回調第二參數寫成var data: TMyData,且編繹+結果正確。
Pointer只是一個指針,var也是傳地址進行操作,本質是一樣的,所以,我們寫這個custom: Pointer是可以多變的。
以上,只是一個例子,請再回頭看Pointer與對象,其它數據類型轉換,你就可以自己繼續延伸,寫自己的自由寫法了。
指針方法二: 指針+-運算
指針的+-運算,即:p := p + 1; p := p - 1; 非inc(...)及 p := Pointer(Cardinal(p) + 1);此類。
D2007(包括)以下版本,這種操作僅限於PChar(PAnsiChar)可以這樣進行操作,D2010開始PByte也可以了:)
PAnsiChar +- len => PAnsiChar;
PAnsiChar +- PAnsichar => len
就這是原因,因為兩地址相加減,可得到長度,在字符串解析過程中,保留一個指針A,另一指針B進行規則匹配,
然後B-A,就得到一個規則內的長度,這個寫字符或內存狀態解析,是一個常用手法。
PAnsiChar支持下標(p[x] := xx; (p + x)^ := xx),其它, PByte要到高版本才支持。
注:個人更喜歡用PAnsiChar進行操作,而不是PByte, Cardinal, NativeUInt, IntPtr之類。
原因很簡單:
a: PAnsiChar從低版本(ansi)兼容到高版本的XE(unicode),且一直支持+-操作
b: PByte的+-到D2010才支持
c: Cardinal進行+-,到了XE2 x64後,就不對了,因為x64的指針值是8字節
d: NativeUInt/IntPtr低版本不支持。
。。。其它用法,想到再加。
指針應用方法三: 偏移
指針偏移在D裡面,估計大家都很少用它操作,但估計個個都在用。
因為不管用哪種語言,這種操作手法是最常用的,因為這手法,內存管理用的最多了:D
都是用了這些簡單的手法進行一系列的操作後,才返回給使用者的。
看下面一段很有意思的代碼:

![]()
1 type
2 PStrRec = ^TStrRec;
3 TStrRec = packed record
4 {$if defined(CPUX64)}
5 padding: LongInt;
6 {$ifend}
7 {$if CompilerVersion >= 20.0}
8 code: Word;
9 elem: Word;
10 {$ifend}
11 ref: Integer;
12 len: Integer;
13 data: array [0..0] of Char;
14 end;
15
16 procedure TForm1.FormCreate(Sender: TObject);
17 begin
18 ReportMemoryLeaksOnShutdown := True;
19 end;
20
21 procedure TForm1.Button1Click(Sender: TObject);
22 var
23 p: PStrRec;
24 s: string;
25 begin
26 p := AllocMem(sizeof(TStrRec) + sizeof(Char) * 10);
27 p.ref := 1;
28 p.len := 10;
29 StrPLcopy(p.data, 'abc', 3);
30
31 Pointer(s) := @p.data[0];
32 ShowMessage(s);
33 end;
View Code
輸入完這代碼後,運行後點擊一下button,顯示了abc的BOX,然後退出。是不是發現沒有洩露啊:)
上面代碼,其實就是模擬了string構成,p就是string的基本構成,然後s接管了p的生存期,然後出了Button1Click作用域後,s就會自動給free了。
額,扯遠了,string構成不是重點,重點是偏移,平常所用的string,其實就是一個偏移的手法,只不過D隱藏了。
上面例子中,Pointer(s) := @p.data[0]就是一個偏移的做法。
指針偏移概念(個人理解):
a: 後偏移:在真實地址後,根據自己的規則,進行移動固定字節後(後偏移),得到的新地址返回給調用者。
這種方式是:GetMem取得X+Y個字節,X=自己分配規則固定長度,Y=req.size,返回時,地址向後移動X (addr := P + X)
釋放的時候,將地址向後移動向前移動X字節,再行FreeMem,string就是這種情況。
b: 不偏移:申請到的地址,不移動地址,但它申請的長度比請求的大,其它長度,用於其它處理。
還是: P := GetMem(X+Y),其中,X長度是A處理所需,Y長度是B處理所需

![]()
1 type
2 PMyData = ^TMyData;
3 TMyData = record
4 v1, v2: Integer;
5 end;
6
7 PMyDataEx = ^TMyDataEx;
8 TMyDataEx = record
9 data: TMyData;
10 dataEx1: Integer;
11 dataEx2: Integer;
12 end;
13
14 procedure DoData(p: PMyData);
15 begin
16 p.v1 := 1;
17 p.v2 := 2;
18 end;
19
20 procedure DoDataEx(p: PMyData);
21 var
22 e: PMyDataEx;
23 begin
24 e := Pointer(p);
25 e.dataEx1 := 10;
26 e.dataEx2 := 20;
27 end;
View Code
額,好像跟偏移沒啥關系,不過,個人覺得還是歸納到這裡。
偏移,用話來說就是:你操作你的,我操作我的,各不相干。
使用指針偏移目地:
上面已經舉例偏移,可能有人會覺得麻煩,有啥子用。我也覺得這種方式的代碼確實有些限制,也不好寫。
但有幾點好處:
a: 減少內存分配次數:少一次分配就加一分效率,在某些場合是能省則省。
b:定位查找快,如果不用這法子,查找這地址與之相匹配的上下文(context),你得循環?hash?
這玩意用個指針+-的法子,就可以找到對應的數據,為何不用?
壞處也是有的:
a: 增加代碼復雜度
b: 出錯幾率相對大
哈,有好有壞,:)
先寫到這裡了。以後想到啥再寫,或整理,或細化一下,感覺寫得不是很工整。。。
額,水平有限,如有雷同,就是盜版!
2014.10.18 by qsl