CおよびC ++のポインター、参照、および配列:i上のポイント

この投稿では、C、C ++のポインター、リンク、配列などの微妙な概念を最終的に理解しようとします。 特に、C配列がポインターかどうかという質問に答えます。



規約と仮定







ポインターとリンク



ポインター 。 ポインタとは何ですか、話をしません。 :)これを知っていると仮定します。 次のことだけを思い出させてください(すべてのコード例は、たとえばmainなどの関数内にあると想定されています)。



 int x; int *y = &x; //            "&".     int z = *y; //        "*".     ,    
      
      







また、私は次のことを思い出します:charは常に正確に1バイトであり、すべてのCおよびC ++標準でsizeof (char) == 1



(ただし同時に、標準ではバイトに正確に8ビットが含まれることを保証しません:))。 さらに、あるタイプTのポインターに数値を追加すると、このポインターの実際の数値は、この数値× sizeof (T)



だけ増加します。 つまり、pの型がT *TYPE



場合、 p + 3



(T *)((char *)p + 3 * sizeof (T))



p + 3



同等です。 同様の考慮事項が減算に適用されます。



参照資料 次にリンクについて。 リンクはポインターと同じですが、構文やその他の重要な違いが異なります。これについては後で説明します。 次のコードは、ポインタの代わりにリンクが含まれていることを除いて、前のコードと違いはありません。

 int x; int &y = x; int z = y;
      
      







割り当て記号の左側にリンクがある場合、リンク自体を割り当てるか、リンクが参照するオブジェクトを割り当てるかを理解する方法はありません。 したがって、このような割り当ては、参照ではなく常にオブジェクトに割り当てられます。 しかし、これはリンクの初期化には適用されません。リンク自体はもちろん初期化されます。 したがって、リンクが初期化された後は、リンク自体を変更する方法はありません。つまり、リンクは常に一定です(ただし、オブジェクトは一定ではありません)。



左辺値 割り当てることができるこれらの式は、C、C ++、および他の多くの言語で左辺値と呼ばれます(これは「左の値」、つまり等号の左側の略語です)。 残りの式は右辺値と呼ばれます。 変数名は明らかに左辺値ですが、それだけではありません。 式a[i + 2]



some_struct.some_field



*ptr



*(ptr + 3)



も左辺値です。



驚くべき事実は、参照と左辺値が、ある意味で同じものであるということです。 推測しましょう。 左辺値とは何ですか? これは流用できるものです。 つまり、これは何かを置くことができるメモリ内の特定の固定された場所です。 つまり、住所。 つまり、ポインターまたはリンク(既に知っているように、ポインターとリンクは、アドレスの概念を表現するC ++の2つの構文的に異なる方法です)。 さらに、リンクは等号の左側に配置でき、リンクが指すオブジェクトへの割り当てを意味するため、ポインターではなくリンク。 したがって、左辺値は参照です。



リンクとは何ですか? これはアドレスの構文の1つです。これもまた、あなたがそれを置くことができる場所です。 そして、リンクは等号の左側に置くことができます。 したがって、リンクは左辺値です。



わかりましたが、(ほとんどすべての)変数を等号の左側に置くこともできます。 (そのような)変数はリンクですか? ほぼ。 変数である式は参照です。



つまり、 int x



を宣言したとしましょう。 現在、xはint TYPE



型の変数であり、他の変数はありません。 これはintであり、それだけです。 しかし、今x + 2



またはx = 3



と書くと、これらの式では、部分式x



int &TYPE



型になります。 それ以外の場合、このxは、たとえば10と変わらず、(トップ10のように)何も割り当てられないためです。



この原則(「変数である式は参照」)が私の発明です。 つまり、教科書や規格などでこの原則を見たことはありません。 それにもかかわらず、それは非常に単純化され、便利であると考えられています。 コンパイラを実装する場合、式内の変数を参照と見なすだけであり、これは実際のコンパイラで想定されているとおりです。



さらに、Cにも左辺値の特別なデータ型(つまり、リンク)が存在すると想定すると便利です。これは、これからも想定し続けることです。 リンクの概念だけをCで構文的に表現することはできません。リンクを宣言することはできません。



「任意の左辺値は参照」という原則も私の発明です。 しかし、「すべてのリンクは左辺値です」という原則は、完全に合法であり、広く認識されている原則です(もちろん、リンクは可変オブジェクトへの参照でなければならず、このオブジェクトは割り当てを許可する必要があります)。



ここで、契約を考慮して、リンクを操作するためのルールを厳密に定式化します。たとえば、 int x



宣言されている場合、式xはint &TYPE



型になります。 この式(またはリンクタイプの他の式)が等号の左側にある場合、それは参照として使用され、他のほとんどすべての場合(たとえば、状況x + 2



)xは自動的にint TYPE



変換されます(別の操作によって) 、次にリンクがオブジェクトに変換されないのは&で、これについては後で説明します)。 等号の左側はリンクのみです。 (非定数)リンクを初期化できるのはリンクのみです。



*および操作 。 契約により、*および&オペレーションを再確認できます。 これで次のことが明らかになります:操作*はポインターにのみ適用でき(特に、これは常に知られています)、同じ型への参照を返します。 &は常にリンクに適用され、同じタイプのポインターを返します。 したがって、*と&は、ポインターとリンクを相互に変換します。 つまり、実際には、何も行わず、ある構文の本質を別の構文の本質に置き換えるだけです! したがって、一般的に言えば、アドレスを取得する操作を呼び出すことは完全に正しいわけではありません。既存のアドレスにのみ適用でき、このアドレスの構文の実施形態を変更するだけです。



ポインターと参照は、 int *x



およびint &x



として宣言されていることに注意してください。 したがって、「宣言が使用を示唆している」という原則が再び確認されました。ポインターの宣言は、リンクをリンクに変換する方法を思い出し、リンクの宣言はその逆です。



また、 &*EXPR



(ここではEXPRは必ずしも1つの識別子ではない任意の式です)は意味をなす場合(つまり、常にEXPRがポインターである場合)は*&EXPR



同等であり、 *&EXPR



同じ場合も同等であることに注意してください意味(つまり、EXPRがリンクの場合)。



配列



したがって、そのようなデータ型-配列があります。 配列は、たとえば次のように定義されます。

 int x[5];
      
      





角括弧内の式は、C89およびC ++ 98のコンパイル時定数でなければなりません。 この場合、角括弧内の数字を配置する必要があります;空の角括弧は許可されません。



すべてのローカル変数(すべてのコード例は関数内にあると仮定します)がスタック上にあるように、配列もスタック上にあります。 つまり、上記のコードにより、サイズ5 * sizeof (int)



巨大なメモリブロックのスタックに直接割り当てられ、そこに配列全体が配置されます。 このコードは、ヒープ上の遠く離れた場所にあるメモリを指すポインターによって宣言されていると考える必要はありません。 いいえ、実際の配列を宣言しました。 スタック上。



sizeof (x)



は何に等しくなりますか? もちろん、配列のサイズ、つまり5 * sizeof (int)



等しくなります。 書いたら

 struct foo { int a[5]; int b; };
      
      





その後、再び、配列の場所は構造内で完全に割り当てられ、この構造のsizeofがこれを確認します。



配列からアドレス( &x



)を取得できます。これは、この配列が配置されている場所への実際のポインターになります。 式&x



の型は、理解しやすいように、 int (*TYPE)[5]



ます。 配列の先頭にゼロ要素が配置されるため、配列自体のアドレスとゼロ要素のアドレスは数値的に同じです。 つまり、 &x



&(x[0])



数値的に等しい(ここでは式&(x[0])



を有名に書いたが、実際はそれほど単純ではないので、これに戻る)。 しかし、これらの式は異なるタイプint (*TYPE)[5]



int *TYPE



持っているため、==を使用して比較することはできません。 ただし、 void *



使用してトリックを適用できます。次の式は真になります: (void *)&x == (void *)&(x[0])







さて、配列は単なる配列であり、他のものではないと確信していると仮定しましょう。 ポインターと配列の間のこのような混乱はどこから来たのでしょうか? 実際、ほとんどすべての操作中に、配列名はそのゼロ要素へのポインタに変換されます。



そのため、 int x[5]



を宣言しました。 ここでx + 0



記述すると、これはx(タイプint TYPE[5]



、より正確にはint (&TYPE)[5]



)を&(x[0])



に変換します。配列xのゼロ要素へのポインター。 現在、xはint *TYPE



型です。



配列名をvoid *



変換するか、==を適用すると、この名前が最初の要素へのポインターに予備変換されます。したがって、

 &x == x //  ,  : int (*TYPE)[5]  int *TYPE (void *)&x == (void *)x //  x == x + 0 //  x == &(x[0]) // 
      
      







操作[] 。 表記a[b]



常に*(a + b)



と同等です*(a + b)



operator[]



および他の操作の再定義を考慮していないことを思い出します)。 したがって、表記x[2]



は次を意味します。





関係する式のタイプは次のとおりです。

 x // int (&TYPE)[5],   : int *TYPE x + 2 // int *TYPE *(x + 2) // int &TYPE x[2] // int &TYPE
      
      







また、角括弧の左側には、正確に配列である必要はなく、任意のポインタを置くことができます。 たとえば、 (x + 2)[3]



と書くことができ、これはx[5]



と同等になります。 また、 *a



a[0]



、aが配列の場合とaがポインターの場合の両方で常に同等であることに注意してください。



さて、約束したとおり、 &(x[0])



戻ります。 この式では、最初にxがポインターに変換され、次に[0]



が上記のアルゴリズムに従ってこのポインターに適用され、結果としてint &TYPE



型の値int &TYPE



取得され、最後に&を使用してint *TYPE



型に変換されることが明らかです したがって、この複雑な式を使用して(配列からポインターへの変換が既に実行されている)配列をポインターに変換するという少し単純な概念を説明するには、ちょっとしたトリックでした。



そして今、埋め戻しの質問&x + 1



何ですか? さて、 &x



は配列全体へのポインタであり、 + 1



は配列全体へのステップにつながります。 つまり、 &x + 1



(int (*)[5])((char *)&x + sizeof (int [5]))



、つまり(int (*)[5])((char *)&x + 5 * sizeof (int))



(ここでint (*)[5]



int (*TYPE)[5]



)。 したがって、 &x + 1



数値的にx + 5



等しく、 x + 5



ではなく、あなたが思うかもしれません。 はい、結果として、配列の外側(最後の要素の直後)にあるメモリを指しますが、誰が気にしますか? 結局のところ、Cでは、配列が範囲外であるかどうかはまだチェックされていません。 また、式*(&x + 1) == x + 5



真であることに注意してください。 次のように書くこともできます: (&x)[1] == x + 5



。 また、true *((&x)[1]) == x[5]



、または(&x)[1][0] == x[5]



(もちろん、セグメンテーションフォールトをキャッチしない限り)私たちの記憶を超えてアピールしようとしたため:))。



関数に引数として配列を渡すことはできません 。 関数ヘッダーにint x[2]



またはint x[]



を記述した場合、これはint *x



と同等になり、ポインターは常に関数に渡されます(渡される変数のサイズはポインターのサイズと同じになります)。 この場合、ヘッダーで指定された配列のサイズは無視されます。 ヘッダーでint x[2]



を簡単に指定し、そこに長さ3の配列を渡すことができます。



ただし、C ++では、配列参照を関数に渡す方法があります。

 void f (int (&x)[5]) { // sizeof (x)   5 * sizeof (int) } int main (void) { int x[5]; f (x); // OK f (x + 0); //  int y[7]; f (y); // ,    }
      
      





この転送では、配列ではなくリンクのみを渡します。つまり、配列はコピーされません。 ただし、通常のポインターの受け渡しと比較すると、いくつかの違いがあります。 配列参照が渡されます。 代わりに、ポインターを渡すことはできません。 指定されたサイズの配列を正確に転送する必要があります。 関数内では、配列参照は配列参照とまったく同じように動作します。たとえば、sizeofは配列のようになります。



そして最も興味深いことに、このプログラムは次のように使用できます。

 //    template <typename t, size_t n> size_t len (t (&a)[n]) { return n; }
      
      





配列用のC ++ 11のstd :: end関数も同様に実装されています。



「配列へのポインタ 厳密に言えば、「配列へのポインター」は配列へのポインターであり、それ以外のものではありません。 言い換えれば:

 int (*a)[2]; //    .  .    int (*TYPE)[2] int b[2]; int *c = b; //     .   .       int *d = new int[4]; //      .  
      
      





ただし、「配列へのポインター」というフレーズは、このポインターの種類が適切ではない場合でも、配列が配置されているメモリ領域へのポインターを非公式に意味する場合があります。 このような非公式の理解によれば、cとd(およびb + 0



)は配列へのポインターです。



多次元配列int x[5][7]



宣言されている場合、xは、どこか遠くを指すポインターの長さ5の配列ではありません。 いいえ、xはスタック上に配置された単一の5 x 7モノリシックブロックです。 sizeof (x)



5 * 7 * sizeof (int)



です。 要素は次のようにメモリ内に配置されます: x[0][0]



x[0][1]



x[0][2]



x[0][3]



x[0][4]



x[0][5]



x[0][6]



x[1][0]



など。 x[0][0]



を書くと、イベントは次のように発展します。

 x // int (&TYPE)[5][7],  : int (*TYPE)[7] x[0] // int (&TYPE)[7],  : int *TYPE x[0][0] // int &TYPE
      
      





**x



についても同じことが**x



ます。 式、たとえば、実際にはx[0][0] + 3



および**x + 3



では、 int &TYPE



最終リンクを変換するときに、メモリが1回だけ抽出されます(2つのアスタリスクが存在するにもかかわらず) int TYPE



つまり、式**x + 3



から生成されるアセンブラコードを見ると、メモリからデータを抽出する操作がそこで1回だけ実行されることがわかります。 **x + 3



は、 *(int *)x + 3



として異なる方法で記述できます。



次に、この状況を見てみましょう。

 int **y = new int *[5]; for (int i = 0; i != 5; ++i) { y[i] = new int[7]; }
      
      







今は何ですか? yは、配列へのポインターの(非公式な意味での)配列へのポインターです(非公式な意味で)。 ここには単一の5 x 7ブロックは表示されません。サイズ7 * sizeof (int)



5つのブロックがあり、互いに遠くなる可能性があります。 y[0][0]



とは何ですか?

 y // int **&TYPE y[0] // int *&TYPE y[0][0] // int &TYPE
      
      





ここで、 y[0][0] + 3



と書くと、メモリからの抽出が2回発生します。配列yからの抽出と、配列y[0]



から遠くなる可能性のある配列y[0]



からの抽出です。 これは、多次元配列xを使用した例とは異なり、配列名からその最初の要素へのポインターへの変換がないためです。 したがって、 **y + 3



は、 *(int *)y + 3



と同等ではありません。



もう一度説明します。 x[2][3]



*(*(x + 2) + 3)



x[2][3]



同等です。 y[2][3]



*(*(y + 2) + 3)



y[2][3]



同等です。 しかし、最初のケースでは、タスクは単一の5 x 7ブロックで「2番目の行の3番目の要素」を見つけることです(もちろん、要素には最初から番号が付けられるため、この3番目の要素はある意味で4番目になります:))。 コンパイラは、実際に目的のアイテムがこのブロックの2 * 7 + 3



3番目にあると計算し、それを抽出します。 つまり、ここでx[2][3]



((int *)x)[2 * 7 + 3]



と同等であり、同じものは*((int *)x + 2 * 7 + 3)



です。 2番目の場合、最初にy配列の2番目の要素を抽出し、次に結果の配列の3番目の要素を抽出します。



最初のケースでは、 x + 2



を実行すると、すぐに2 * sizeof (int [7])



、つまり2 * 7 * sizeof (int)



ます。 2番目の場合、 y + 2



2 * sizeof (int *)



シフトです。



最初の場合(void *)x



および(void *)*x



(および(void *)&x



!)は同じポインターですが、2番目の場合はそうではありません。



All Articles