1. ホーム
  2. デルファイ

Delphi用ポインタ

2022-02-27 18:42:17
ポインターは、データ構造のある部分から別の部分へ乱暴に導くジャンプのようなものです。高級言語へのポインターの導入は、私たちが決して立ち直れないかもしれない後退であった。 - アンソニー・ホア

ポインターはおそらく最も誤解され、恐れられているデータ型なので、多くのプログラマはポインターを避けたがります。

しかし、ポインターは重要です。ポインターを明示的にサポートしていない言語や、ポインターを使いにくくしている言語でも、ポインターは舞台裏で非常に重要な役割を担っているのです。だから、ポインターを理解することは重要です。ポインターを理解するには、いくつかの方法があります。

この記事は、ポインタの理解や使い方に困っている読者を対象としています。Win32のDelphi環境の文脈で論じられています。すべての面をカバーしているわけではありませんが(例えば、アプリケーションのメモリは大きな連続した領域ではありません)、実用上役に立つと思います。ポインターは理解しやすいと思います。

メモリ

このパラグラフで私が何を書いているかはすでにご存知でしょうが、とにかく読んでみてください。

ポインタとは、他の変数を指し示す変数のことです。それを説明するためには、まず、メモリアドレスと変数の概念を理解する必要があります。まずは、コンピュータのメモリについて簡単に説明しましょう。

コンピュータのメモリは、単純化するとバイトの長い列と考えることができる。バイトはストレージの最小単位で、256種類の値(0~255)が格納されています。現在の32ビット版Delphiでは、メモリは最大2Gバイトの長さの配列と考えることができます。バイトに何が格納されているかは、その内容がどのように解釈されるか、例えば、どのように使用されるかによります。値97は97型のバイトと見ることができ、これは文字aと見ることができます。もし、複数のバイトが含まれていれば、より多くの値を格納することができます。256*256では2バイトで異なる値を表すことができる。

メモリ内のバイトは、0から始まって2147483647まで、数字でアクセスできます(2Gバイトあると、Windowsはこの効果を知らなくても仮想化しようとします)。この巨大な配列の中のバイトのインデックスをアドレスと呼びます。

また、1バイトはメモリ上で利用可能な最小のアドレス表現ブロックであるとも言えます。

実は、メモリはとても複雑なんです。コンピュータによっては、8ビットでないバイトを持ち、256以上の値も256以下の値も表現できるものがありますが、Delphi for Win32はそのようなコンピュータには遭遇しません。メモリはソフトウェアとハードウェアの両方で管理され、そのすべてが実際に存在するわけではありませんが(メモリマネージャはこれらの問題を解決するためにハードディスクとのスワップ領域も扱います)、この記事の目的では、メモリを複数のプログラム間で共有されるバイトの塊として考えることが役に立ちます。

変数

変数とは、プログラムに対して読み書きができる巨大な記憶装置の配列の1バイトまたは複数バイトのことです。これらは、名前、型、値、アドレスで識別されます。

変数が宣言されると、エディタは適切な大きさのメモリ領域を確保します。変数が格納される正確なアドレスは、コンパイラとランタイムコードによって決定されます。変数の正確なアドレスについては、仮定することはできません。

変数のタイプは、メモリ記憶装置の使用方法を定義します。例えば、以下のように定義されます。 サイズ (サイズ)は何バイト使用するかを決定し、その 構造体 (構造)です。例えば、次の図は、メモリ断片を表しています。これは、$00012344から始まる4バイトを表しています。バイトの値は、$4D、$65、$6D、$00です。

なお、上の画像では $00012344 を開始アドレスとしていますが、これは他のメモリ位置と区別するために作られたものです。メモリアドレスは様々なものに依存し、予測不可能であるため、本当の意味で反映されたものではありません。

データ型は、バイトがどのように使用されるかを決定します。例えば 整数 あるいは、C言語の文字列 'Mem' を表すChar型の配列[0..3]、あるいはコレクション変数、複数のシングルバイト変数、小さな構造体など、他の何かです。 シングル または ダブル の部分が型に含まれるなど。つまり、格納されている変数の型がわからないと、メモリに格納されている値の意味を推測することはできない。

変数のアドレスは、その最初の1バイトのアドレスです。上の図では、仮にそれが 整数 で、アドレスは $00012344 です。

未初期化の変数

メモリは変数に再利用可能です。通常、変数のためのメモリは、その変数の寿命と同じだけ確保されます。例えば、関数やプロシージャ(どちらもルーチンと総称されます)内のローカル変数は、そのルーチンが実行されている間だけ利用可能です。オブジェクトのドメイン(これも変数です)は、そのオブジェクトが存在する限り利用可能です。

変数を宣言すると、コンパイラはその変数に必要なバイト数を確保する。しかし、その内容は、関数やプロシージャで以前使用されていたバイトに格納される。つまり、初期化されていない変数の値は未定義(undefined)なのです(undefinedではありません)。例えば、次のような簡単なコンソール・プログラムでは。

プログラムuninitializedVarです。

{apptype コンソール}。

手続き Test;

ヴァル

  A: 整数です。

開始

  Writeln(A)です。 // まだ初期化されていない

  A := 12345;

  Writeln(A)です。 // 初期化されました。12345

を終了します。

開始

  テストします。

  Readln;

を終了します。

最初に表示される値(初期化されていない変数A)は、変数Aが格納されているアドレスの前の値に依存します。この場合、2147319808 ($7FFD8000) と表示されますが、他のコンピュータでは異なる値が表示されます。この値は初期化されていないため、未定義です。複雑なプログラムでは、特にポインターの場合、これが原因でプログラムがマヒしたり、予期せぬ結果になったりすることがよくあります。代入文は、変数Aを12345($00003039)に初期化し、2番目の値は正常に表示されます。

ポインタ

ポインタも変数です。しかし、値や文字を格納するのではなく、メモリ記憶装置のアドレスとなります。メモリを大きな配列と見立てた場合、ポインタはその配列の中の別の配列のエントリインデックスを指すものと考えることができます。( メモリを配列として見た場合。 ポインタは、配列内の別のエントリのインデックスを含む配列のエントリと見なすことができます。 .)

次のような宣言と初期化手順を想定してください。

ヴァル

  I: 整数。

  J: 整数

  C: Char;

開始

  I := 4222;

  J := 1357;

  C := 'A';

で、以下のようなメモリレイアウトの場合。

このコードを実行した後、Pがポインタである場合。

P := @I です。

次の両方の場合。

上の図では、各バイトを表示しています。これは通常不要なので、簡略化すると

このグラフはメモリのフットプリントの本当の大きさを反映していませんが(CはIやJと同じ大きさに見えるかもしれません)、ポインタを理解するには十分なものです。

Nil

汝、NULLポインターに従ってはならない。その先には混沌と狂気が汝を待っている。 - ヘンリー・スペンサー

Nil は特殊なポインタ値です。任意の型のポインタに割り当てることができます。NULLポインタを表します(nilはラテン語でnihil、何もない、ゼロという意味です;あるいはNILは リストにない ). これは、ポインタの状態は定義されているが、どの値にもアクセスできないことを意味します(C言語のnilはNULLです - 上記引用を参照してください)。

Nil は利用可能なメモリを実行しませんが、定義済みの値として、多くのルーチンはそれをチェックすることができます(たとえば アサインド() 関数)。有効な値を与えても検出されません。 古いポインタや初期化されていないポインタは、通常のポインタと何ら変わりはない . 両者を区別する方法はありません。プログラムロジックは、ポインタが有効なものか、あるいは ゼロ .

.NETのDelphi ゼロ の値です。 0 これは、メモリ領域の最初のバイトを指しています。明らかに、このバイトはDelphiのコードからはアクセスできません。バックエンドがどのように動作するかをよく理解していない限り、通常は ゼロ と同等です。 0 . ゼロ の値は、何らかの目的のために後のバージョンで変更される可能性があります。

タイプポインタ

上記の例におけるPは ポインター 型になります。つまり、Pはアドレスを持っていますが、そのアドレスに格納されている変数に関する情報は知りません。ポインターは、それが指すメモリ領域に格納されている特定の種類の内容を解釈できるため、汎用型であると言えます。

もう1つのポインタQがあるとします。

ヴァル

  Q:^Integer;

Qの型は^Integerで、"Pointing to Integer"と読むことができます(^Integerは↑Integerと等価です)。つまり、これは整数ではなく、一般的なメモリ記憶装置への参照である。変数JのアドレスをQに代入するには @ アドレス演算子、または価格設定用擬似関数 アドル ,

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

Q はローカルアドレス $00012348 を指しています(変数 J で識別されるメモリセルを指しています)。しかし、Q は型付きポインタなので、コンパイラは Q が指すメモリセルを整数として扱います。

まれにですが、@と等価な疑似関数Addrを使ったコードを見かけることがあります。複雑な式の場合、@はどの部分が作用しているのかがわかりにくいことがあります。Addrは関数の構文を使っており、括弧があることで混乱が少なくなっています。

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

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

ポインターを使った値の割り当ては、変数を直接使うのとは少し違います。通常、ポインタでしかできないことです。通常の変数に値を代入する場合、次のような形になります。

  J := 98765;

メモリメモリセルに整数値98765(16進数$000181CD)を代入する。ポインタQでメモリ・メモリセルにアクセスするには、^演算子を用いて間接的にアクセスする必要があります。

  Q^ := 98765;

これをリファレンスを下げるといいます( 再参照 ). 仮想矢印がQのアドレス(この場合は$00012348)を指していて、そこに値を代入することを想像してください。

構造体については、異議がなければ構文を省略することができます。 ^ 演算子を使用します。わかりやすくするために、.

一般的な型へのポインタをあらかじめ定義しておくことはよくあることです。例えば 整数 はパラメータ受け渡しには使えないので、あらかじめ定義された型が必要です。

タイプ

  PInteger = ^Integer です。

procedure Abracadabra(I: PInteger);

実は ピンテガー 型やその他の一般的なポインタ型は、すでにDelphiのランタイムライブラリで利用可能です(例. システム シスユーティリティ 単位)があらかじめ定義されています。通常、Pに指された型の名前を加えた名前になります。基本型名に T が付いている場合、T は無視されます。

タイプ

  PByte = ^Byteです。

  PDouble = ^Double。

  PRect = ^TRect。

  PPoint = ^TPoint;

匿名変数

上記の例では、オンデマンドで変数を定義しています。時には、変数が必要かどうか、いくつの変数が必要かを判断できないこともあります。ポインタを使えば 匿名変数 . 実行時にメモリを確保し、ポインタを返すことが可能です。擬似関数の使用 新規作成() :

ヴァル

  PI PInteger。

開始

  New(PI)です。

New() はコンパイラの擬似関数である。これはPIのために基本型サイズのメモリを確保し、PIにこのメモリ領域(メモリ領域のアドレスが格納されている)を指し示させるものです。この変数は名前を持たないので匿名です。間接的なポインタアクセスのみが可能です。この変数に値を代入したり、関数間で受け渡ししたり、不要なときに呼び出したりすることができます。 ディスポーザブル(PI) を使用して解放します。

  PI^ := 12345;

  ListBox1.Add(IntToStr(PI^));

// たくさんのコード

  Dispose(PI)です。

を終了します。

New と Dispose の他に、低レベルの関数 GetMem と FreeMem も呼び出せますが、New と Dispose にはいくつかの利点があります。これらはすでにポインタの基本型を知っており、必要な初期化およびメモリ記憶ユニットの解放を行います。したがって、可能な限りGetMemとFreeMemよりもNewとDisposeが好まれます。

すべての New() は、同じ型とポインターを使用する対応する Dispose() 呼び出しを持つことを確認してください、さもなければ、変数は正しく解放されません。

変数を直接使うよりも利点があることは明らかではないかもしれませんが、必要な変数の数がわからないような状況では便利です。もしリンクリスト ( 以下 TList にはポインタが格納されており、2 倍の値をリストに格納したい場合は、各値に対して New() を呼び出し、そのポインタを Tlist に格納すればよいのです。

ヴァル

  P:PDoubleです。

開始

  while HasValues(SomeThing) do

  開始

    New(P)です。

    P^ := ReadValue(SomeThing)。

    MyList.Add(P)。

// etc...

もちろん、Dispose()はリストが使われなくなった段階で、それぞれの値に対して呼び出されます。

無名変数を使うと、型ポインタによるメモリ操作を簡単に説明することができます。異なる型の2つのポインタを同じメモリブロック上で実行すると、異なる値を表示することができます。

プログラムInterpretMemです。

{apptype コンソール}。

ヴァル

  PI PInteger。

  PC: PAnsiChar。

開始

  New(PI)です。

  pi^ := $006d654d;     // バイト数 $4D $65 $6D $00

  PC := PAnsiChar(PI);  // 両方とも同じアドレスを指すようになりました。

  Writeln(PI^)です。         // 整数を書き込む。

  Writeln(PC^)です。         // 1文字(4D)を書き込む。

  Writeln(PC)です。          // 4D $65 $6D $00 を C スタイルの文字列として解釈する。

  Dispose(PI)を実行します。

  読み取りを行う。

を終了します。

PI メモリセルに$006D654D(7169357)を記入する。以下(アドレスは純然たる架空のものであることに注意)。

PC は同じメモリブロックを指しています(基本型が異なるため、直接値を代入することはできず、型変換を行う必要があります)。ただし PC を参照しています。 AnsiChar を呼び出すと PC であり、得られるのは AnsiChar ASCII文字の値は$4Dまたは'M'である。

PC は、特殊なケースですが PAnsiChar 型であり、実際には AnsiChar のポインタがある場合、他の型とは若干異なる扱いを受けます。他の記事で説明したように 別記事 . PC は、通常#0で終わる文字列を指していると見られる参照の低下がない場合、Writeln(PC)を呼び出して4D $65 $6D $00バイトを「Mem」と表示します。

ポインタ、特に複雑なポインタについて考えるとき、私はいつもページとペンか鉛筆を用意して、この記事のような図を描きます。変数のアドレスもフェイク(32ビットではなく、まるで 30000 , 40,000 , 40004 , 40008 および 50000 を理解しやすくするため)。

バッドポインタ

ポインターは適切に使用すれば、便利で柔軟なものです。しかし、何か問題が起きると大変なことになります。そのため、多くの人がポインターの使用を避けようとします。ここでは、よくある間違いについて説明します。

未初期化のポインタ

ポインタは他の変数と同様に初期化する必要があるか、別のポインタ値を与えるか、あるいは 新しい または GetMem などがあります。

ヴァル

  P1: PInteger。

  P2: PInteger;

  P3: PInteger。

  I: 整数。

開始

  I := 0;

  P1 := @I;  // OK: @ 演算子を使用する

  P2 := P1;  // OK: 他のポインタを代入

  New(P3)です。   // OKです。新規

  Dispose(P3)を実行します。

を終了します。

例えば、PIntegerを単に宣言しただけで、初期化していない場合、ポインタはメモリのランダムな領域を指すランダムなバイト値を含んでいます。

このランダムなメモリ・アドレスにアクセスすると、厄介なことが起こります。もし、そのメモリアドレスが現在のアプリケーションの予約範囲内にない場合、アクセス違反エラーが発生し、プログラムがクラッシュします。しかし、そのメモリがアプリケーションの予約範囲内にあり、データを書き込むと、変更されてはならないデータが変更される可能性があります。そのデータが後でプログラムの他の部分で使用されると、プログラムのデータエラーにつながる可能性があります。このようなエラーは発見するのが困難です。

実際、AVなどの明らかなエラーが出るのはいいことです(ハードディスクの破損は除く)。アプリケーションのクラッシュはまずいですが、問題の所在を突き止め、修正するのは簡単です。しかし、誤ったデータや結果では、問題はより悪化し、気づかれないか、噴出するのに長い時間がかかるかもしれません。したがって、ポインタの使用には十分な注意が必要です。初期化されていないポインタをチェックするように注意してください。

古いポインタ

古いポインターとは、以前は有効であったが、その後廃止されたポインターのことである。このポインタが指すメモリの領域は、しばしば解放され再利用されます。

古いポインターは、メモリが解放されたにもかかわらず、まだ使用されているポインターであることがよく起こります。これを避けるために、一部のプログラマは常にポインタを ゼロ . つまり、nil はそのポインタが使えないことを示すものです。これは一つの方法ですが、いつもうまくいくとは限りません。

もう1つのよくあるシナリオは、複数のポインタが同じメモリ領域で実行され、そのうちの1つを使用してメモリを解放することです。そのポインタが nil に設定されていても、他のポインタは解放されたメモリを指しています。運良く不正なポインタエラーを報告しても、何が起こるかは不明です。

第三に、同様の問題として、不安定なデータ、例えば、いつ消えてもおかしくないようなデータを指していることがあります。最も大きなエラーは、関数内でローカルデータへのポインタを返すことです。ルーチンが終了すると、データは消え、ローカル変数も存在しなくなります。典型的な(愚かな)例です。

関数 VersionData: PChar;

ヴァル

  V: Charの配列[0..11]。

開始

  CalculateVersion(V)を計算します。

  結果 := V;

を終了します。

Vはプロシージャスタックに配置されています。ここには、各実行関数のローカル変数とパラメータ、および関数の戻り値の機密アドレスが格納されます。結果値が指すのは V ( PChar は直接配列を指すことができます。 記事 ). バージョンデータ 終了後、別の実行ルーチンによってスタックが変更された場合は、関係なく 計算バージョン 計算結果は廃止され、ポインタは同じスタック位置の新しい内容を指す。

Pcharが文字列を指す場合にも同じ問題があります。 PCharsに関する記事 . 動的配列の要素を指すことも古典的な問題です。動的配列は、小さくなったり SetLength が呼ばれたりすると移動してしまいます。

誤ったベースタイプの使用

ポインターはあらゆるメモリ記憶装置を指すことができ、異なるタイプの2つのポインターが同じ領域を指すことができるということは、同じメモリ領域に異なる方法でアクセスできることを意味します。Byteへのポインタ(^Byte)を使えば、整数型などの内容を1バイト単位で変更することができます。

しかし、上書きや過読も可能である。例えば、1バイトを格納するメモリ領域に整数ポインタを使用してアクセスした場合、コンパイラはこの連続した4バイトを整数として扱うため、Byte型の1バイトだけでなく、次の3バイトも書き込むことになります。また、このバイトを読むと、さらに3バイトが追加されます。

ヴァル

  PI PInteger。

  I, J: 整数。

  B: バイト。

開始

  PI := PInteger(@B);

  I := PI^;

  J := B;

を終了します。

J は正しい値です。これは、コンパイラがゼロをパッドして1バイトを整数(4バイト)に拡張しているからです。しかし、変数Iの場合はそうではなく、1バイトの後に3バイトが続き、未定義の値を構成している。

ポインターは、変数そのものを設定することなく、その変数の値を設定することができます。これは、デバッグの際に混乱することがあります。ある変数の値が間違っていることは分かっていても、ポインターで設定されているため、コードがどこでその変数を変更したのかが分からないのです。

オーナーと孤児

ポインタは基本型だけでなく、所有者のセマンティクスも異なります。New や GetMem などの特定のルーチンを使ってメモリを要求した場合、あなたがそのメモリの所有者になります。さて、このメモリを保持したいのであれば、ポインタを安全な場所に保存する必要があります。このポインタがこのメモリにアクセスする唯一の方法であり、その中のアドレスが失われると、アクセスも解放もできなくなるのです。要求されたメモリは必ず解放しなければならないので、自分の責任で管理するのが一つのルールです。よく設計されたプログラムは、この点を考慮しなければなりません。

所有者を理解することが重要です。メモリの所有者は、そのメモリを解放する責任を負わなければなりません。この作業は代理人にやってもらうこともできますが、正しく行われるようにしなければなりません...。

よくある間違いは、割り当てられたメモリブロックへのポインタを使用し、そのポインタを別のメモリブロックに向けることです。最初のメモリブロックへのポインタが別のメモリブロックを指しているため、元のメモリブロックが失われてしまいます。最初に要求されたメモリブロックを取り出す方法はありません。メモリブロックは次のようになります。 孤児 . もはやアクセスして再び処理することは不可能です。これは、次のようにも呼ばれます。 メモリリーク .

これはボーランドのニュースグループから引用したサンプルコードです。

ヴァル

  bitdata: Byteの配列。

  pbBitmap: ポインタ。

開始

  SetLength(bitdata, nBufSize)を設定します。

  GetMem(pbBitmap,nBufSize)を実行します。

  pbBitmap := Addr(bitdata);

  VbMediaGetCurrentFrame(VBDev, @bmpinfo.bmiHeader, @pbBitmap, nBufSize)を使用します。

実は、このコードにはいくつかのコンフリクトがあります。 長さの設定 ビットデータ バイトを割り当てる。なぜかプログラマは GetMem に対して pbBitmap は同じバイト数を確保します。そして、その後に pbBitmap が別のメモリアドレスに割り当てた結果、同じバイト数の GetMem のメモリにアクセスできない。( pbBitmap がアクセスする唯一の方法でしたが、もはやそれを指していません)。つまり、メモリリークが発生しているのです。

実は、他にもいくつかエラーがあります。 ビットデータ は動的な配列であり ビットデータ はポインタのアドレスだけであり、バッファの最初のバイトのアドレスではありません(詳しくは後述の動的配列を参照)。また pbBitmap もポインタなので、@演算子を使って関数呼び出しの引数を渡すのは誤りです。

正しいコードは以下の通りです。

ヴァル

  bitdata: Byteの配列。

  pbBitmap: ポインタ。

開始

  if nBufSize > 0 then

  開始

    SetLength(bitdata, nBufSize)を設定します。

    pbBitmap := Addr(bitdata[0])。

    VbMediaGetCurrentFrame(VBDev, @bmpinfo.bmiHeader, pbBitmap, nBufSize)を使用します。

  を終了します。

または:

ヴァル

  bitdata: Byteの配列。

開始

  if nBufSize > 0 then

  開始

    SetLength(bitdata, nBufSize)を設定します。

    VbMediaGetCurrentFrame(VBDev, @bmpinfo.bmiHeader, @bitdata[0], nBufSize)を使用します。

  を終了します。

深刻な問題のように見えますが、複雑なコードで簡単に発生する可能性があります。

ポインタはそれ自身のメモリブロックを実行する必要はないことに注意してください。ポインターはしばしば、配列のトラバース(以下略)や、構造体のメンバの操作に使われます。そのためにメモリが割り当てられていなければ、メモリブロックを制御する責任はない。使い切ると失効する一時的な変数と考えてください。

ポインタ演算と配列

ソフトウェア品質とポインタ演算のどちらを取るかですが、両方を同時に取ることはできません。 - バートランド・メイヤー

Delphiでは、ポインタに対していくつかの簡単な操作ができます。もちろん、ポインタに代入したり、等価(P1 = P2 ならば)か不等価かを比較したりすることができますが、ポインタをインクリメントしたりデクリメントしたりすることも可能で、それには Inc 12月 . きちんとしたのは、これらの増分と減分が ポインタの基底型の大きさによってスケーリングされます。 . 例(ポインターを偽のアドレスに設定したことに注意。これで何もアクセスしない限り、何も悪いことは起こりません)。

プログラム PointerArithmetic。

{apptype コンソール}。

用途

  SysUtilsです。

procedureWritePointer(P: PDouble);

開始

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

を終了します。

ヴァル

  P:PDoubleです。

開始

  P := Pointer($50000);

  WritePointer(P)を実行します。

  Inc(P)です。    // 00050008 = 00050000 + 1*SizeOf(ダブル)

  WritePointer(P)です。

  Inc(P, 6)です。 // 00050038 = 00050000 + 7*Sizeof(Double)

  WritePointer(P)です。

  Dec(P, 4)です。 // 00050018 = 00050000 + 3*Sizeof(Double)です。

  WritePointer(P)です。

  Readln;

を終了します。

と出力されます。

00050000

00050008

00050038

00050018

このような型の配列にシーケンシャルにアクセスできるようにすることが目的です。(1次元)配列は同じ型の連続した項目を含むので、つまり、ある要素がアドレス N であり、次の要素はアドレス N+SizeOf(element) -ループの次の繰り返しでは、ポインタをインクリメントして配列の次の要素にアクセスし、その繰り返しです。

プログラムIterateArrayです。

{apptype コンソール}。

ヴァル

  分数:Doubleの配列[1..8]。

  I: 整数。

  PD: ^Double

開始

// 配列をランダムな値で埋めます。

  ランダム化する。

  for I := Low(Fractions)からHigh(Fractions) do

    Fractions[I] := 100.0 * Random;

// ポインタを使ってアクセスする。

  PD := @Fractions[Low(Fractions)]です。 

  for I := Low(Fractions)からHigh(Fractions)まで do

  開始

    Write(PD^:9:5)を実行します。

    Inc(PD)です。 // 次の項目を指す

  を終了します。

  書き込む。

// インデックスを利用した従来のアクセス。

  for I := Low(Fractions)からHigh(Fractions)まで do

    Write(Fractions[I]:9:5)を実行します。

  書き込む。

  読み取る。

を終了します。

ポインタのインクリメントは、少なくとも古いプロセッサでは、インデックスに基底型のサイズを掛けて、それを反復毎に配列の基底アドレスに加えるよりも、おそらくわずかに高速です。

実際には、この方法で行う効果は、期待するほど大きくはありません。まず、最近のプロセッサは、よくあるケースはインデックスを使って特別な方法で対処しているので、ポインタも更新する必要はありません。そして、上記において、少し最適化されたアクセスを使うことによって見出される利得は、Write()処理を行うためにかかる時間によって大きく覆い隠されてしまいます。そして、上記において、少し最適化されたアクセスを使用することによって見出された利得は、Write()処理を実行するためにかかる時間によって、大きく覆い隠されるのである。

上のプログラムを見てもわかるように、ループの中でポインタをインクリメントすることを忘れがちです。 フォー・トゥー・ドゥー あるいは、ループを終了させるために別の方法やカウンターを使用することもできます(その場合は、手動でデクリメントして比較しなければなりません)。つまり、ポインタを使ったコードは一般に保守が大変なのです。非常にタイトなループを除いては、ポインタの方が速いということはないので、私はDelphiでこの種のアクセスを使用することに非常に慎重です。

配列へのポインタ

しかし、時にはメモリにアクセスするためのポインタしか持っていないことがあります。Windows API の関数はしばしばデータをバッファとして返し、そのバッファにはあるサイズの配列が格納されます。その場合でも、IncやDecを使うより、バッファを配列へのポインタにキャストする方が簡単でしょう。 例を挙げます。 例を挙げます。

タイプ

  PIntegerArray = ^TIntegerArray;

  TIntegerArray = Integerの配列[0..65535]です。

ヴァル

  Buffer: Integerの配列。

  PInt: PInteger。

  PArr: PIntegerArray;

  ...

// ポインタ演算を使用する。

  PInt := @Buffer[0] です。

  for I := 0 to Count - 1 do

  開始

    Writeln(PInt^)です。

    Inc(PInt)です。

  を終了します。

// 配列ポインタとインデックスを使用する。

  PArr := PIntegerArray(@Buffer[0]) です。

  for I := 0 to Count - 1 do

    Writeln(PArr^[I]);

  ...

を終了します。

デルファイ 2009

Delphi 2009 では、ポインタ演算は PChar 型(および PAnsiChar PWideChar ) が、他のポインタ型でも可能になりました。いつ、どこで、これが可能になるかは、新しい POINTERMATH コンパイラディレクティブを使用します。

ポインタ演算は通常オフになっていますが、{$POINTERMATH ON}でコードの一部をオンにし、{$POINTERMATH OFF}で再びオフにすることが可能です。ポインタ演算(ポインタ数学)をオンにしてコンパイルされたポインタ型では、一般にポインタ演算が可能です。

現在のところ PChar , PAnsiChar PWideChar であり、ポインタ演算がデフォルトで有効になっている他の型は PByte しかし、それをオンにするのは、例えば PInteger を使えば、上のコードはかなり簡略化されます。

{$pointermath on}

ヴァル

  Buffer: Integerの配列。

  PInt: PInteger。

  ...

// 新しいポインタ演算を使用する。

  PInt := @Buffer[0] です。

  for I := 0 to Count - 1 do

    Writeln(PInt[I])を実行します。

  ...

を終了します。

{ポインターマス オフ}。

そのため、特別な TIntegerArray PIntegerArray また、PInt[I]の代わりに、(PInt + I)^構文を用いても、同じ結果になります。

どうやら デルファイ2009 へのポインタでは、新しいポインタ演算は意図したとおりに動作しません。 ジェネリック パラメトリック型がどのような型としてインスタンス化されたとしても、インデックスが サイズオブ(T) は、期待通りです。

参考文献

Delphiの多くの型は、実際にはポインタですが、そうでないように見せかけています。私は、これらの型を リファレンス . 例えば、動的配列、文字列、オブジェクト、インターフェースなどです。これらの型は、裏ではすべてポインタですが、いくつかの特別なセマンティクスと、しばしばいくつかの隠されたコンテンツを持っています。

ダイナミックアレイ

多次元ダイナミックアレイ

文字列

オブジェクト

インターフェイス

参照パラメータ

未定義パラメータ

リファレンスとポインタを区別するものは。

リファレンスはイミュータブルです。 参照をインクリメントしたりデクリメントしたりすることはできません。例えば上の例では、配列へのポインタのように、参照は特定の構造体を指しますが、その中に入ることはできません。

参照にポインタ構文が使用されていません。 これは、それらが そのため、このことを知らない多くの人は、ポインタを理解するのが難しく、その結果、ポインタを使わない方がよいことを行ってしまうのです。

このような参照をC++の 参照型 . これらは多くの点で異なっています。

ダイナミックアレイ

Delphi 4以前は、動的配列は言語の機能ではありませんでしたが、そのコンセプトは存在していました。動的配列は、実行時に割り当てられ、ポインタによって管理されるメモリブロックです。動的配列は成長または圧縮することができます。つまり、指定したサイズのメモリブロックを再割り当てし、元のブロックの内容を新しいブロックにコピーする必要があり、元のブロックは解放され、ポインタは新しいブロックを指します。

Delphiの動的配列(例えば 整数の配列 ) Delphiの型は同じです。しかし、メモリの読み出しと割り当てを管理するのは、実行時に添付されるコードです。次のメモリ記憶装置ポインタは、割り当てられた要素数と参照数の2つのフィールドを追加したアドレスを指しています。

上記のように、Nを動的配列変数のアドレスとすると、参照回数( 参照回数 )はN-8であり、割り当てられた要素数( 長さインジケータ )はN-4です。最初の要素のアドレスはNである。

参照が追加されるたびに(代入、パラメータ渡しなど)、参照カウントは1ずつ増え、参照が削除されるたびに(変数がスコープを出るとき、動的配列のメンバーを含むオブジェクトが解放されるとき、動的配列を指す変数が別の動的配列またはnilを指すときなど)、参照カウントは1ずつ減少する。

低レベルルーチンの使用 移動 または 塗りつぶし文字 などのルーチンがあります。 TStream.Write(ストリーム・ライト 動的配列へのアクセスは、しばしば間違っています。通常の配列(動的配列と区別するために静的配列と呼ばれます)では、変数はメモリブロックと同等です。動的配列の場合はそうではありません。したがって、ルーチンが配列の要素をメモリ・ブロックとしてアクセスしたい場合、動的配列の変数を参照することはできず、動的配列の最初の要素を使用する必要があります。

ヴァル

  項目:整数の配列。

  ... 

// 誤:Item 変数のアドレスが渡される

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

  ...

// 正しい:最初の要素のアドレスが渡される

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

上のコードに注目してください。 ストリーム.ライト は、タイピングされていない ヴァー パラメータがあり、これも参照渡しとなります。これについては後述します。

多次元ダイナミックアレイ

以上、1次元の動的配列について説明しました。動的配列は多次元にすることもできます。しかし、それは構文上のことで、実際はそうではありません。多次元の動的配列は、実際には別の一次元配列を指す一次元配列です。

このような宣言があったとします。

タイプ

  TMultiIntegerArray = Integerの配列の配列です。

ヴァル

  MyIntegers TMultiIntegerArray;

これで、多次元配列が宣言され、MyIntegers[0, 3]によって要素にアクセスできるようになったように見えます。しかし、宣言された型は(構文レベルでは)このように読むべきです。

タイプ

  TMultiIntegerArray = (Integerの配列)の配列です。

あるいは、より明示的に以下のように記述します。

タイプ

  TSingleIntegerArray = Integerの配列。

  TMultiIntegerArray = TSingleIntegerArrayの配列。

可視である。 TMultiIntegerArray への参照は、実際には のTSingleIntegerArrayです。 一次元の配列です。このように TMultiIntegerArray は、行と列に並んだ連続したメモリブロックではなく、実際には長さが不定の配列です。例えば、各要素は別の配列を指し、それぞれの部分配列は異なるサイズを持っています。したがって

SetLength(MyIntegers, 10, 20)とします。

(10個の TSingleIntegerArrays 各サブアレイは20個の整数を持ち、表向きは長方形の配列です)、各サブアレイにアクセスし、変更することができます。

SetLength(MyIntegers, 10)を設定します。

SetLength(MyIntegers[0], 40)を設定します。

SetLength(MyIntegers[1], 31)を設定します。

// etc...

文字列

配列のインデックスは0と1のどちらから始めるべきですか?私の妥協案である0.5は、適切な検討もされずに却下されました。 - スタン・ケリー・ブートル

文字列は、多くの点でダイナミックアレイと同じです。また、同じ内部構造を持ち、参照カウントと、そこに格納されている文字列データの長さを示す(同じオフセットで)。

違いは、構文とセマンティクスにあります。文字列を ゼロ を設定することができます。 '' (空文字列)にするとクリアされます。文字列は定数にもなる(参照カウントは-1であり、実行時ルーチンは特別な値としてこれをインクリメントまたはデクリメントするか、文字列を解放する)。動的配列の最初の要素のインデックスは1であるが、動的配列の最初の要素のイン デックスは0である。

文字列の詳細については PCharsと文字列に関する記事 .

オブジェクト

オブジェクト、より具体的にはクラスのインスタンスであり、そのライフサイクルはコンパイラによって管理されていない。その内部構造は非常にシンプルである。クラスの各インスタンスは、オフセット0にVMTテーブルへのポインタを持つ(参照される各ポインタの実行時からの相対的なアドレスである)。これには、そのクラスの各仮想メソッドへのポインタが含まれています。このテーブルの負のオフセットには、クラスに関する追加情報が含まれています。これについては、あまり多くを語らないことにします。VMTテーブルはクラスごとに1つしかありません(オブジェクトごとではありません)。

インターフェースを実装しているクラスは、そのインターフェースで実装されているメソッドを含むテーブルへのポインタも持っており、実装されているインターフェースごとに1つずつ持っています。このテーブルはまた、負のオフセットにいくつかの追加情報を持っています。これらのオフセットにあるオブジェクトポインタは、ベースクラスに関連するドメインに関する情報を指しています。コンパイラはその詳細を知っています。

VMTポインタとインタフェース・テーブル・ポインタに続くのは、構造体に似たオブジェクトのドメインです。

オブジェクトのRTTIデータやその他のクラス情報は、VMTポインタが指すVMTテーブルなど、オブジェクトへのこれらの参照から取得されます。コンパイラは残りのデータを取得する方法を知っており、通常は他の構造体へのポインタや循環参照を含む複雑な構造体を介して取得します。

次の例では、次のような文章を想定しています。

タイプ

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

// その他のフィールドとメソッドの宣言

    procedure Notify(Aspect: TAspect); override;

    procedure Clear; override;

    手続きEditを行います。

    procedure ClearLine(Line: Integer);

    関数 Update(Region: Integer): Boolean; virtual;

// etc...

  を終了します。

  ヴァル

    ホワットシット TWhatsitです。

  始める

    Whatsit := TWhatsit.Create;

オブジェクトのレイアウトは以下の通りです。

インターフェース

インターフェイスは、実際にはメソッドの集合体です。内部的には、コードへのポインタの配列へのポインタです。例えば、こんな宣言があったとしよう。

タイプ

  IEditable = インターフェース

    手続き Edit;

    procedure ClearLine(Line: Integer);

    関数 Update(Region: Integer): ブール値

  を終了します。

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

  パブリック

    procedure Notify(Aspect: TAspect); override;

    procedure Clear; override;

    手続きEditを行います。

    procedure ClearLine(Line: Integer);

    関数 Update(Region: Integer): Boolean; virtual;

// etc...

  を終了します。

ヴァル

  MyEditableです。IEditable。

開始

  MyEditable := TWhatsit.Create;

インターフェース、実装オブジェクト、実装クラス、メソッドの関係は以下の通りです。

MyEditable が作成した要素のリストを指し示します。 TMyClassです。 オブジェクトを IEditable ポインタを使用します。なお MyEditable はオブジェクトの開始アドレスを指すのではなく、オフセットを持っています。 MyEditable オブジェクト内のポインタのリストへのポインタ。インターフェイス内の各メソッド実装を含む。コードでは、このポインタを調整するために 自己 ポインタ (これは実際には マイエディタブル はオブジェクトの開始アドレスを指しています(渡されたポインタから IEditable をオブジェクトの中に入れてから、実際のメソッドを呼び出す)。これは、そのクラスが実装しているインターフェイスのメソッドのスタブである。

例えば、インスタンスのアドレスが50000であったとします。 TWhatsit の実装は IEditable インターフェースでは、インスタンス内のポインタのオフセットは 16 です。 MyEditable は50016を指しています。で50016 IEditable このポインタは、クラスで実装されているインターフェイスメソッドの表(例えば30000番台)を指し、さらにそのメソッドのスタブ(例えば60000番台)を指します。スタブは、Selfから渡された値が50016にあることを知り、16を引いて50000を得ますが、これはちょうどそのインターフェースを実装しているオブジェクトのアドレスにあたります。そして、スタブは、50000をSelfのアドレスとして、本当の関数を呼び出す。

上の画像はQueryInterface、_AddRef、_Releaseのスタブですが、簡単のために無視しています。

ペンシルボックスの紙が必要な理由がわかりましたか?   ;-)

パラメータを参照する

引用パラメータは、しばしば ヴァル パラメータがありますが アウト パラメータは参照パラメータでもあります。

参照パラメータは、渡されたときに実際にルーチンに実値を渡すわけではなく、パラメータのアドレスを渡します。例えば

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

開始

  Int := Int or (1 shl Bit);

を終了します。

は、多かれ少なかれ、次のコードと同等です。

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

開始

  Int^ := Int^ または (1 shl Bit);

を終了します。

相違点

       ポインタ構文を使用しない。パラメータ名は自動的にデリファレンスされます。つまり、ターゲット変数はポインタではなくパラメータ名で操作されます。

       参照パラメータを変更することはできません。パラメータ名は、対象の変数を実行するために使用し、別のアドレスを指定したり、インクリメントやデクリメントを行うためではありません。

       変数はアドレスで渡す必要があります。例えば、実際のメモリーセルは何らかの変換が必要です。ですから、整数の参照引数の場合、17、98765、Abs(MyInteger)を渡すことはできません。変数でなければなりません(配列の要素、オブジェクトや構造体のメンバなどを含む)。

       実パラメータは,宣言されたパラメータと同じ型でなければなりません。 TObject としてパラメータを渡すことはできません。 TEdit . この問題を回避するためには,型付けされていない参照パラメータを宣言する必要があります ( 非定型参照パラメータ ).

参照パラメータは、ポインタパラメータよりも構文が単純です。しかし、注意しなければならない微妙なルールがいくつかあります。ポインターを渡すと、間接参照のレベルが追加されます。言い換えれば、ポインタ P を整数に変換し、引数に渡す必要があります。 P^ :

ヴァル

  Int: 整数です。

  Ptr: PInteger;

  Arr: 整数の配列。

開始

// Int, Ptr, Arrの初期化は表示されない....

  SetBit(Ptr^, 3)です。    // Ptrが渡される

  SetBit(Arr[2], 11)です。 // @Arr[2]が渡される

  SetBit(Int,7)です。     // @Int が渡される

型パラメータがない

型付けされていないパラメータは参照パラメータでもありますが、以下のようなものがあります。 ヴァル , コンスト または アウト . 任意の型の引数を渡すことができるため、任意のサイズと型の引数を受け入れることができるルーチンを書くことが簡単になりますが、これには引数の型に関する情報を渡すメカニズム、または型に依存しないルーチンとしてのメカニズムも必要です。パラメータにアクセスする際には、型変換を行う必要があります。

内部的に型付けされていない引数は、ポインタとして渡されることもある。以下の2つの例では、最初の汎用ルーチンは任意のサイズのバッファを埋めることができ、Buffer引数の型は重要ではありません。

// 型が重要でないルーチンの例

procedure FillBytes(var Buffer; Count: 整数。

  値: Byteの配列)。

ヴァル

  P:PByteです。

  I: 整数。

  LenValues 整数。

開始

  LenValues := Length(Values)です。

  if LenValues > 0 then

  開始

    P := @Buffer; // バッファをbyteの配列として扱う。

    I := 0;

    while Count > 0 do

    開始

      P^ := Values[I];

      I := (I + 1) mod LenValues;

      Inc(P)です。

      Dec(Count)です。

    を終了します。

  を終了します。

を終了します。

2番目のTTypedListのサブクラスであるTIntegerListのメソッドです。

function TIntegerList.Add(const Value): 整数。

始める

  Grow(1)です。

  結果 := Count - 1;

// FInternalArray: 整数の配列。

  FInternalArray[Result]:=Integer(Value)。

を終了します。

ご覧のように、ポインタを使用するには、たとえパラメータがすでに目的のポインタであっても、実際の参照を渡さなければなりません。また、間接参照のレベルも上がります。

参照先にアクセスするには、通常の参照パラメータとして使用するだけでよいが、型変換が必要で、コンパイラがポインタの非参照方法を指定する。

間接参照レベルに注意してください。FillBytes 関数を使って動的配列を初期化する必要がある場合、変数を渡すことはできず、配列の最初の要素を渡します。実は、静的配列の先頭アドレスも渡すことができます。したがって、配列を型参照なしで実引数として渡したい場合は、意図的に間違った動的配列にアクセスしたい場合を除き、配列そのものではなく、最初の要素を最後に渡してください。

データ構造

ポインターは、リンクリスト、ツリー、レイヤーなどのデータ構造で広く使われています。ここでは、これらについては触れません。多くの言語(Java)は公式にポインターを使用しないと言っていますが、高レベルの構造はポインターと参照なしでは実装できないことに注意することが重要です。構造体の詳細については、このトピックに関するドキュメントを読んでください。

ポインターのリンクリストに大きく依存するデータ構造の簡単な図を示します。

このような構造体を使用する場合、通常はクラス内部にカプセル化されており、ポインターを使用することでクラス内部での実装の難易度は下がりますが、ポインタ