(準備中)
Cのインクリメント、デクリメント、*(ポインタの間接参照演算子)、代入が混じった式、例えば
k=*++n; のような式は、どういう結果になるのかわかりにくいです。
どうもプログラムが思ったような動作をしないという場合は、このあたりに問題が潜んでいるケースも
結構あります。
ここでは演算の優先順位のわかりにくいものをピックアップして、どういう挙動になるのか確認していきます。
中にはCの標準規格で”未定義の動作”とされている式も取り上げていますが、それらは処理系依存であり、
なるだけ書いてはいけない式と言われていますのでご注意ください。
Cの演算子の基本的な事項は「Cの演算子」に書いていますので、ご参照下さい。
Cの演算の優先順位と結合規則のまとめです。
優先順位 | 演算子 | 結合規則 |
高 | ( ) [ ] -> .(構造体、共用体のメンバの指定) ++ --(後置) | −> |
! ++ --(前置) *(間接演算子) &(アドレス演算子) sizeof | <− | |
*(かけ算) / % | −> | |
+ - | −> | |
<< >> | −> | |
< <= > >= (比較の関係演算子) | −> | |
== != | −> | |
& | −> | |
^ | −> | |
| | −> | |
&& | −> | |
|| | −> | |
? : (条件演算子) | <− | |
= += などの代入演算子 | <− | |
低 | , | −> |
なんとなく、加算・減算よりビット演算子やシフト演算子のほうが優先順位が高いような気がしませんか。
しかし、実際は、加算・減算>シフト演算>ビット演算の順になります。
@ i=10+5<<2;
加算が最初に行われるので、i=15<<2 と同値です。
シフト演算のところを2進数で書くと以下の通りで、2ビット左シフトで15は60になります。
15 00001111 15<<2 00111100 ->60
A i=15&10+5<<2;
ビット演算が混じってきた式です。
加算が一番優先順位が高いので、i=15&15<<2 と同値です。
ビット演算よりシフト演算が先に行われるので、この式は i=15&60 となります。
以下2進数で書くと以下の通りで、12になります。
15 00001111 60 00111100 ------------- AND 00001100 ->12
<operator0.c>
#include <stdio.h> int main(void){ int m,n,i,b,a[3]={1,5,10}; m=3; n=++m; printf("m=%d: n=%d \n", m, n); m=3; n=m++; printf("m=%d: n=%d \n", m, n); i=1; b=a[++i]; printf("b=a[++i]; b=%d: i=%d\n",b,i); i=1;b=a[i++]; printf("b=a[i++]; b=%d: i=%d\n",b,i); return 0; }
@m=3; n=++m;
の場合は、前置のインクリメントなので、最初に加算が行われm=4の結果がnに代入されます。
その結果、mもnも4になります。
Am=3; n=m++;
の場合は後置形ののインクリメントなので、まず3がnに代入され、それからmが加算されます。
その結果、m=4ですが、nは3になります。
Bi=1; b=a[++i];
の場合、前置のインクリメントでi=2となり、bにはa[2]の値、すなわち10が代入されます。
Ci=1; b=a[i++];
の場合は、まずa[1]の値がbに代入され、それからiの値がインクリメントされます。
その結果、i=2になりますが、bの値はa[1]の5になります。
このようにインクリメント、デクリメントの後置の場合の挙動には注意が必要です。
実行例(Borland C++55でコンパイル)
ポインタ間接参照演算子(間接演算子)とインクリメント・デクリメントが組み合わさったパターンは
さらにわかりぬくいです。まず後置形のインクリメント・デクリメントの例を試してみます。
<operator.c>
#include <stdio.h> int main(void){ int i,j,k,m, *n, a[3]={1,5,10}; n=a; printf("*n=%d: n=%d \n", *n, n); n=a; i=*n+1; /* @ */ printf("i=*n+1; i=%d: n=%d \n", i, n); n=a; j=*n++; /* A */ printf("j=*n++; j=%d: n=%d \n", j, n); printf("*n=%d: n=%d \n", *n, n); /* B */ n=a; m=(*n)++; /* C */ printf("m=(*n)++; m=%d: n=%d \n",m,n); printf("*n=%d: n=%d\n",*n,n); return 0; }
@ i=*n+1;
*(間接演算子)の優先順位が一番高いので、まず*n=a[0]=1が確定します。
それから加算が行われ i=1+1=2 となります。
A j=*n++; これがわかりにくいし、思わぬバグを生みやすいパターンです。
後置のインクリメントが一番優先順位が高いわけですが、まずnには現在のa[0]のアドレスがつかわれて
*nの演算が行われます。その結果j=*n=a[0]=1 ということになります。
その後nはがインクリメントされ、nにはa[1]のアドレスが入ることになります。
*n++ は*(n++) と同等になります。
B 前のAのステップの結果、nはa[1]のアドレスを指していますので、ここでは*n=a[1]=5になります。
C m=(*n)++;
( )の優先順位が一番高いので、まず*nが演算されます。*n=a[0]=1 です。
次にその値(=1)がインクリメントされるるので、m=1+1=2 となりまます。
実行例(Borland C++55でコンパイル)
次は前置形のインクリメント・デクリメントを含んだやや複雑な式をみてみます。
<operator2.c>
#include <stdio.h> int main(void){ int i,j,k,m, *n, a[3]={1,5,10}; n=a; printf("*n=%d: n=%d \n", *n, n); n=a; i=++*n; printf("i=++*n; i=%d: n=%d \n", i, n); /* @ */ printf("a[0]=%d\n",a[0]); /* A */ n=a; j=*++n; printf("j=*++n; j=%d: n=%d \n", j, n); /* B */ n=a; k=++*++n-3; printf("k=++*++n-3; k=%d: n=%d \n", k, n); /* C */ printf("a[1]=%d\n",a[1]); /* D */ n=a; m=++*n++; printf("m=++*n++; m=%d: n=%d\n", m,n); /* E */ printf("a[0]=%d\n",a[0]); /* F */ return 0; }
@ i=++*n;
前置のインクリメントとポインタの間接演算子の優先順位は同じで、結合規則は右から左へなので
最初に*n=a[0]=1 という演算がなされます。
次にその値がインクリメントされ、i=1+1=2 ということになります。
nの値はa[0]のアドレスを指したままです。
A ここでa[0]=2になることに留意してください。(後のEの演算の時にこの値が使われます)
B j=*++n;
前置のインクリメントとポインタの間接演算子の優先順位は同じで、結合規則は右から左へなので、
最初にnのインクリメントがされます。nはa[1]のアドレスを指すことになります。
その後に間接演算子の演算がされ、jにはa[1]=5 が代入されます。
これは、
n=n+1;
j=*n;
という計算と同値です。
C k= ++*++n-3;
こういう式を書く人はまずいないと思いますが、これは何をやっているかわからないですね。
前置のインクリメントとポインタの間接演算子の優先順位は減算より上で(かつ同じで)、結合規則は右から左へなので、
++(*++n)-3;と同値です。
まず、*++n ですが、これは上記Bで見た通り、まずnの値がインクリメントされ、nはa[1]のアドレスを指すことになります。
間接演算子でa[1]の値(=5)が取り出され、
その値が更に前置のインクリメントでインクリメントされて6になります。
その値から3を引くので、k=6-3=3ということになります。
これは、
n=n+1;
*n=*n+1;
k=*n-3;
という計算をしたのと同じです。2
D ここでa[1]=6になっていることに留意してください。
E m= ++*n++;
これはかなりわかりにくい式です。
後置形のインクリメントの優先順位が一番高いので、
++(*(n++)) と書いたのと同値です。
まず後置のインクリメントの演算ですが(++*n++ の赤色の部分の演算)、現在の値が演算に使われるので、a[0]の値が演算に使われます。
(Aでa[0]=2 になっていることに注意してください)
そのa[0]の値が間接演算子で使われるので*n=2になります。それから後置のインクリメントが行われて、nはa[1]の
アドレスを指すことになります。
それから前置のインクリメント(++*n++ の赤色の部分)の演算が行われますが、これは*n(=2) のインクリメントなので、
mには3が代入されることになります。
実施例
更に複雑な例をみてみます。
<operator6.c>
#include <stdio.h> int main(void){ int p,q,*n, a[3]={1,5,10}; n=a; printf("*n=%d: n=%d \n", *n, n); p= *n++; /* @ */ q=p + ++*n; /* A */ printf("p=%d: q=%d: *n=%d: n=%d \n",p,q, *n,n); printf("a[0]=%d: a[1]=%d, a[2]=%d",a[0],a[1],a[2]); return 0; }
@ p= *n++;
これは<operator.c>のAで見た形です。p=a[0]=1 です。 nはインクリメントされて1になります。
A q= p + ++*n;
p=1です。
++*nは<operator2.c>の@で見た形です。 n=1になっているので、*n=a[1]=5
*n(=5)がインクリメントされるので、++*n=6 になります。
よって q=1+6=7 になります。
実行例
以降、もう少しわかりにくい式が登場しますが、中にはCの標準規格で未定義の動作と
されているものも含まれます。一応、手持ちのコンパイラではコンパイルできていますが、
未定義の動作を含む式は、コンパイラによって結果が異なる可能性があるので、書くべきでは
ないとされています。
ここでは未定義の動作を含む式も参考までにあげてあります。
未定義の動作としては、Cの標準規格に例えば以下のものがあげられます。
・2つの副作用完了点の間で、式の評価によって1つのオブジェクトが2回以上変更された場合。
・あるいは、変更前の値の読取りが、格納される値を決定するため以外の目的で使用される場合。
何を言っているのかわかりにくいのですが、例えば
i=1;
a=i=i++;
という式にはi=i++の部分に未定義の動作があります。
i=i++ の右辺の部分は後置形インクリメントなので、現在のiの値である1が右辺のiに代入された後、
インクリメントされi=2になります。iの値が2回以上変更されているので上記の未定義の動作に該当します。
なお、後置インクリメントのインクリメントは、現在の値を渡した後に行われると定義されていますが、
インクリメントがいつ行われるのかは定義されていないようです。
Borland C++55でコンパイルすると、上記の式は、a=1, i=2という結果になります。
つまりa=i という代入が行われた後にインクリメントされたことになります。
しかし、a=iの代入が行われる前にインクリメントされると、a=2になります。
未定義の動作は、このように演算結果が変わる可能性があるということになります。
i=(i=3)+1; という式もiの値が2回以上変更されているので未定義の動作になるようです。
<operator3.c>
#include <stdio.h> int main(void){ int p,q,*n, a[3]={1,5,10}; n=a; printf("*n=%d: n=%d \n", *n, n); p= *n++ + ++*n; /* @ */ printf("p=%d: *n=%d: n=%d \n", p, *n,n); printf("a[0]=%d: a[1]=%d, a[2]=%d",a[0],a[1],a[2]); return 0; }
@ p= *n++ + ++*n; (※)この式は未定義の動作を含みます。
この式も、<operator6.c>と同じく、*n++と++*nを足しています。
そうするとpの値は7になりそうですが実行例を見るとp=4になっています。
同じ*n++と++*nの足し算ですが、こちらはひとつの式にまとめていますので、何か異なる
動作があったようです。
以下の実行結果からは筆者は以下のように推察しています。
(A) まず、*n++が実行されますが、その結果*n=a[0]となります。この段階では後置のインクリメントはまだ保留されています。
(B) 次に、演算子の優先順位としては++*nが実行されます。 n=0の状態なので、*n=a[0]=1です。これがインクリメントされて
++*n=2になります。この段階でa[0]も2になります。
(C)最初の*n++の値もa[0]なので、最初の*n++の値も2となります。
(D)結果、p=a[0]+a[0]=2+2=4 となったようです。
(E)最後に、最初の*n++の後置のインクリメントが実行され、n=1になります。
注意すべき点は、(A)の段階では*n=a[0](=1) なのですが、値は1に確定しておらず、(C)の結果によって
2になった点です。ひとつの式の演算の途中で、変数の値がどのように扱われいるのかは逆アセして機械語にでも
しないとわかりませんが、上記のような処理が行われていると推察します。
下記の実行例はBorland C++55でコンパイルしたケースですが、Vusual Studio2022のCでも同様の結果なので、
Borland C++55固有の動作ではないと思います。
実行例(Borland C++55でコンパイル)
次は左辺にも間接演算子とインクリメントがある例です。
だんだんややこしくなってきました。
左辺の変数のアドレスは確定していないといけないので、左辺に以下のような式(j-1)を書くと
コンパイル時にエラーが出ますが、単項演算子は使えるようです。
j -1 = i++;
<operator7.c>
#include <stdio.h> int main(void){ int p,q,*n, a[3]={1,5,10}; n=a; printf("*n=%d: n=%d \n", *n, n); *++n= ++*n; /* @ */ printf("*n=%d: n=%d \n", *n,n); printf("a[0]=%d: a[1]=%d, a[2]=%d",a[0],a[1],a[2]); return 0; }
@ *++n = ++*n; (※)この式は未定義の動作を含む式になると思います。
最初に左辺の*++nが実行されます。その結果、n=1となり nはa[1]のアドレスを指すことになり、
演算の結果はa[1]の値として保存されることになります。
次に右辺の++*nが実行されます。n=1なので、*n=a[1]=5です。この結果がインクリメントされ、++*nの値は6と
なります。a[1]も6になります。
最後に6が左辺に代入されます。
ここまでのところを集大成したのが次のoperand4.cです。
実際に、 *++n= *n++ + ++*n; という意味不明な式が使われる局面はないと思いますが、
この式はコンパイルできます。この式の動作を見ると、間接演算子、後置形のインクリメンット・デクリメントの
動作が正確に理解できそうです。
<operator4.c>
#include <stdio.h> int main(void){ int *n, a[3]={1,5,10}; n=a; printf("*n=%d: n=%d \n", *n, n); *++n= *n++ + ++*n; printf("*n=%d: n=%d \n", *n,n); printf("a[0]=%d: a[1]=%d, a[2]=%d",a[0],a[1],a[2]); return 0; }
@ *++n= *n++ + ++*n; (※)この式は未定義の動作を含みます。
式としてはoperator3.cのp= *n++ + ++*n; に似ていますが、結果は異なります。
実行結果から推察すると以下のような動作が行われたと思われます。
(A)左辺の*++n nはインクリメントされn=1になるので、左辺はa[1]に値を保持することになります。
(B)右辺の最初の項*n++は、後置のインクリメントなので、*n=a[1]
(C)第2項++*n *n=a[1]=5で、この値がインクリメントされて6になる。a[1]=6
(D)右辺の最初の項の値はa[1]の値なので、最初の項の値も6になる。
(E) 6 + 6=12が左辺に代入された結果として、a[1]=12になる。
(F) 右辺の最初の項の後置インクリメントが実行されn=2になり、nはa[2]のアドレスを指す。
ということで以下の実行例のようになります。
実行例(Borland C++55でコンパイル)
更に右辺に代入演算子や条件演算子が混じって来た場合の動作について検証してみます。
<operator10.c>
#include <stdio.h> int main(void){ int a,p; a=1; p= a++ +(a=5); /* @ */ printf("a=%d: p=%d\n", a,p); a=1; p = ++a + (a=5); /* A */ printf("a=%d: p=%d\n", a,p); a=1; p =(a=5) + a++; /* B */ printf("a=%d: p=%d\n", a,p); a=1; p =(a=5) + (++a); /* C */ printf("a=%d: p=%d\n", a,p); a=1; p =((a==1)? a++ : 3) + (a=5); /* D */ printf("a=%d: p=%d\n", a,p); a=1; p =((a==1)? ++a : 3) + (a=5); /* E */ printf("a=%d: p=%d\n", a,p); return 0; }
@ p= a++ +(a=5); (※)この式は未定義の動作を含みます。
(A) まず第1項のa++が実行される。後置形のインクリメントであるので、暫定的にaの値は1となる。
(B) 次に第2項のa=5が実行され、aの値は5となる。
(C) aの値が5となったことにより、(A)の段階では第1項のaの値は暫定的に1であったが、このステップで
第1項の値は(aには5が格納されたので)5に変更となる。
(D) 第1項と第2項の加算が行われ、p = 5 + 5 =10 となる。
(E) 最後に第1項の後置のインクリメントが実行され、a=6になる。
演算結果から(C),(D),(E)の処理を機械語レベルで推察してみる。
変数aの値を保存するメモリが100番地,
変数pの値を入れるメモリの番地は200番地として、このメモリの値を[100],[200]と書き表わすとすると、
8086アセンブラ風に書くと、以下のような処理になっていると思われる。
MOV [100], 5 '最終的にa=5になる。
MOV AX, [100]
ADD AX, [100]
MOV [200], AX '結果をpに保存する
INC [100] 'aはインクリメントする
A p = ++a + (a=5); (※)この式は未定義の動作を含みます。
(A) 第1項++aは前置のインクリメントなので、この段階では暫定的にaの値としては2になる。
(B) 次に第2項のa=5が実行され、aの値は5となる。
(C) aの値が5となったことにより、(A)の段階では第1項のaの値は暫定的に1であったが、このステップで
第1項の値は(aには5が格納されたので)5に変更となる。
(D) 第1項と第2項の加算が行われ、p = 5 + 5 =10 となる。
B p =(a=5) + a++; (※)この式は未定義の動作を含みます。
(A)第1項でaの値は5となる。
(B)第2項は後置インクリメントなので、aの値としては5が演算に使われる。
(C) p=5+5=10 となる。
(D)後置インクリメントが実行されてa=6となる。
C p =(a=5) + (++a); (※)この式は未定義の動作を含みます。
この式はBorland C++55でコンパイルした時と、Visual Studio2022のVCでコンパイルしたときの結果が
異なります。このような式の評価の順序はコンパイラによって異なるようです。
(a=5)の評価と(++a)の評価がどちらが先に行われたのかの違いによるようです。
Borland C++55は (++a)を先に評価し、(a=5)が後に評価されたようです。この結果aには5が入り、
p=10という結果になっています。
これに対しVCは先に(a=5)を評価し、(++a)を後で評価したようです。この結果、aには6が入り、
p=12という結果になっています。
このように、どの項から評価するかというのは定まっていないようで、cの標準規格では「未規定の動作」と
書かれています。(「不定」というケースもあるようです)
D p =((a==1)? a++ : 3) + (a=5);
(A) 条件演算子の式が評価され、a==1なので、結果はa++ になります。
a++は後置インクリメントなのでaの値は1となります。
(B) 第2項のa=5が評価され、aには5が入ります。
(C) p= 1 + 5 =6 となります。
ここで、注意すべき点は、@との違いです。
(A)のステップの結果として、1が確定し、即値(定数)として使われるようです。
なので、(A)の評価の後は、p = 1 + (a=5); という式と同値になっています。
@の p = a++ + (a=5); の結果とことなるのはこのためです。
条件演算子の式の評価の結果として変数a++がリターンされて使われるのではなく、a++
の演算結果の1がリターンされて使われています。
a++の後置のインクリメントの演算も条件演算子の式の最後で行われたものと推察されます。
インクリメントしてa=2となったものの、その後、第2項の(a=5)の結果として、a=5となっているものと
思います。そのため、@の式と結果が異なっています。
後置のインクリメント、デクリメントは条件演算子の式の評価の最後で行われたということです。
条件演算子の終わりは副作用完了点になっており、この時点で変数の値が確定します。
なので@の式と違い、この式は未定義の動作にはなりません。
E p =((a==1)? ++a : 3) + (a=5);
(A) 条件演算子の式が評価され、a==1なので、++aが実行され、2という結果が返ります。
(B) 第2項のa=5が評価され、aには5が入ります。
(C) p= 2 + 5 =6 となります。
実行例(Borland C++55)
実行例(Visual studio2022 VC)
<operator8.c>
#include <stdio.h> int main(void){ int p,a,b; a=5; b=5; p = (a==b)? a++ : ++b; /* @ */ printf("p=%d: a=%d: b=%d\n",p,a,b); a=5; b=1; p = (a==b)? a++ : ++b; /* A */ printf("p=%d: a=%d: b=%d\n",p,a,b); a=5;b=5; p = ((a==b)? a++ : ++b) + (a = b++); /* B */ printf("p=%d: a=%d: b=%d\n",p,a,b); a=5;b=5; p = ((a==b)? ++a : ++b) + (a = b++); /* C */ printf("p=%d: a=%d: b=%d\n",p,a,b); a=5;b=1; p = ((a==b)? a++ : ++b) + (a = b++); /* D */ printf("p=%d: a=%d: b=%d\n",p,a,b); return 0; }
@ p = (a==b)? a++ : ++b;
a==b なので、a++が実行される。後置インクリメントなので、5が値として使われp=6。
最後に後置インクリメントが実行されa=6となる。
A p = (a==b)? a++ : ++b;
a != b なので、++bが実行される。前置インクリメントなので、b=2となり、P=2
B p = ((a==b)? a++ : ++b) + (a = b++);
条件演算子の式では、a==bなのでa++が実行される。後置インクリメントなので5が値として返される。
第2項a= b++ では後置インクリメントなのでbの現在の値である5がaに代入され、p=5+5=10 となる。
最後にb++の後置インクリメントが実行されb=6になる。
C p = ((a==b)? ++a : ++b) + (a = b++);
条件演算子の式では、a==bなので++aが実行される。前置インクリメントなので6が値として返される。
第2項a= b++ では後置インクリメントなのでbの現在の値である5がaに代入され、p=6+5=11 となる。
最後にb++の後置インクリメントが実行されb=6になる。
Dp = ((a==b)? a++ : ++b) + (a = b++);
条件演算子の式では、a!=bなので++bが実行される。前置インクリメントなので2が値として返される。
第2項a= b++ では後置インクリメントなのでbの現在の値である2がaに代入され、p=2+2=4 となる。
最後にb++の後置インクリメントが実行されb=3になる。
更に間接演算子も混じっている例
<operator9.c>
include <stdio.h> int main(void){ int p,a[3]={1,5,10},b,*n; n=a; b=1; printf("n=%d\n",n); p = (*n==b)? *n++ : ++b; /* @ */ printf("p=%d: *n=%d: n=%d: b=%d\n",p,*n,n,b); printf("a[0]=%d: a[1]=%d: a[2]=%d\n",a[0],a[1],a[2]); b=100; p = (*n==b)? *n++ : ++b; /* A */ printf("p=%d: *n=%d: n=%d: b=%d\n",p,*n,n,b); printf("a[0]=%d: a[1]=%d: a[2]=%d\n",a[0],a[1],a[2]); b=5; p = ((*n==b)? *n++ : ++b) + (*n = b++); /* B */ printf("p=%d: *n=%d: n=%d: b=%d\n",p,*n,n,b); printf("a[0]=%d: a[1]=%d: a[2]=%d\n",a[0],a[1],a[2]); b=100; p = ((*n==b)? *n++ : ++b) + (*n = b++); /* C */ printf("p=%d: *n=%d: n=%d: b=%d\n",p,*n,n,b); printf("a[0]=%d: a[1]=%d: a[2]=%d\n",a[0],a[1],a[2]); return 0; }
@ p = (*n==b)? *n++ : ++b;
(A) 条件演算子の式では、*n==b なので*n++が実行される。
後置インクリメントなので、*nは現在の値である1を返す。よってp=1
(B) 後置インクリメントで、nはa[1]のアドレスを指すことになるので、*n=a[1]=5になる。
Ap = (*n==b)? *n++ : ++b;
(A) 条件演算子の式では、*n!=b なので++bが実行される。
前置インクリメントなので、b=101となり、よってp=101
Bp = ((*n==b)? *n++ : ++b) + (*n = b++);
(A) 条件演算子の式部分。@の実行後*n=5になっているので、*n==b。よって*n++が実行される。
後置インクリメントなので、*nは現在の値の5が返される。よって条件演算子の式の結果として5が得られる。
それから、後置インクリメントが実行され、nはa[2]のアドレスを指すことになる。
(B) 第2項の*n= b++ の評価に移る。 b++は後置インクリメントなので、現在のbの値の5が使われ、*nには5が
代入される。(A)の過程でnはa[2]のアドレスを指しているので、*n=a[2]=5となる。
(C) p=5+5=10
(D) b++の後置インクリメントにより、b=6になる。
Cp = ((*n==b)? *n++ : ++b) + (*n = b++);
(A) 条件演算子の式部分。*n!=b。よって++bが実行される。
前置インクリメントなので、++b=101が返される。よって条件演算子の式の結果として101が得られる。
(B) 第2項の*n= b++ の評価に移る。 b++は後置インクリメントなので、現在のbの値の101が使われ、*nには101が
代入される。Bの結果、nはa[2]のアドレスを指しているので、*n=a[2]=101となる。
(C) p=101+101=202
(D) b++の後置インクリメントにより、b=102になる。
実行例(Boralnd C++55でコンパイル)