程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> 更多編程語言 >> Delphi >> delphi.指針.應用,delphi指針應用

delphi.指針.應用,delphi指針應用

編輯:Delphi

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

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved