はじめに
自分の中で持っているC言語における変数とメモリ,アドレス,ポインタに対するイメージをまとめておく。
変数とメモリのイメージ
まず、変数とメモリの対応関係を以下のようなデータを格納する箱としてイメージする。
アドレスは4byteで表され、データは1byteごとに格納されるとする。
コンピュータは変数をメモリ上で
- アドレス: データが何番目のメモリに入っているかを表す、データのインデックス、住所のように考えておく。
- データ: データそのものが入っている。
で管理している。
例えば
・新しく変数にデータを格納する時
アドレスが○○のメモリに××のデータを入れる。
・変数のデータを参照する時
アドレスが○○のメモリには何が入っている?
->××が入っている
のようなイメージ。
変数宣言とメモリの対応イメージ
例えばC言語で以下のように変数を宣言した場合
char a = 2;
メモリと宣言した変数との対応イメージは以下。
(charは1byteとする)
参照方法 | 値 | |
---|---|---|
aのアドレスは? | &a | 0x00000001 |
aに入っているデータは? | a | 00000010 |
&を変数名につけると変数のアドレスを参照してくれます。
ポインタってなんでしょう
まず前提として
ポインタ = どこかにある変数のアドレスしか格納しない変数。
のようなイメージです。
要はアドレス格納用専用変数。
ポインタとメモリ対応イメージ
例えば
char *p = NULL;
を宣言した時のイメージは以下。(NULLは何もデータが入っていないデータ箱のアドレス00000000とする)
ポインタはアドレスしか格納しない変数なので
普通の?変数と同じくアドレスとデータを持ちメモリに確保されます。
(ここがポイント)
ではアドレス格納用専用変数であるポインタpに適当な変数aのアドレスを代入してみましょう。
p = &a;
この時のポインタとメモリのイメージは以下。
(ポインタ変数のサイズは4byteとする。)
以下に各値の対応関係をまとめます。
参照方法 | 値 | コメント | |
---|---|---|---|
pのアドレスは? | &p | 0x00000011 | (俗に言うダブルポインタ。なんという事は無い,アドレス格納用変数のアドレスを意味するだけ。) |
pに入っているデータは? | p | 0x00000001 | (アドレスしか格納されない変数なのだから,どこかの変数のアドレスが格納されている。今回の例で言うとaのアドレスが格納されている。) |
pに入っているアドレスのメモリには何が入っている? | *p | 00000010 | 適当な変数aに格納されているデータ"2"の事 |
ポインタの宣言と参照におけるややこしい所
ポインタを宣言する時は
データ型 *変数名
と記載するのだが
ポインタで値を参照する際には
変数名 ->ポインタに格納されているある変数のアドレス
*変数名 ->ポインタに格納されているアドレスのメモリに入っているデータ
となっており、宣言の時と参照の時でアスタリスクの意味するところが違ってくるので混乱を招く。
個人的には
ポインタは宣言する時と参照する時は別物と考えて
・宣言時
->*変数名: アドレス格納専用変数という事を明示するため*をつける必要がある。
・参照するとき
変数名: ポインタはアドレス格納専用変数なので、入っている値(どこかの変数のアドレス)がそのまま出てくる。
*変数名: データ表示モードみたいな物で、ポインタに格納されているアドレスのメモリに入っているデータを表示する。:
みたいに覚えている。(分かりにくい?)
ダブルポインタ
以下のような物がダブルポインタとかポインタへのポインタと呼ばれる。
char **pp = NULL;
ポインタはアドレス格納専用変数というだけで、当然メモリにアドレスとデータの値を持つ。
なので、ダブルポインタはある適当なポインタのアドレス格納しか格納しない変数という事になる。
例えば
char = 2;
char *p = NULL;
p = &a;
char **pp = NULL;
pp = &p;
というようなコードがあるとすると、以下のようなイメージ。
実験(実際にどうなっているのか確かめてみよう)
実際に上記のイメージを確かめてみるため
以下のようなソースコードを作成した。
#include <stdio.h>
int main() {
int a = 1;
int *p = NULL;
p = &a;
int **pp = NULL;
pp = &p;
return 0;
}
a: 適当な変数
p: ポインタ
pp: ダブルポインタ
という意味で記述した。
今回は上記のソースコードをWSL2 Ubuntuのgccでコンパイルし
gdbを使ってデバッグしてみた。
(以下デバッグ開始まで)
gcc -g -o test test.c
gdb ./test
(gdb) b main
(gdb) r
main関数を最後まで実行後
各値は以下のようになった。
まずは適当な変数aとポインタpの関係から。
(gdb) print a
$1 = 1
(gdb) print *p
$4 = 1
(gdb) print &a
$2 = (int *) 0x7fffffffe074
(gdb) print p
$3 = (int *) 0x7fffffffe074
ポインタpを参照すると、代入されたaのアドレスが
*pでデータ表示モードにするとaのアドレスからaに格納されているデータの値が
表示される事が分かる。
では次にポインタpとダブルポインタppの関係を見てみます。
(gdb) print &p
$5 = (int **) 0x7fffffffe078
(gdb) print pp
$6 = (int **) 0x7fffffffe078
(gdb) print &pp
$7 = (int ***) 0x7fffffffe080
ダブルポインタppを参照すると代入されたポインタpのアドレスが
また&ppを参照するとダブルポインタもメモリにデータそのものとアドレスを持つので
ダブルポインタのアドレス(冗長ですが)が
表示される事が分かる。
別件
ポインタの初期化をNULLで行うと
アドレスの0x00,つまり0番地(メモリに存在しないアドレスで指定しても格納されているデータは出てこない)
が格納されるのかと思っていたが,実際は違うみたい。
時間があれば調査してみるかも。
(gdb) print p
$2 = (int *) 0x64
(gdb) p *p
Cannot access memory at address 0x64
(gdb) p pp
$4 = (int **) 0x1000
(gdb) p *pp
Cannot access memory at address 0x1000
追記: アドレスとデータ(1byte単位)の並びについて
おそらくアドレスは以下のようにデータ
1byteごとで1bitで割り振られているのではないか
という事を実際に確認してみました。
以下のようなソースを作成。
#include <stdio.h>
int main() {
int x = 0;
double y = 0;
int a[2] = { 0 };
printf("size of x : %ld\n", sizeof(x));
printf("size of &x : %ld\n", sizeof(&x));
printf("size of y : %ld\n", sizeof(y));
printf("size of &y : %ld\n", sizeof(&y));
printf("size of a[1] : %ld\n", sizeof(a[1]));
printf("size of &a[1] : %ld\n", sizeof(&a[1]));
printf("address of &a[0] : %p\n", &a[0]);
printf("address of &a[1] : %p\n", &a[1]);
return 0;
}
}
結果は以下のようになった。
size of x : 4
size of &x : 8
size of y : 8
size of &y : 8
size of a[1] : 4
size of &a[1] : 8
address of &a[0] : 0x7ffe4ce5c930
address of &a[1] : 0x7ffe4ce5c934
ポインタ変数のサイズは型に関係なく8byte、
配列a[]の各要素のアドレスを見てみると
int型4byteで&a[1]が&a[0]に対して4bit進んでいたため
アドレスはデータ1byteにつき1bit割り当てられている事が確認出来た。
(アドレスが1進むにつきデータは1byte進む)
アドレス表記についてはサイズを表示させると8byteなのに対して
0x000000000000、6byteになっていたので
この点は予想とは違った。
(追記)
@shiracamusさんよりコメント頂き、上位の2byteの表示が省略されているだけだった。
参考文献
コンピュータはなぜ動くのか 第2版 知っておきたいハードウエア&ソフトウエアの基礎知識 矢沢 久雄 (著)日経BP
プログラムはなぜ動くのか 第3版 知っておきたいプログラミングの基礎知識 矢沢 久雄 (著)日経BP
【C言語】「NULL」の意味とNULLを用いた「安全なポインタの使い方」
ポインタとは?
ポインタ変数のサイズ
ポインタ