はじめに
2022年10月に42東京のpiscineに参加しましたがC言語のポインタ理解に苦労しました。初学者がハマると思われるポイントはほぼ似たところだと思うので、ざっくり自分自身が軒並み嵌っていったポイントとそこへの理解についてまとめてみました。なお、ポインタの基礎の基礎部分であり、極めて初歩的な部分ですので、ポインタを理解されている方には目新しいものはないと思います。また厳密な説明ではなく、理解イメージとなっていますが、嵌ってらっしゃるpiscine生の方のお役に立てたら嬉しいです。
Pointerのざっくりとした整理
ポインタとはなにかや宣言や使用法の基本については参考書などを読んでいただきたいですが、基本的にポインタ型の変数は、他のメモリの住所を格納でき、*
や&
の記号をつけて使うことで、それぞれのデータにアクセスできます。
-
int *pdata
のように宣言することで、ポインタ型の変数として宣言 -
&pdata
のように&
を付すと、ポインタ自体の住所(&
アドレス取得) -
*pdata
のように*
を付すと、ポインタが指している先のデータ(*
間接参照) -
pdata
と演算子を何も付さないと、ポインタが指している先の住所
を指すことになります。
以下のコードではdata
であるint型の変数に数字5
を格納し*pdata
とintのポイント型を宣言し、そこにdata
のアドレスを格納しています。
その後、*pdata
でdata
の数字5にアクセスし、くわえて&pdata
でpdata
自身の住所そしてpdata
でint型変数data
の住所をプリントしています。
#include <stdio.h>
int main(void)
{
int data;
int *pdata;
data = 5;
pdata = &data;
printf("%d\n", *pdata);// 5
printf("%p\n", &pdata);// 0x7ffc1e01a560 (それぞれの実行環境で違う)
printf("%p\n", pdata);// 0x7ffc1e01a55c (それぞれの実行環境で違う)
}
初心者にはこれが一体どこを指しているのか?*
や&
の使い方がさっぱり理解できずに苦労しました。意図しない挙動となっているときには*
や&
を付けてみたり消してみたりと試行錯誤で正解を探すような状況でした。
嵌ったポイント1:*
と&
と変数名
でどのデータにアクセスできるのか
まずメモリについては2階建ての建物のようなものとイメージし、二階には自分の住所を、一階には倉庫を有しているものとザクッと捉えました。
普通の変数とポインタ型の変数のイメージ
上記のメモリのイメージに普通のINT型とINTのポインタ型のメモリのイメージを投影すると以下の通りとなります。
ここでは、ポインタ型のデータ保管庫にポインタが指す先のメモリの住所が格納されていることに注意してください。では、それぞれのデータ部分にどのようにアクセスするかについて考えてみます。
*
や&
などの記号(演算子)がメモリイメージのどこに影響するか
*
や&
をポインタ型の変数につけるとどうなるのか、文字で理解しようとすると混乱しますが、上記のメモリのイメージ上でどの場所にアクセスするのかをイメージ的に把握すると簡単です。
なぜなら、ポインタ型も普通の(ポインタ型でない)変数も挙動は以下の通りほぼ同様であり、別個で覚える必要性が無いからです。
1. 演算子はつけず変数名
だけで呼び出す = データ保管庫に保管されているデータにアクセス(ポインタ型なら指している先の住所):図中のA(青色部分)
2. 変数名に&
をつけて呼び出す = メモリーの自分の住所情報にアクセス:図中のB(黄色部分)
3. 変数名に*
をつけて呼び出す = ここだけ ポインタ独特の挙動 となるが、データ保管庫に保管しているポインタが指している先の住所の保管庫にあるデータにアクセス: 図中の3
となっており、ポインタ型の変数と普通の変数の挙動に差異が発生するのは *
である間接参照の演算子のみとなります(そもそもポイント型でなければ間接参照できない)。上記をテーブルにまとめると以下のようになります。
変数名 で呼び出し |
&変数名 で呼び出し |
*変数名 で呼び出し |
|
---|---|---|---|
int data | 5 | 0x7ffc1e01a55c | できない |
int *pdata | 0x7ffc1e01a55c | 0x7ffc1e01a560 | 5 |
挙動 | 自分の保管庫にあるデータへアクセス(イメージ図青部分) | 自分の住所へアクセス(イメージ図黄色部分) | 自身の保管庫に格納してあるメモリ住所の保管庫に格納されているデータにアクセス |
差異 | なし | なし | あり |
重ね重ねになりますが、ポインタ型変数OR普通の変数であろうと&
や変数名のみ
の場合はメモリイメージ上アクセスする場所は同じですので、どの情報が必要でそれがメモリイメージ上どの場所に保管されているのかを把握し、演算子を適切に使うことが必要になると思います。
嵌ったポイント2:変数宣言時の*と間接参照の*
ポインタの初期に嵌るのは*
の役割への理解が混乱することでした。*
は、
- ポインタ型の変数宣言時や関数の仮引数に使用される
- ポインタ型変数への間接参照に使われる
*
は上記の通り2つの別の挙動を行いますが、初学者には同じコード上に*
が複数現れるため、役割の理解に混乱が生じます。ここについては、同じ記号*
が使われるものの、
- 変数宣言や関数の仮引数の
*
は単に自分はポインタですよと宣言の役割 - 本文内の
*
は間接参照の役割
と全く別の役割を果たしており、両者は同じ記号を有するものの関連性はないと理解したほうが、混乱はしないと思います。
参考書などでは、int *ptr = &nbr
のように、*
を使ってポインタが指す先の保管庫にデータを格納しているように見えることもあるが、これは、int *ptr
の変数宣言とptr = &nbr
を一括で行っているだけです。
嵌ったポイント3: 演算子の優先順位
ポインタ型の挙動については上記のように理解したものの、実際プログラムしてみると理解している挙動と実際の動きが違うことが多々あり苦労しました。演算子の挙動の理解と同時に認識が必要になるのが演算子間の優先順位です。演算子はたくさんあるので、詳細は参考書を確認いただきたいのですが、特に間接参照*
との対比で重要になるのが[]
+
-
で、演算子の優先順位は[]
> *
> + or -
です。*
間接参照で思った通りのデータにアクセスできてないときには演算子の優先順位を考えて見てください。
例えば、配列の要素にアクセスする上で、array[1]
とあれば、*(array + 1)
と*
の前に()
をつけて演算子の優先順位を+
優位にすることが必要になります。
間接参照*
の優先順位が中途半端であることから、二次元配列などへの*
によるアクセスが難しくなっていると思います。ここは素直に[]
の添字演算子を使うのが吉かと。
array[ i ][ j ]
と簡単にアクセスできるものが、間接参照だと*(*(array + i) + j)
に複雑な括弧を使う必要がでてくる
まとめ
自分自身も未だポインタの理解が浅いなぁと思っているので、自身の理解の確認を兼ねて、記事を書いてみましたが・・・言語化って難しいですね。うまく説明できてない点もあるやと思いますが、優しい目線で読んでいただけたらと思います。間違っている部分や提案などございましたら、ぜひぜひ教えて下さいませ。
参考文献
この記事は以下の情報を参考にして執筆しました。