C言語ワークショップ 第6回  演算子

●はじめに

 ここまでのところで、いくつか演算子というものが出てきました。この回では演算子についてより深く解説を行いたいと思います。

●演算と演算子

 演算とはある決まった組み合わせによって作用し、新しい物を生成する定義された動作で、演算子はその演算動作を指定するものと説明されます。
 これは一番身近な演算子である"+"で考えると、この足すという動作は2つの値を合わせ(作用)、そしてその結果(新しい物の生成)をおこなう演算であり、その演算子(演算指定)は"+"です。
 さて、そのほか、数学でも使われるの簡単な演算子が利用できます。
 
四則演算子(2項演算子、算術演算子)
演算子名形式演算または演算結果
乗算op1 * op2op1とop2の積
除算op1 / op2op1とop2の割った商
剰余op1 % op2op1とop2の割った余り
加算op1 + op2op1とop2の和
ポインタを進める
減算op1 - op2op1とop2の差
ポインタを戻す
ポインタの差

 opの部分はオペランドといい、日本語にすれば項とか演算対象とかと言います。数学で移項って呼ぶあの項です。この演算子の場合2つのオペランドをつかいますので、2項演算と呼びます。
 演算子を覚える場合、記号だけが演算子と考えるより、オペランドも含めて覚える方が良いと思います。+だけではなく+の両サイドにくっついているオペランドがあって初めて演算出来るわけであり、+と2つのオペランドで初めて上の四則演算になるわけです。
 そして、加算、減算に関してはポインタにも適用されます。たとえば

 int *c;
 c+1;
 1+c;

 とした場合op1がポインタcでop2が数値(逆でもいいです)でポインタを進める動作をします。この場合はcに入っているアドレス値にint型で1つ進んだアドレスを演算する動作(PCの場合実際のアドレス値が+2)をします。

 減算も同じですがop1がポインタcでop2が数値であり、入れ替えは出来ません。

 int *c;
 c-1;

 ならばint型で1つ戻すアドレスを演算する動作(PCの場合実際のアドレス値が−2)です。

 しかし 1-c は×です。ただし -1+cなら(-1)+cとなり減算記号ではなく負の記号の−なので当然OKです。普通の計算でも 10+5と5+10は同じ15という結果を得られますが、減算の場合10−5と5−10では結果が違います。減算ではオペレーションの位置も意味をもってますから、数値(数)からポインタを引くという動作は意味不明だからです。

 減算にはもう一つ

 int *c;
 int *d;
 c-d;

 といったようにポインタ同士の減算をしてその差の演算が可能です。2つのopがポインタですので問題なく引くことが出来るわけです。

次に他の2項演算子を紹介します。  
2項演算子
演算子名形式演算または演算結果
ビットシフトop1 << op2
op1 >> op2
op1をop2ビット左へシフトした値
op1をop2ビット右へシフトした値
比較op1 < op2
op1 <= op2
op1 > op2
op1 >= op2
op1 == op2
op1 != op2
真1、偽0
単純代入op1 = op2代入後のop1の値
[副作用]op2をop1へ代入
キャスト(op1)op2op1(型)にop2を型変換した値
配列添字op1[op2]配列(ポインタ)op1の値op2で示した要素の値
(*(op1+(op2)))を返す

<ビットシフト>

 ビットシフト演算子は2進でのビットを右または左にシフトする演算です。

 char bit;
 bit=1;     (bitの値 :00000001 = 1)
 bit << 1;   (結果   :00000010 = 2)
 bit << 2;   (結果   :00000100 = 4)

 bit=4;     (bitの値 :00000100 = 4)
 bit >> 1;   (結果   :00000010 = 2)

 この様にビットシフトは2で乗除した値になります(2進数なので当然ですね。10進数で桁を左シフトしたら10倍になりますから)

<比較演算子>

 次に比較演算子ですが文の説明のところで比較演算子と等価演算子が出てきましたので解説は不要だと思います。この演算子は数値が結果になりますがその数値は真1、偽0の2つです。数値が得られるわけですからそのまま利用できますので条件式以外でも

 10*(a==b);

 のように使えばaとbが等しい場合に10という値が得られるといった使い方も出来ます。

<単純代入演算子>

 次に単純代入です。ちょっとややこしい部分がありますが上の表の演算または演算結果で、代入後のop1の値と書いてあります。これは

 a=b;

 と書いたときこのa=b自体がなにを示すのかというとaにbを代入した後のa値だというわけです。

 kekka = (a=b);

 と書けばa=bという単純代入の結果(代入した後のa値)がkekkaに得られると見ることができます。(当然kekka = (a=b)全体も値を示すわけですが・・・)
 そしてその副作用としてaにbが代入されるというちょっと複雑な解釈になります。プログラム中で

 a=b;

 と書いて、普通は副作用である代入のみをおこなってそれ自体の値を扱うことは少ないのですが

 switch(a=getkey()){ }

 のようにaに代入された後の値がswith文に使われ、aには関数getkey()(キーボードの入力を取得)を代入といった解釈での使い方があることを知っておくことが重要なのです。

<キャスト演算子>

 さて、次はキャスト演算子です。実は前の回でも何度か出てきていますが、これはop1で指定した型にop2を変換するための物です。キャストとは型変換という意味です。正確にば明示的型変換といいます。
 たとえば

 (char)x;

 はxという変数の値をchar型に変換した物が得られます。
 実際には

 double a;
 int b=3;
 a=3/10;

 という演算を行うとaにはどんな値が入るのかを見てみます。まず3/10というわり算を行いますが数値の型はintです。この場合 int 割る int の答えはint型(整数)になってしまいますので、本当の演算結果は0.3だけれど、整数の0になってしまい、aに代入される値は0です。int型同士の演算なので答えもそのままintで出してしまうのです。ですから答えを小数にしたければ

 a=(double)3/10;

 のようにint型の3を一度double型に変換します。こうすると、3/10は double 割る int となり、決まり上、int型の10を自動的にdoubleへ変換して(暗黙的変換、黙示的変換といいます)、double型で0.3という答えを出してくれるわけです。

 また、よく利用する方法で

 char *pt;
 pt=(char *)0x2000;

 というようにポインタ変数へアドレスを直接代入します。 この場合では0x2000という部分はただの数値(型で見ればint型の定数)です。これをポインタ変数へ代入するために明示的に(char *)でchar型ポインタに変換して代入しています。
 これは単純代入(op1=op2)の2つの項がどちらも数値(算術型)、もしくは、どちらもポインタ型でなくてはいけないという決まりがあるため、その決まりに則って代入する場合には数値である0x2000を(char *)0x2000というようにポインタに置き換えることで代入できるわけです。

 処理系によりキャストしなくても代入出来る物(エラーにならないコンパイラ)があります。このことはポインタの回の説明ですこし説明しましたが、実際のプログラムに置いてはキャストするようにしないと安全とは言えません。

 さて、これを応用すれば

 (int *)2000 - c;

 で見た感じ数値からポインタを引けます。ただし数値といってもアドレスを示す数値(=ポインタ)に(int *)をつけてint型のポインタですよってしているからです。結局ポインタ同士の差を出すわけです。

<配列添字演算子>

 配列添字?どういうこと?って思うかもしれません。配列の説明では参照のやり方を単純に

 配列名[n] や  a[2];

 といった感じで単純な説明をしました。これも実際には演算子なのです。
 考え方としては

 op1[op2]

 の、op1はポインタ型変数名。それに要素を表す整数のオペランドop2が[op2]という演算子として引っ付いた物です。ですから配列の本来の概念はこちらです。あくまでポインタの参照方法としてこの演算子があるのです。
 ですから当然

 char a,b;
 char *pt;

 pt=&AMP;a;

 pt[0]=10;

 といったように最初から配列で宣言せず、ポインタ変数を宣言しておき、それに配列添字を付けて参照が出来るわけです。結局、”配列の[n]の添字が無い場合はポインタになりますよ”と説明してきたのはこれが理由であり、実際のところはポインタに[n]を付ければ配列として参照できますってのが本来の概念です。

 ここまで見てきた演算子の場合は記号ばかりでしたが、変わった物で

 sizeof op ; または sizeof(op);

 というサイズオブ演算子というのがあります。これも演算子で動作はopで指定した物のサイズ(メモリの占有サイズ)を求めるという物です。たとえは

 size=sizeof(int);

 としたらこれは、int型自体の大きさを求め、PC系の場合だと、intは2バイトのメモリを使う型なので2というサイズの値が変数sizeに代入されます。また、

 int c;
 size=sizeof(c);

 のように、型指定子だけでなく、変数自体を表記しても一緒で、opで指定した物のサイズを演算してくれます。

実際に型のサイズを調べてみよう!

 char a;
 a=sizeof(a);
 printf("char: %d\n",a);
 a=sizeof(int);
 printf("int: %d\n",a);
 a=sizeof(long);
 printf("long: %d\n",a);


●演算子の優先順位と結合規則

 ここでは、プログラム中において、演算子を使った式を作成した場合に、その式がどのような順番で実行されるかを考えます。

 C言語において式の評価される順番は

 1.演算子の優先順位
 2.演算子の結合規則

 によって決定されます。

 まず、次の式を見てください。

 d = a * b + c;

 これは、一番シンプルかつ誰でも分かると思う式ですね。

 普通の数学と同じく優先順位は + < * となります。
 数学での「加減算より剰余算の方を先にする」という決まりと同じく、C言語においても同じ優先順位であり、剰余算の方が優先順位が高くなります。当然優先順位が高ければ、先にその式が評価されるわけです。

 これが演算子の優先順位です。

 では次の式を見てみましょう。

 a <= b >= c > d

 ぱっと見てなんとも見にくい感じがしますが、この場合はどうでしょうか。 C言語において比較演算子の優先順位は全く同じ優先順位となります。ですので上の式だと、どの演算子も優先順位が同じになってしまいます。このような場合には、順番を決定づける2つ目の「演算子の結合規則」が有効になります。

比較、等価演算子の結合規則は「左から右」と決められています。ですのでこの式は

 (((a <= b) >= c) > d)

 のように左側から右側へ順次評価されていきます。この「左から右」、「右から左」の順番のことを結合規則と呼びます。

 このように、すべての演算子には優先順位と結合規則を持っています。

優先順位と結合規則一覧
優先順位演算子結合法則
プライマリop1(op2) (関数呼び出し)
op1[op2] (配列添字)
op1.op2、op1->op2 (メンバー参照)
op1++、op1--(後置増分減分)
左から右(→)
単項演算子++op1、--op1(前置増分減分)
&op1 (アドレス演算)
sizeof op1 (記憶量演算)
+op1、-op1 (正負号)
!op1、~op1 (否定、補数(ビット反転)演算)
右から左(←)
キャスト演算子(op1)op2 
算術演算子op1*op2、op1/op2、op1%op2左から右(→)
op1+op2、op1-op2
シフト演算子op1<<op2、op1>>op2
比較演算子op1<op2、op1<=op2、op1>op2、op1>=op2
等価演算子op1==op2、op1!=op2
ビットANDop1&op2
ビットXOR10op1^op2
ビットOR11op1|op2
論理AND12op1&&op2
論理OR13op1||op2
3項演算子14op1 ? op2 : op3右から左(←)
代入演算子15op1=op2 (単純代入)
op1+=op2、op1-=op2(加算減算代入)
op1*=op2、op1/=op2(乗算除算代入)
op1%=op2 (剰余代入)
op1<<=op2、op1>>=op2 (シフト代入)
op1&=op2、op1^=op2、op1|=op2 (ビット演算代入)
順序演算子16op1 , op2左から右(→)