はじめに
本記事は、いろんなテキストやサイトの説明を見てみたけどいまいちポインタが分からない、という方に向けて書いたつもりです
目標はC言語のポインタが使えるようになることです
なので混乱しがちな変数のメモリには触れず、簡単のためにアドレスと値が対応しているように書いています
ポインタで苦しんでいる方に質問を受けた感じ、主に以下の傾向がありました
- 値とアドレス、ポインタ変数とアドレスなどが混沌としている
- 代入が変数にどのように作用するか知らない
- 関数に引数として変数を渡したときの決まりを知らない
C言語の基本的なルールを抑えられていないためにポインタが理解できていないことが多いようです
ここでは簡単なコードを例に変数の状態の変化を見ていきます
その中でC言語のルールを確認しつつポインタの理解に繋げていきます
int型を使った例
ポインタの前にまずはint型の変数を確認していきましょう
ポインタを理解するにはまずポインタでない普通の変数を理解することが重要です
# include <stdio.h>
int main() {
int a = 0; // (1)
int b = 100; // (2)
a = b; // (3)
printf("%d\n", a); // (4)
return 0;
}
コメント(4)の部分でどんな値が出力されるでしょうか
下の結果を見る前に考えてみてください
実行結果
100
簡単ですね
ではどうしてこのような結果になるか、変数の状態に注目ながら確認していきましょう
変数の状態を確認する
ここでは変数の状態を、変数名, 型, アドレス, 値 で表現します
この4つの要素を追うことで変数の状態が理解できます
4つもあるなんて大変!と思うかもしれませんが、変数宣言後に変化するのは 値 だけなので安心してください
では上記のコードで変数の状態がどのように変化しているか順に見ていきましょう
コメント(1)での変数の状態
int a = 0 でaが0で初期化されました
このときの変数aの状態は以下のように表せます
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| a | int | 0001 | 0 |
※アドレスの値は例です。以降の表でも同様
コメント(2)での変数の状態
int b = 100 でbが100で初期化されました
このときの変数の状態は次のようになります
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| a | int | 0001 | 0 |
| b | int | 1001 | 100 |
bは新たに作成されたのでaとはアドレスが異なりますね
コメント(3)での変数の状態
a = b でaにbが代入されました
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| a | int | 0001 | 100 |
| b | int | 1001 | 100 |
aの値が100に更新されましたね
a = b は、aの値をbの値である100で更新する処理です
そのためaのアドレスは変わらず、値のみ100に変化します
aがbの状態とすべて同じになるわけではないんですね
代入は '左辺の値' を更新する処理 であることを覚えておいてください
よって、コメント(4)で出力される値は 100 になります
コードの説明は以上になります
変数名, 型, アドレス, 値 を使って変数の状態を確認するイメージが掴めたでしょうか
では次からintのポインタ型について見ていきます
intのポインタ型を使った例
# include <stdio.h>
int main() {
int a = 0; // (1)
int* b; // (2)
b = &a; // (3)
printf("%d\n", *b); // (4)
*b = 100; // (5)
printf("%d\n", a); // (6)
return 0;
}
コメント(4)、(6)の部分でどんな値が出力されるでしょうか
下の結果を見る前に考えてみてください
実行結果
0
100
あっていましたか?
コピペでいいので実際にコードを実行して動作を確認してみてください
ではどうしてこのような結果になるか、変数の状態を見ながら確認して行きましょう
コメント(1)での変数の状態
int a = 0 でaが0で初期化されました
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| a | int | 0001 | 0 |
コメント(2)での変数の状態
int* b でintのポインタ型の変数bが宣言されました
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| a | int | 0001 | 0 |
| b | int* | 1001 | ???? |
宣言されたので変数bは作成されますが、初期化されていないので値は不明です
これは int c; と宣言したときに、cの値が分からないのと同じです
コメント(3)での変数の状態
b = &a でbに &a が代入されました
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| a | int | 0001 | 0 |
| b | int* | 1001 | 0001 |
bの値だけがaのアドレスで更新されています
変数の前に & を付けるとその変数のアドレスを表すようになるのでした
つまり &a は変数aのアドレス 0001 です
そして代入は左辺bの値を更新する処理でした
よって、bの値だけがaのアドレス 0001 で更新されます
コメント(4)での変数の状態
特に更新がないので変数の状態は(3)と同じです
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| a | int | 0001 | 0 |
| b | int* | 1001 | 0001 |
printf("%d\n", *b) で何が出力されるでしょうか
ポインタ変数は * を前につけることで、そのポインタ変数が値に持つアドレスの変数になるのでした
つまり、bはaのアドレスを値として持っているので、*b とすることでaと同じ状態になります
表に *b を加えると次のようになります (*bは変数名ではないですが)
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| a | int | 0001 | 0 |
| b | int* | 1001 | 0001 |
| *b | int | 0001 | 0 |
a と *b は名前が違うだけで同じものなんですね
ほんと!?と思った方は実際に確かめてみましょう
確認のためにaと*bの状態を実際に出力してみます
変数のアドレスは printf("%p", &a) のようにして出力できます
確認用のコード
# include <stdio.h>
int main()
{
int a = 0;
int *b;
b = &a;
printf("変数名= a, 型=int, アドレス=%p, 値=%d\n", &a, a);
printf("変数名=*b, 型=int, アドレス=%p, 値=%d\n", &*b, *b);
return 0;
}
出力結果 (※アドレスの値は実行環境によりことなります)
変数名= a, 型=int, アドレス=61ff18, 値=0
変数名=*b, 型=int, アドレス=61ff18, 値=0
aと*bのアドレスと値が同じになっていますね
今後確かめたい変数がある場合は適宜この方法を使って確認してください
長くなりましたが、以上から(4)の出力は 0 になります
コメント(5)での変数の状態
*b = 100 で*bに100が代入されました
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| a, *b | int | 0001 | 100 |
| b | int* | 1001 | 0001 |
先ほどaと*bは名前が違うだけで同じものだと分かったので一緒に書いています
*bに100が代入されたので*bの値が100になり、同じものであるaの値も100になりました
よって、コメント(6)での出力は 100 になります
int型の変数とintのポインタ型の変数の関係のイメージがなんとなくつかめたでしょうか
次は関数の引数に注目して変数の状態を見ていきます
int型を引数とする関数の例
# include <stdio.h>
void f(int b) {
printf("%d\n", b); // (2)
b = 100; // (3)
}
int main() {
int a = 0; // (1)
f(a);
printf("%d\n", a); // (4)
return 0;
}
コメント(2),(4)の部分でどんな値が出力されるでしょうか
下の結果を見る前に考えてみてください
実行結果
0
0
あっていましたか?
ではどうしてこのような結果になるか確認していきます
コメント(1)での変数の状態
int a = 0 でaが0で初期化されています
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| a | int | 0001 | 0 |
コメント(2)での変数の状態
関数fのなかでint型の変数bが新たに作成されます
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| a | int | 0001 | 0 |
| b | int | 1001 | 0 |
aとbのアドレスが異なる部分が重要です。
-
bは
f(a)で渡されたaとは関係なく 新しい変数として作成される ため、aとアドレスが異なります -
そして、bの値は
f(a)で渡された aの値で初期化される ので、bの値はaと同じ0になります
この2つは引数についてのC言語での決まり事です
ここをしっかり押さえておくことが、関数やポインタを理解する上で重要です
文章だと難しく感じるかもしれませんが、表を見ると分かりやすいと思います
以上より、コメント(2)では 0 が出力されます
コメント(3)での変数の状態
b = 100 でbに100が代入されます
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| a | int | 0001 | 0 |
| b | int | 1001 | 100 |
これはaとは関係のない処理なのでaの値は変化しません
コメント(4)での変数の状態
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| a | int | 0001 | 0 |
f(a) の処理が終了したので変数bは消滅しました
これは スコープ という言葉で説明される仕組みですが、本筋からそれるのでここでは説明しません
コメント(3)以降に変数aの状態を変更する処理がないため、変数aの状態は(3)の状態と同じになります
よって、コメント(4)では 0 が出力されます
説明は以上になります
関数の引数となっている変数の状態のイメージが掴めたでしょうか
では次に関数の引数がintのポインタ型になった場合を見ていきましょう
intのポインタ型を引数とする関数の例
# include <stdio.h>
void f(int* b) {
printf("%d\n", *b); // (2)
*b = 100; // (3)
}
int main() {
int a = 0; // (1)
f(&a);
printf("%d\n", a); // (4)
return 0;
}
コメント(2), (4)の部分でどんな値が出力されるでしょうか
実行結果
0
100
あっていましたか?
int型の場合と違い変数aの値が(4)の時点で変更されていますね
コピペでいいので実際にコードを実行して動作を確認してみてください
ではこれから変数の状態を確認していきましょう
コメント(1)での変数の状態
int a = 0 でaが0で初期化されています
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| a | int | 0001 | 0 |
コメント(2)での変数の状態
関数fの中でintのポインタ型の変数bが作成されます
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| a | int | 0001 | 0 |
| b | int* | 1001 | 0001 |
ここからint型の場合と異なっていますね
順を追って見ていきましょう
C言語の決まりで、bはaとは関係なく作成されるのでした
このときのbの状態は以下のようになります
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| b | int* | 1001 | ???? |
int* b と宣言した場合と同じですね
初期化されていないので値が不明な状態です
そしてもう一つのC言語の決まりで、bの値は f(&a) で渡された &a つまり 0001 で初期化されるのでした
初期化後のbの状態は以下のようになります
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| b | int* | 1001 | 0001 |
では、 printf("%d\n", *b) の出力は何になるでしょうか
aと*bを含めた変数の状態は次のようになります
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| a, *b | int | 0001 | 0 |
| b | int* | 1001 | 0001 |
intのポインタ型のbはaのアドレスを値として持つので、aと*bは同じものになるのでした
以上より、コメント(4)では 0 が出力されます
わざわざポインタなんか使わずに直接変数aを使いたいと思いますが、C言語では残念ながらできません
変数aはmain関数で定義されているので、関数fからは直接使えないのです
そのため面倒ですが、関数fの中で変数aの値を変更する場合は、ポインタ型の変数bを使う必要があります
scanfが scanf("%d", &a) のように変数のアドレスを受け取るようになっているのはこのためだったんですね
コメント(3)での変数の状態
*b = 100 で*bに100が代入されました
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| a, *b | int | 0001 | 100 |
| b | int* | 1001 | 0001 |
aと*bは同じものなのでaの値も100になります
コメント(4)での変数の状態
| 変数名 | 型 | アドレス | 値 |
|---|---|---|---|
| a | int | 0001 | 100 |
f(&a) の処理が終了したので変数bは消滅しました
コメント(3)以降にaを変更する処理がないため、aの状態は(3)の状態と同じになります
よって、コメント(4)では 100 が出力されます
説明は以上です
おわりに
ポインタは変数のアドレスを値として扱うため直感的でなく最初は理解が難しいと思います
さらに関数や配列と一緒になるとより複雑になります
しかしそのような複雑な状況も、ひとつひとつをみればC言語の小さな決まりからできていることが分かります
変数で混乱しないためには、今扱っている変数の状態を把握した上で、C言語の決まりに沿って考えることが重要です