LoginSignup
2
3

More than 1 year has passed since last update.

C言語の「*p」のあれこれ(*p++の悪夢)

Last updated at Posted at 2020-06-05

「*p」のあれこれ

C言語のポインタ学んでたら、「* p」周りで頭がこんがらかってきたので、整理しておく。

まずは基本「*p」

#include<stdio.h>
int main(void)
{
    int a[3], * p;
    p = a;
    a[0] = 10, a[1] = 20, a[2] = 30;
    printf("*p...%d\n", *p);
}
// 出力結果:*p...10

これは簡単。
「p = a」で、pにa[0]のアドレスを入れる。
そして、a[0]の中身を取り出しているだけ。

「*p+1」

#include<stdio.h>
int main(void)
{
    int a[3], * p;
    p = a;
    a[0] = 10, a[1] = 20, a[2] = 30;
    printf("*p+1...%d\n", *p+1);
}
// 出力結果:*p+1...11

これもそう難しくはない。
a[0]の中身を取り出して、それに1を足しているだけだ。
(C言語の文法的な仕様で、「*(参照)」のほうが「+」よりも優先度が高く、先に処理される)

「*(p+1)」

#include<stdio.h>
int main(void)
{
    int a[3], * p;
    p = a;
    a[0] = 10, a[1] = 20, a[2] = 30;
    printf("*(p+1)...%d\n", *(p+1));
}
// 出力結果:*(p+1)...20

さて、今回は括弧が付いた。
括弧は最も優先度の高い演算子である。
よって、「p+1」が先に処理される。
「p+1」は、pが指定する「a[0]」からアドレスを1つ進めるということだ。
すなわち。「a[0+1]」と捉えることができるので、ポインタpは「a[1]」と同値であることが分かる。
これに「*」が付くので、ポインタの指す中身が取り出されて、出力結果は20。

「*p+=1」

#include<stdio.h>
int main(void)
{
    int a[3], * p;
    p = a;
    a[0] = 10, a[1] = 20, a[2] = 30;
    printf("*p+=1...%d\n", *p+=1);
}
// 出力結果:*p+=1...11

これはどうだろう。
「アスタリスク」(参照)は「+=」よりも優先順位が高い。
そのため、「*p」が先に処理される
「*p」は「*a[0]」と同値であるから、示す値は「10」。
その後、「+=1」されるので、出力結果は11。

ここからが鬼門「*p++」

#include<stdio.h>
int main(void)
{
    int a[3], * p;
    p = a;
    a[0] = 10, a[1] = 20, a[2] = 30;
    printf("*p++...%d\n", *p++);
}
// 出力結果:*p++...10

多くの初学者は、インクリメントしているので「10+1で11」だと思うであろう。私もそうだった。
しかしそれは間違いである。

ここで重要なことは、インクリメントには「前置インクリメント(例:++a)」「後置インクリメント(例:a++)」の2種類があり、その違いを理解することである。


というわけで、先に前置/後置インクリメントの違いを確かめておこう。

前置インクリメントと後置インクリメント
...
int a, a_before, a_after;

a = 0;
printf("a...%d\n", a);

// 前置インクリメント
a_before = ++a; 
printf("a_before(前置、++a)...%d\n", a_before);
printf("その後のa...%d\n", a);

// 後置インクリメント
a = 0;
a_after = a++; 
printf("a_after(後置、a++)...%d\n", a_after);
printf("その後のa...%d\n", a);

...
// 出力結果:a...0
//         a_before(前置、++a)...1
//         その後のa...1
//         a_after(後置、a++)...0
//         その後のa...1

こちらのサンプルコードの実行結果から分かるように、
・前置インクリメント→値を渡す前に値が変化
・後置インクリメント→値を渡したのちに値が変化

という違いがある。

より正しく書くと、
後置インクリメントは、「値を利用してから変化させる」と言える。


さて、ここで「* p++」について再び考えてみよう。

// 中略
p = a;
a[0] = 10, a[1] = 20, a[2] = 30;
printf("*p++...%d\n", *p++);
// 中略

これは後置インクリメントだ
先に、ポインタpの参照先である「a[0]」に格納されている値「10」がprintf関数へ渡されて出力される。
ここではじめてpの値が使われる。このあと、格納されている値(アドレス)が進むのである。

ところで、本当に最終的にポインタpのアドレスは進められているのか?実験をしてみよう。
追記してみて、その後の値がどうなっているのか調べてみる。

// 中略
printf("*p++...%d\n", *p++);
// 追加:【実験】その後の値はどうなっているのか?
printf("その後の*p...%d\n", *p);
// 出力結果:*p++...10
//          その後の*p...20

これでハッキリとした。
後置インクリメントなので、
「値を使ってから」
「ポインタ内のアドレスを進める」
のである。

まぁ当然だよね「(*p)++」

#include<stdio.h>
int main(void)
{
    int a[3], * p;
    p = a;
    a[0] = 10, a[1] = 20, a[2] = 30;
    printf("(*p)++...%d\n", (*p)++);
}
// 出力結果:(*p)++...10

これは簡単だ。
さっきの例を見たあとでは混乱するかもしれないが、冷静に考えてみてほしい。
後置インクリメントより、括弧のほうが優先順位が高い。
先に括弧内の処理がされるのである。

驚きの後置インクリメントパワー「*(p++)」

#include<stdio.h>
int main(void)
{
    int a[3], * p;
    p = a;
    a[0] = 10, a[1] = 20, a[2] = 30;
    printf("*(p++)...%d\n", *(p++));
}
// 出力結果:*(p++)...10

ここで思い出してほしいのが、後置インクリメントによって変化するのは、「値を使われてから」という特徴である。
printf関数に値が渡されてはじめて値が使われるので、出力結果ではインクリメントされていない状態である。
よって、*p++と同じ動作をする。
ややこしいと感じるなら、以下のように普通のint型変数として考えればわかりやすいだろう。

int a;
a = 0;
printf("a++...%d", a++);
a = 0;
printf("(a++)...%d", (a++));
// 出力結果:a++...0
//          (a++)...0

インクリメントの場所変えてみようぜ「*++p」

#include<stdio.h>
int main(void)
{
    int a[3], * p;
    p = a;
    a [0] = 10, a[1] = 20, a[2] = 30;
    printf("*++p...%d\n", *++p);
}
// 出力結果:*++p...20

これは前置インクリメントだ。
前置インクリメントは、値を使う前にインクリメントされる
つまり、はじめにポインタpがa[0]からa[1]へ進む。
そして、printf関数にポインタaの参照先a[1]の値「20」が渡される。

最後に、一番前でインクリメントしようぜ「++*p」

#include<stdio.h>
int main(void)
{
    int a[3], * p;
    p = a;
    a[0] = 10, a[1] = 20, a[2] = 30;
    printf("++*p...%d\n", ++*p);
}
// 出力結果:++*p...11

これは単純に「*p」で取り出した値「10」をインクリメントしている。
つまり「11」。

まとめ

インクリメントの前置・後置の違いを理解すると、何も難しくない。
とても勉強になった。

2
3
5

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
3