具体例を用いたポインタの説明
C言語のポインタの説明は一般的に「番号のついた箱があって…」といった感じで抽象的な説明に終止しています。
ですが人によってはもっと具体的な説明のほうがわかりやすいのではと考えたため、このページではVisual Studio (以下VS)の機能を用いて具体的に説明していきます。
なお、このページではVS 2019を使用していますが、他のバージョンのVSでも同様の機能が実装されています。他の統合開発環境(IDE)はよくわかりませんが、類似した機能があるのではないかと推測します。
下準備
最初に下準備として、VSで新規プロジェクトを作成し、以下のようにコードを記述してください。
#include <stdio.h>
int main()
{
int a, b;
printf("0x%p, 0x%p", &a, &b);
a = 0x01234567;
b = 0x89ABCDEF;
}
そうしたらF10を3回押して(もしくはVSのメニューから「デバッグ」ー「ステップオーバー」を3回選択して)コードをステップ実行してください。これで6行目の左側に矢印が表示された状態で実行が中断されます。
次にVSのメニューから「デバッグ」ー「ウィンドウ」ー「メモリ」ー「メモリ1」と選択してください。これで以下のようなよくわからないウィンドウが表示されます。
念の為VSのメニューから「デバッグ」ー「ウィンドウ」ー「自動変数」と選択してください。これで以下のように変数の内容が一覧で表示されます。
これだと説明が少し難しいので、「自動」ウィンドウの上で右クリックして「16進数で表示」にチェックを入れてください。これで以下のように数値が16進数で表示されるようになります。
メモリを直接覗き見る
それでは具体的に値を見ていきます。
「自動」ウィンドウの&aに表示されている値が変数aのアドレスとなります。ここでは0x006ffd40がアドレスです(アドレスはプログラムを実行するたびに変化します)。この0x006ffd40を「メモリ1」ウィンドウのアドレスのところに入力してリターンキーを押すと、そのアドレスの付近のメモリの内容を表示してくれます。
&bについても同様です。こちらのほうが説明しやすいので、以下アドレス0x006ffd34を基準に説明します。下図左の赤枠が変数bの内容、右の赤枠が変数aの内容になります。(&a = 0x006ffd40で&b = 0x006ffd34なので、0x006ffd34から12 byte右に進んだアドレスが&aのアドレスになります。)
さてもう一度F10を押してプログラムを1ステップ進めます。
すると変数aに値が代入され
それに合わせてメモリの内容が変化します。&aのアドレスから4 byteの色が変化しており、ここが変更されたことがわかります。
代入した値が0x01234567なのにメモリ上では"67", "45", "23", "01"の順になっているのは、Windowsがリトルエンディアンで値を扱うためです。エンディアンについてはまた別のページで。
さらにもう一度F10を押してプログラムを1ステップ進めると
変数bに値が代入され
それに合わせて&bのアドレスにある値が変化します。
ポインタ変数の値を覗き見る
それでは次のようなコードだとどうなるでしょうか。
int main()
{
int a = 0x01;
int* b = &a;
*b = 0x02;
}
この場合、4行目を実行したところでポインタ変数bに変数aのアドレス = &aの値が代入され
5行目が実行されたところでポインタ変数bの指すアドレス、つまり変数aに値が書き込まれます。
配列を覗き見る
次は配列で試してみましょう。
int main()
{
int a[3] = { 0x01, 0x23, 0x45 };
int *b = a + 1;
}
3行目が実行されたときに配列の値が初期化されます。
配列aのアドレスは0x0053fe80で、
書き込まれた値は以下のようになります。配列の要素1つ1つはリトルエンディアンで値を保持していますが、配列全体としてはインデックスの小さい順番に並んでいます。
4行目を実行すると
変数aのアドレスに+4された値がポインタ変数bに代入されます。
なぜ+1ではなく+4なのかというと、aやbがint型 (4 byte)だから。ポインタを扱う上でその指し示す先が何byteの型なのかを意識しないといけないと不具合の温床になるから、との配慮でしょう。
なので変数の型をshort型に変更すると
int main()
{
short a[3] = { 0x01, 0x23, 0x45 };
short *b = a + 1;
}
配列の要素1つ1つは2 byteの大きさになり
ポインタ変数bの値は変数aのアドレスより2 byte分大きな値となります。
悪いことをしてみる
ここでちょっと悪いコードを書いてみます。はい、配列の範囲外に値を書き込んでみます。いわゆるバッファオーバーランです。
int main()
{
int a[3] = { 0x01, 0x23, 0x45 };
a[3] = 0x67;
}
3行目で配列の値を初期化したあと
4行目で予想通り配列の範囲外に値が書き込まれます。
ちなみにVSではこのバッファオーバーランを検知して例外がスローされるようです。
さらにちなみに以下のコード(a[3]ではなくa[4]に値を書き込む)だと
int main()
{
int a[3] = { 0x01, 0x23, 0x45 };
a[4] = 0x67;
}
先ほどとは異なる例外がスローされるようです。
さいごに
C言語のポインタについて具体例を用いて説明してみましたが、いかがだったでしょうか。変数がメモリ内でそれぞれどのように管理されているのか知ることでポインタについての理解が深まることを期待します。