60
41

More than 5 years have passed since last update.

*p++のお話(インクリメント演算子って不思議だね)

Last updated at Posted at 2018-08-07

はじめに

printf("%d\n",*p++);

ってどうやって表示されるんだろうねってお話です。
あと、それだけだと面白くないので、インクリメント演算子で遊んでみたら面白いことが分かったってお話もします。

基本的に以下のコードを実行することにします。

#include <stdio.h>
int main(){

   int i[3] = {1,10,100};
   int *p = i;

   printf("%d\n", /*この部分をいろいろと変えます*/);
   printf("%d\n", *p);

   return 0;
}

コメント部分にいろいろな式を入れ出力結果を確認し、ポインタpが現在何を指しているか確かめるために2行目のprintfを追加しています。

環境は、paiza.ioを使いました。だってコンパイルめんどうだし
Cの規格はC11です。

いろいろ実行してみた。

いろいろと変える部分にいろいろ入れてみました。
どうなるか予想しながらページをスクロールするのも楽しいかもしれません。

++(*p)

実行結果
2
2

まあそうですよね。前置インクリメントは右にあるものをインクリメントして結果を与えるので、pの中身(iの先頭)がインクリメントされて表示されますよね。
次の行は結局そのまま*pが出力されてるので、先ほどインクリメントされたものが出力されます。

(*p)++

実行結果
1
2

これも当たり前田のクラッカーですよね。後置インクリメントでは*pが結果として出たあとにインクリメントされます。
したがって、1行目ではiの先頭が出力され、2行目ではiの先頭(インクリメント済み)が出力されます。

*(++p)

実行結果
10
10

そろそろ飽きてきた方はしばらく飛ばしてもいいかもしれません。
ポインタのインクリメント後、その中身を表示するので、iの2番目の中身が二度表示されます。

*(p++)

実行結果
1
10

これは知識があるかどうかで意見が分かれそうですね。
p++が括弧で囲われているからといって、出力より先にインクリメントされるわけではありません。
先にpが結果として出され、その結果がアスタリスクにより中身がprintfされます。
一通りの処理が終わった後、ポインタのインクリメントが行われます。
したがって上記のような実行結果になります。

*p++

実行結果
1
10

さあ、タイトル回収しました。
これは結局、先ほどの*(p++)と代わりありません。
このことから、後置インクリメントはアスタリスクの評価よりも優先順位が高いということが分かる気がします。
実際、後置インクリメントの演算子優先順位はかなり上位にいます。

と、ここで終わってしまうのはなんかもったいないのでいろいろ試してみましょう。

*++p

実行結果
10
10

これは*(++p)と同じですね。

++*p

実行結果
2
2

中身表示からのインクリメントなのでこうなります。

そろそろ飽きてきたので、次は、インクリメントを複数使ってみましょう。

*(p++++)

実行結果
Main.c:6:24: error: expression is not assignable
    printf("%d\n",*(p++++));
                  ~~~^
1 error generated.

p#にならないんですか?!
後置インクリメントはどうやら2回連続では使えないようです。

*(++++p)

実行結果
Main.c:6:21: error: expression is not assignable
    printf("%d\n",*(++++p));
                  ^ ~~~
1 error generated.

またエラーです。どうやら前置インクリメントも2回連続で使えないようです。

*(++p++)

実行結果
Main.c:6:21: error: expression is not assignable
    printf("%d\n",++p++);
                  ^ ~~~
1 error generated.

うーん、インクリメント演算子はそもそも2回は使えないんでしょうか。

++*p++

実行結果
2
10

こいつ、動くぞ・・・!
1回目で先頭をインクリメントしたものが、2回目でiの2番目が出力されていますね・・・
++*Pが実行されたあと、p++が実行されたといった感じですかね・・・

++*(p++)

実行結果
2
10

これは、後置インクリメントの優先順位が高いことを思い出せば当たり前なのかもしれません。

(++*p)++

実行結果
Main.c:6:25: error: expression is not assignable
    printf("%d\n",(++*p)++);
                  ~~~~~~^
1 error generated.

なんなんでしょう。もういまいち意味が分かりません。
どうしたらエラーが起きるか突き止めてみましょう。

++*++p

実行結果
11
11

括弧で分けるなら、++(*(++p))でしょうか。
ポインタインクリメント、中身、中身インクリメントって感じですかね。

*++p++

実行結果
Main.c:6:20: error: expression is not assignable
    printf("%d\n",*++p++);
                  ~~~~~~^
1 error generated.

エラーですね。
実行順を考えるなら、先にポインタ前置インクリメント、中身評価、次にポインタ後置インクリメントといったところでしょうか。

(*++p)++

実行結果
10
11

これはなんでしょう・・・
ポインタ前置インクリメント、中身評価、中身インクリメントで動くんでしょうか。
としたら、ポインタインクリメントと中身インクリメント一回ずつならエラーが起きない・・・?

調べてみた。

いろいろ調べた結果、ちゃんとしたソースが得られた訳ではありませんでしたが、
おそらく一度の式で変数の値を複数変更することはできないだそうです。

[2018年8月8日追記]
コメントで助言を頂いたので変更させていただきます。
今回のコンパイルエラーの原因は、インクリメントの仕様にありました。

そもそものお話をすると、C言語の式には種類があり、rvalue(右辺値)とlvalue(左辺値)の2つがあります。1
これはC++でよく出てくるような話らしいですが、C言語にもその概念があります。
lvalueは、式の左辺に出てくることが多いですが、基本的には変数やポインタのことを指します。
一方、rvalueは、数字とかアドレスとか、データそのものを指します。
lvalueはデータがある場所を知っているため代入ができ、rvalueは値そのものなのでどこにあるか分からず代入ができないといったところでしょうか(知識不足のため雑です)

そして、インクリメント演算子は、オペランドとしてlvalueを取る必要があり、その結果としてrvalueを返します。
したがって、p++++とか++++pは、rvalueに対してインクリメントをしていることになるので、そりゃあコンパイルエラーになるわけですよ。
一方、間接演算子(アスタリスク)はポインタ型をオペランドとし、lvalueを返します。
なのでインクリメントされても正しく処理されるわけです。

したがって、たとえば++*p++がうまくいったのは、

  1. p++が評価され、rvalueのポインタ型pが返る。
  2. *pが評価され、pの中身がlvalueとして返る。
  3. 前置インクリメント演算子により、pの中身がインクリメントされ、結果がrvalueとなる。
  4. その結果がprintfで出力される。
  5. 最初のp++によりpがインクリメントされる。

という流れになったからであると思われます。
加えて(++*p)++がうまくいかなかったのは、

  1. 後置インクリメント演算子により(++*p)が評価される。
  2. *pにより、pの中身がlvalueとなる。
  3. ++*pにより、pの中身がインクリメントされ、rvalueとして渡される。
  4. しかし、後置インクリメント演算子にrvalueが渡されることになるのでエラーになる。

ということだと思います。いや~奥が深いですね~

まとめ(2018年8月8日修正)

  • 後置インクリメントは比較的優先順位が高いが、一番最後に評価されるため、*p++は先頭出力→次のアドレスを指すといった流れになる。
  • C言語の言語仕様として、rvalueとlvalueがあり、インクリメントのオペランドはlvalueであり結果はrvalueとなるため、rvalueがオペランドにくると代入ができずエラーとなる。
  • つまりインクリメントを多用してわかりにくくする奴はグーで殴っていい

最後に

#include <stdio.h>
int main(){
   int i[3] = {1,10,100};
   int *p = i;

   printf("%d\n", (*p++)++);
   printf("%d\n", *p);
   printf("%d\n", *(--p));

   return 0;
}

これがどう実行されるか今のあなたには分かりますか?
(これは未定義ではないのか...)

60
41
4

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
60
41