趣味の開発ノート

ITの学習やプログラミング・ノーコードアプリ開発のことなど。

【C言語】配列変数のポインタについて理解を深めたい

プログラミングの幅を広げるために、最近はC言語を学習している。 現在「ポインタ」について学習しているのだけど、これがやっぱり難しい。

変数のポインタについては、まだなんとかイメージができてきた。
でも、配列のポインタの話になって一気にこんがらがってしまった。

そこで、実際にint型の変数・int型の配列変数とそれらのポインタを色々出力してみて、どの変数名にどのような値が格納されているかを実験して見てみることにした。

※おそらく、こちらの記事の方がまとまっています。

nouka-it.hatenablog.com

ポインタとは

とはいえ、そもそもポインタとは?というイメージがないと難しい。
ここでは簡単に、

  • ポインタとは、変数の値が格納されているメモリの場所(アドレス)を示すもの
  • ポインタ値を格納しているものをポインタ変数とよび、その型はポインタ型である(禅問答みたい)

変数のポインタ

まずは、基本的な変数のポインタの使い方を整理。

  • ポインタ変数を定義するときは、変数名の頭に*をつける
  • 変数のポインタ値を表すときは、変数名の頭に&をつける(アドレス演算子
  • ポインタ変数に格納されている実際の値を参照するときは、ポインタ変数名の頭に*をつける(間接参照演算子
    • 同じ*付きの変数名でも、「通常の型の変数についているのか」「ポインタ変数についているのか」で違う意味合いになるので注意

次に、色々な出力を試してみる。

▼変数とポインタの実験コード

#include <stdio.h>

int main()
{
    int hp = 10;
    int *p_hp;    // ポインタ変数
    p_hp = &hp;

    // 変数とポインタ変数を見てみる
    printf("①変数hpの値 hp = %d\n", hp);
    printf("②変数hpのポインタ値 &hp = %p\n", &hp);
    printf("③ポインタ変数p_hpの値(ポインタ値) p_hp = %p\n", p_hp);
    printf("④ポインタ変数p_hpに格納されている値 *p_hp = %d\n\n", *p_hp);

    return 0;
}

▼出力結果

①変数hpの値 hp = 10
②変数hpのポインタ値 &hp = 0x16b133628
③ポインタ変数p_hpの値(ポインタ値) p_hp = 0x16b133628
④ポインタ変数p_hpに格納されている値 *p_hp = 10

ざっくりみると、

  • ①と④の表記だと、実際に格納されている値が出力される。
  • ②と③の表記だと、ポインタ値が出力される。

となっている。ここまでは基本的なところ。

配列のポインタ

配列変数は、変数の時とポインタ周りの挙動が違っているので、具体的に比較してみよう。

通常の変数定義の時と違うのは、

  • 配列変数ののポインタへの代入時には、変数のポインタの時のように変数名の頭に&アドレス演算子)をつける必要がない

と言うこと。

添字なしの場合

添字(インデックス)なしの配列変数・ポインタ値はどうなるか?を色々出力。

▼配列変数とポインタの実験コード(添字なし)

#include <stdio.h>

int main()
{
    int monster_hp[5] = {5, 10, 20, 22, 30};
    int *p_monster_hp;   // ポインタ変数
    p_monster_hp = monster_hp;

    // 配列変数とポインタ変数を見てみる(添字なし)
    printf("①配列変数monster_hpの値(ポインタ値) monster_hp = %p\n", monster_hp);
    printf("②配列変数mosnter_hpのポインタ値 &monster_hp = %p\n", &monster_hp);
    printf("③ポインタ変数p_monster_hpの値(ポインタ値) p_monster_hp = %p\n", p_monster_hp);
    printf("④ポインタ変数p_monster_hpに格納されている値 *p_monster_hp = %d\n\n", *p_monster_hp);
}

▼出力結果

①配列変数monster_hpの値(ポインタ値) monster_hp = 0x16b133630
②配列変数mosnter_hpのポインタ値 &monster_hp = 0x16b133630
③ポインタ変数p_monster_hpの値(ポインタ値) p_monster_hp = 0x16b133630
④ポインタ変数p_monster_hpに格納されている値 *p_monster_hp = 5

出力を見てみると、通常の変数の時との違いは

  • ①の時に、配列変数monster_hpの値はポインタ値を出力している(通常の変数hpではその値を出力)

と言うこと。この添字なしの配列名には、配列の1つ目の要素のポインタ値が格納されているというのが、一つ覚えておくポイント。

添字ありの場合

次に、添字(インデックス)ありの配列変数・ポインタ値はどうなるか?を色々出力。
つまり、要素を取り出すときの表記。1番目の要素を例に見てみる。

▼配列変数とポインタの実験コード(添字あり)

#include <stdio.h>

int main()
{
    int monster_hp[5] = {5, 10, 20, 22, 30};
    int *p_monster_hp;   // ポインタ変数
    p_monster_hp = monster_hp;

    // 配列変数とポインタ変数を見てみる(添字あり)
    printf("①配列変数monster_hpの1番目の値 monster_hp[1] = %d\n", monster_hp[1]);
    printf("②配列変数mosnter_hpの1番目のポインタ値 &monster_hp[1] = %p\n", &monster_hp[1]);
    printf("③'ポインタ変数p_monster_hpの1番目の値 p_monster_hp[1] = %d\n\n", p_monster_hp[1]);
    // printf("④ポインタ変数p_monster_hpに格納されている値 *p_monster_hp[1] = %p\n", *p_monster_hp[1]); 
}

▼出力結果

①配列変数monster_hpの1番目の値 monster_hp[1] = 10
②配列変数mosnter_hpの1番目のポインタ値 &monster_hp[1] = 0x16b133634
③'ポインタ変数p_monster_hpの1番目の値 p_monster_hp[1] = 10
# ④は出力されず
  • ①と②は通常の変数と同じ挙動をとる
  • ③のケースで、通常の変数ではポインタ値を出力する形だが、配列の要素の場合はその実際の値が出力されている。
  • 値に対して間接参照演算子は適用できないので、④は出力できない(コメントアウトしている)

この③のケースの解釈が難しいんだけど、配列のポインタ変数に添字をつけると、要素のポインタ値ではなくその元の要素が取り出されるらしい。

もし通常の変数のパターンと対応させたいなら、次のように記述することになる。

▼配列変数とポインタの実験コード(添字あり・通常の変数と対応)

#include <stdio.h>

int main()
{
    int monster_hp[5] = {5, 10, 20, 22, 30};
    int *p_monster_hp;   // ポインタ変数
    p_monster_hp = monster_hp;

    // 配列変数とポインタ変数を見てみる(添字あり)
    printf("①配列変数monster_hpの1番目の値 monster_hp[1] = %d\n", monster_hp[1]);
    printf("②配列変数mosnter_hpの1番目のポインタ値 &monster_hp[1] = %p\n", &monster_hp[1]);
    printf("③ポインタ変数p_monster_hpの1番目の値(ポインタ値) p_monster_hp + 1 = %p\n", p_monster_hp + 1);  // 1番目の要素のポインタ値を出力
    printf("④ポインタ変数p_monster_hpに格納されている値 *(p_monster_hp + 1) = %d\n\n", *(p_monster_hp + 1)); // 1番目の要素のポインタ値を得て、その値を参照する
}

▼出力結果

①配列変数monster_hpの1番目の値 monster_hp[1] = 10
②配列変数mosnter_hpの1番目のポインタ値 &monster_hp[1] = 0x16b133634
③ポインタ変数p_monster_hpの1番目の値(ポインタ値) p_monster_hp + 1 = 0x16b133634
④ポインタ変数p_monster_hpに格納されている値 *(p_monster_hp + 1) = 10

③④をみると、配列のポインタ変数からn番目の要素のポインタ値を取りたい場合、添字[n]ではなく演算+ nを行う必要があると言うことになる。

対応関係

上手いまとめ方がわかんないけど表にした。

出力する値の種類 通常の変数 配列変数 説明
hp monster_hp[1] 一般的な表記で実際の値を出力
ポインタ値 &hp &monster_hp[1] アドレス演算子&でポインタ値を出力
ポインタ値 p_hp p_mosnter_hp + 1 ポインタ変数でポインタ値を出力
*p_hp *(p_monster_hp + 1) ポインタ変数に間接参照演算子*で実際の値を出力

まとめ

色々な値を出力させてみて結果を試してみた。
とりあえず、通常の変数と配列変数とで挙動が違うと言うのは整理できたかな。

どの変数が、どのような中身(実値なのかポインタ値なのか)を持っているのかがわかるようになると、理解できるようになってくるのかな。