メモリとポインタの動きの理解(主にCの文法)

ポインタの基礎をさらっと説明し、
最後に、C言語でポインタの必要性を意識させられるような例を挙げます。

ポインタとは

ポインタとは、メモリに割り当てられている アドレス値を格納するための変数 である。
C言語では、データ構造の作成や、配列アクセスなどで、必須となる機能である。

ポインタの基本文法

  • ポインタの宣言

intなどの "型名" + "*" でその型に対するポインタの型となります。

int* p // int型へのポインタ変数pの宣言。
  • ポインタの利用方法

ポインタには、メモリのアドレスが入っているが、できることは次の2とおりである。

  1. アドレスを他の変数やポインタ変数に渡す。
  2. ポインタに格納されているアドレスの指す場所にある、値を読み取る。( 間接参照 )

1.の使用例

int* p1; //ポインタ変数p1
int* p2; //ポインタ変数p2

p1 = (int*)0x0000; // アドレス0x0000番地をポインタ変数p1に格納
p2 = p1; // p1が格納しているアドレス値をp2へ代入。 このときp1 = p2 = 0x0000となっている。

2.の使用例

int* p1; //ポインタ変数p1
int val; // int型の値を格納する変数 val

p1 = (int*)0x0000; // アドレス0x0000番地をポインタ変数p1に格納

*p1 = 10;  // ポインタ変数p1に対して、"*"を付けて間接参照することで、アドレスの指す先の値を書き替えている。 
// 注意すべきは、↑を実行してもp1の値は変わらず、0x0000であるということである。

val = *p1; // ポインタ変数の前に、"*"を付けるとことで、ポインタの格納されるアドレスが指す先にあるデータを参照できる。 この場合10が読み取れる。
printf("%d", val); // 10が表示される。
*p1 = 20; // ポインタ変数p1に対して、"*"を付けて間接参照することで、アドレスが指す先の値を書き替えている。 
// 注意すべきは、↑を実行してもp1の値は変わらず、0x0000であるということである。

printf("%d", val); // 20が表示される。

C言語とポインタ

C言語では、上述のように、ポインタ変数を使用してアドレスをやりとりすることも、もちろんありますが、
実は気づかぬうちに、内部でポインタ的な観点で処理が実行されているものが多くあります。

以下に例を挙げます。

  • 配列を操作するとき
int array[3]; // int型のサイズで連続して3つ分の領域をメモリ上に確保した配列。ここで、確保された領域の先頭アドレスが0x0000番地だったと仮定する。
// 以下全て、内部的には、ポインタ参照が内部で行われることで、配列の処理が実現されています。

// 配列に値を代入。
array[0] = 5;
array[1] = 4;
array[2] = 3;

解説します。
まず、C言語では、配列名のみ記述すると、「 その配列の先頭アドレスを示す。 」という決まりがあります。
この例の場合、arrayと書くだけで、0x0000番地を指すことになります。

では、array[0] = 5;  では、何が起きているかというと、

イデックスでしめす[0]は、arrayから、int型サイズで0個目を見てねということになります。(int型サイズで、というのは、arrayがint型へのポインタ変数であるため。)
つまり 0x0000番地 + 0 × intのsize(4) = 0x0000番地 を参照している状態になります。

従って、array[0] = 5; は以下のように書き替えられます。

*(int*)(0x0000) = 5;  // 0x0000番地をキャスト演算子(int*)というものを用いて、ポインタ型と認識させ、ポインタに対して "*"で関節参照しています。

さらに見ていきます。
array[1] = 4;  では、何が起きているかというと、
イデックスでしめす[1]は、arrayから、1個目を見てねということになります。
つまり 0x0000番地 + 1 × intのsize(4) = 0x0004番地 を参照している状態になります。

従って、array[1] = 4; 以下のように書き替えられます。

*(int*)(0x0004) = 4;  // 0x0004番地をキャスト演算子(int*)というものを用いて、ポインタ型と認識させ、ポインタに対して "*"で関節参照しています。

このように配列を操作する際には、配列の先頭アドレスのポインタと、配列のデータ型から、参照が行われています。

これを用いて、配列を宣言せずに、ポインタを配列のように扱うことも可能です。( 動的確保

int* p; // int型へのポインタ変数

// malloc()関数は、引数に指定したサイズ分のメモリを動的に確保し、その確保した領域の先頭アドレスを返す。

p = (int*)malloc(sizeof(int) * 3 ); // sizeof(int)はint型のサイズ4byteを意味する。 それを×3つ分。
// このとき、確保した領域の先頭アドレスは0x0000番地だったとする。つまり、p=0x0000とする。

// 配列に値を代入。
*(p + 0) = 5; // p[0]と書いてもよい。 pは確保領域の先頭アドレス。
*(p + 1) = 4; // p[1]と書いてもよい。 pは確保領域の先頭アドレス、 + 1はpの型に応じて、一つ領域を進めるという意味。
*(p + 2) = 3; // p[2]と書いてもよい。

このように、C言語では、配列など、ポインタを受け渡しすることで、任意のアドレスにアクセスすることが可能になります。

こういった技術を使うことで、ハードウェアの特定アドレスにもともと割り当てられているレジスタを操作したりすることも可能になります。
他にも、データ構造を表すときにも、ポインタでデータ間を繋いだりすることで表現します。