Help us understand the problem. What is going on with this article?

身体で覚えるポインタ型 〜Pointer Stew〜

More than 1 year has passed since last update.

はじめに

 C言語を習得する上で最難関とされる「ポインタ型」の取り扱いについて、参考書を読んで理解しようとするより、実際のコードを見て、いじって身体で学んだ方が圧倒的に理解が早く、またイメージがつきやすいと感じたため、Pointer Stew という有名な「ポインタと配列を使用するパズル」(既に難しそう笑)を用いてポインタ型に関する理解を深める。

ポインタ型とは

 「あるデータを指すデータ」のこと。データのアドレス。データがどこに記憶されているか。「ポインタもまたデータである」と言うことが一番大事。

Pointer Stew

 それでは、実際のコードを見ながらポインタ型の取り扱い方について見ていく。

#include <stdio.h>
1 int main()
2 {
3     char *x[4] = {"enter", "new", "point", "first"};
4     char **xp[4];
5     char ***xpp;
6     int   i;   
7     for (i = 0; i < 4; i++)
8         xp[i] = x + (3 - i);
9     xpp = xp;
10  
11    printf("%s", **++xpp);
12    printf("%s ", *--*++xpp+3);
13    printf("%s", *xpp[-2]+3);
14    printf("%s\n", *--xpp[-1]+1);
15 
16    return 0;
17}

 サイトによって書き方はまちまちだが、見た感じ内容に大差はなかったので今回はこのコードを参照する。
ちなみに、出力結果が何になるかわかるだろうか?一度考えてみることをオススメする。答えがわかって自信がある方は、この先を見ずにブラウザバックしても良いだろう。なぜなら答えが…

解説

1. char型のポインタ配列

3     char *x[4] = {"enter", "new", "point", "first"};

 ここでは「 x 」が an array of pointers to characters つまり、「char型のポインタ配列」(要素がchar型のポインタ、である配列)であることを示している。ここで、char型は1文字を格納するデータ型であるので、char型のポインタは(複数)文字列の先頭文字のアドレスを格納しているデータである。この意味で、char型のポインタは以下のようにchar型の配列と似た使い方が可能である。( s1,s2 は共に printf の中で先頭文字のアドレスを意味している。)

char *s1  = "pointer"; printf("%s\n", s1); >>> pointer
char s2[] = "stew";    printf("%s\n", s2); >>> stew

 したがって、今回の場合は「ポインタ型の配列」のため、配列の各要素がポインタ型(上で言う s1, s2 )である。ゆえに、以下のように出力できる。

char *x[4] = {"enter", "new", "point", "first"};
printf("%s\n", x[0]);
>>> enter
printf("%p\n", x);
>>> 0x7ffc2369d7d0 (ASLRが有効になってるため、プログラムを実行する度に変化する)

 なお、上の出力の2つ目は、ポインタ配列 x のアドレス、つまり、ポインタ配列 x の先頭のアドレス(ただし、"enter" のアドレスではないことに注意!! "enter" のアドレスは、printf("%p", x[0])で参照可能。)を表している。

2. char型のポインタ型のポインタ配列

4     char **xp[4];
6     int   i;   
7     for (i = 0; i < 4; i++)
8         xp[i] = x + (3 - i);

 ここでは、「 xp 」がan array of pointers to pointers to characters つまり、「char型のポインタ型のポインタ配列」であることを示している。したがって、先ほどの例を踏まえると、以下のように出力される。

char **xp[4];
int   i;
for (i = 0; i < 4; i++){
    xp[i] = x + (3 - i);
    printf("i=%d: %p\n",i,xp[i]);
}
>>> i=0: 0x7ffc2369d7e8
>>> i=1: 0x7ffc2369d7e0
>>> i=2: 0x7ffc2369d7d8
>>> i=3: 0x7ffc2369d7d0

 ちなみに、この時 i の値が 1 変わると アドレス(番地・住所)が 8 変化することが確認できる。これも、

int size = sizeof(x[0]); 
printf("%d\n", size);
>>> 8

 によって確認できる。char型のポインタ配列の各要素に割り当てられているサイズは 8 byte のようだ。

3. char型のポインタのポインタのポインタ

5     char ***xpp;
9     xpp = xp;

 ここまでくれば流れで予想できると思うが、ここでは、「 xpp 」が a pointer to pointers to pointers to characters つまり、「char型のポインタのポインタのポインタ」であることを示している。なので、以下のように出力される。

printf("%p\n", xpp);   >>> 0x7ffc2369d7b0
printf("%p\n", *xpp);  >>> 0x7ffc2369d7e8
printf("%p\n", **xpp); >>> 0x400714
printf("%s\n", **xpp); >>> first

本題

 それでは実際に Pointer Stew を考えるが、その前に「型定義時と運用時には「*」の意味が異なる」ことについて少しだけ触れる。

 型定義時は今まで述べてきた通り、
- 「char *」 =「char型のポインタ」
- 「char **」=「char型のポインタのポインタ」

と言うように、「『*』=『のポインタ』」と考えることができていた。
 しかし、上の printf 関数内での「*」の使われ方はそれとは明らかに違うことがわかる。例えば「 **xpp 」が first を表しているように、ポインタに「*」をつけることで、「ポインタが指している対象」を意味する。(つまり、ここでは **xpp はchar型のポインタの役割をしている。)


11    printf("%s", **++xpp);

 これは、9 行目の xpp = xp と言う式から分かる通り、 xpp が「char型のポインタ型のポインタ配列 xp 」の先頭アドレスを意味している。「++」は+1と同義(足すタイミングが異なるだけ。後ろに付く場合もある)だと考えれば、先頭から2つ目のアドレスを指しており、(ここまでで ++xpp )そこに「*」が2つ付くことによって、 xp[1] = x + (3 - 1) = x + 2char *x[4] = {"enter", "new", "point", "first"} からここでの出力は pointであることが分かる。


12    printf("%s ", *--*++xpp+3);

 これは、「++」の知識が多少必要である。「i++」≒「i=i+1」であるから、先の計算で xpp は 1 インクリメントされている。したがって、ここで更に1インクリメントすることによって、「char型のポインタ型のポインタ配列 xp 」の「3つ目」のアドレスを指している。(ここまでで ++xpp
 そこに「*」が1つ付くことによって、 xp[2] = x + (3 - 2) = x + 1、これを「--」で1デクリメントすることによって、「char型のポインタ配列 x 」の先頭のアドレスを示す。(ここまでで --*++xpp)
 更に「*」が付くことによって先頭の要素 "enter" の先頭のアドレスを意味し、最後に3足すことで4文字目"e"のアドレスを意味する。そこから printf するので、ここでの出力は erであることが分かる。


 ここまででだいぶ慣れてきたと思うので、残りの二問は自力でやって見ることをオススメする。(答えも分かっただろうし…)


13    printf("%s", *xpp[-2]+3);

 これは、xpp[-2] の意味をきちんと理解する必要がある。「 xpp の後ろから2つ目の要素だな!」と思うかもしれないが、残念ながらそれは間違いである。正しくは、xpp の指すアドレスに[]の中の数 -2 を足したアドレスを指すと言うことである。(他にも*(ポインタ変数 + 要素番号)なんてものもある。)

 先ほどまでですでに xpp は2インクリメントされているので、ここでは元々 xpp が指していたアドレスを指す。つまり、xp[0] = x + (3 - 0) = x + 3 そこに「*」が付くことによって "first" の先頭アドレスを指し、先ほどと同様にして出力はstとなる。


14    printf("%s\n", *--xpp[-1]+1);

 最後はewである。

 と言うのはあまりにも雑すぎるので一応解説すると、xpp[-1] が先程と同様にして xp[1] = x + (3 - 1) = x + 2 を指し、そこから1デクリメントして「*」が付くことによって "new" の先頭アドレスを指し、最後に+1 をするので2文字目から出力が始まり、結果はewとなる。

 以上より、プログラムを通した答えはpointer stew となる。

最後に

 ポインタ型を難しくしている要因は、記号の使い方だと思う。例えば

3     char *x[4];
4     char **xp[4];
5     char ***xpp;

 というように定義するが、これだと結局 x , xp , xpp が何を表すものとして定義されたのかがわかりにくい。多少の誤差かもしれないが、

3     char*   x[4];
4     char**  xp[4];
5     char*** xpp;

 と型を表す方にポインタ型である記号をつけた方が格段にわかりやすくなると思う。また、先ほども述べたが、型定義時に使う「*」と関数内で使われる「*」が同じ記号なのも苦戦するポイントだと思う。「同じような役割を持つ記号は同じ記号にする」と言う考え方が働いているようだが、かえってわかりにくくなっている気がする。(他に理由があったら教えてください。)

参考

 僕自身間違った認識をしている部分もあるかと思いますので、何かミスがあればご指摘よろしくお願いします。

Why do not you register as a user and use Qiita more conveniently?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away