演算子は C の動詞です。値を取り、新しい値を生み出します。全部で 40 個ほどありますが、大半のコードは十数個で足ります。そのうち 2 つは、独立した節を割く価値があるほど鋭い罠を抱えています。
算術演算子
(書式文字列中の %% はパーセント記号そのものを表示します。% ひとつでは書式指定子が始まってしまいます。)
2 項の算術演算子は + - * / % の 5 つ、それに符号反転の単項 - と、事実上何もしない + があります。
整数除算の罠
整数同士の a / b は整数除算を行い、余りを捨てます。丸めは行われません。結果はゼロ方向に切り捨てられます。
最後のケースが、本番環境まで生き延びるバグです。double に代入しても助けにはなりません。除算 total / count は代入が起こる前に既に完了しており、既に int だからです。対策は、たいていはキャストを使って、式の内側で片方のオペランドを浮動小数点にすることです。
関連するルールが 2 つあります。整数でゼロ除算するのは未定義動作で、たいていプログラムがクラッシュします。そして INT_MIN / -1 はオーバーフローします。
剰余演算子
% は余りを返し、整数にのみ使えます。
偶数判定の x % 2 == 0 と、配列の添字を巻き戻すための % n が、日常の 2 大用途です。
負の数では、C の % は被除数の符号に従います。
したがって x が負になりうるとき、x % 2 == 1 は奇数判定として壊れています。-7 % 2 は -1 だからです。代わりに x % 2 != 0 を使いましょう。
浮動小数点の余りに % を使うとコンパイルエラーです。math.h の fmod() を使ってください。
代入と複合代入
= は値を格納します。複合形は演算と格納を組み合わせます。
x += 5 は x = x + 5 を意味しますが、x を一度しか評価しません。対象が arr[compute_index()] のようなものであるときに、これが効いてきます。
代入自体も、代入した値を返す式です。だからこそ a = b = c = 0 が動き(右から左に代入されます)、= と == の取り違えが黙ってコンパイルされてしまうのです。
インクリメントとデクリメント
++ は 1 を足し、-- は 1 を引きます。それぞれに前置形と後置形があります。
単独の文としては 2 つは交換可能で、for のヘッダーで i++ が慣例なのは、伝統以上の深い理由はありません。
やってはいけないのは、ある式の中で変更される変数を、その式の中で 2 回以上使うことです。
int i = 5;
int x = i++ + i++; /* 未定義動作 */
arr[i] = i++; /* 未定義動作 */
printf("%d %d", i++, i);/* 未定義動作 */
C はこれらの部分式が評価される順序を定めていないので、結果が未規定であるにとどまらず、プログラム全体の動作が未定義になります。文を 2 つに分ければ、あいまいさは消えます。
比較演算子
6 つあり、どれも int を生み出します。真なら 1、偽なら 0 です。
結果が専用の真偽型ではなくただの int であることは C を特徴づける性質です。そこから何が導かれるか(= と == のバグを含めて)は C の真偽値 を参照してください。
比較の間違いのうち 2 つは C に特有です。数学が示唆するようには連鎖できません。if (1 < x < 10) は常に真になります。1 < x が 0 か 1 に評価され、それが 10 と比較されるからです。if (x > 1 && x < 10) と書きましょう。そして == で文字列を比較するのは、テキストではなくポインタの比較です。string.h の strcmp を使ってください。
論理演算子
&& は AND、|| は OR、! は NOT です。3 つとも、ゼロ以外の値はすべて真として扱います。
これらは短絡します。&& は片方のオペランドが偽になった時点で止まり、|| は片方が真になった時点で止まります。残りは決して評価されません。これは単なる最適化ではなく、安全のために頼ってよい保証です。
この 2 つの検査の順序を入れ替えると、NULL での呼び出しはヌルポインタを参照外ししてクラッシュします。先にガードし、それから使う。&& の連鎖における順序はロジックの一部です。
条件演算子
C で唯一の 3 項演算子は、2 つの値から選びます。
条件 ? 真のときの値 : 偽のときの値 です。式なので、if 文が置けない場所にも収まります。printf の引数の中や、初期化子の中などです。短く保ちましょう。条件演算子をネストさせると、あっという間に読めなくなります。
ビット演算子(簡単に)
これらは整数の個々のビットに対して働きます。
フラグ、マスク、ハードウェアレジスタに使われます。注意が 2 つ。& と | は && や || ではありません(短絡せず、ビットごとに働きます)。そして符号付きの値をシフトしたり、型の幅を超えてシフトしたりするのは未定義です。ビット操作には符号なし型を使いましょう。
優先順位
式にかっこがないとき、何が何に結び付くかを決めるのがこの表です。優先順位の高い順に並んでいます。
| レベル | 演算子 | 結合方向 |
|---|---|---|
| 1 | () [] -> . x++ x-- | 左から右 |
| 2 | ! ~ ++x --x +x -x *p &x sizeof (type) | 右から左 |
| 3 | * / % | 左から右 |
| 4 | + - | 左から右 |
| 5 | << >> | 左から右 |
| 6 | < <= > >= | 左から右 |
| 7 | == != | 左から右 |
| 8 | & | 左から右 |
| 9 | ^ | 左から右 |
| 10 | ビット単位の OR | 左から右 |
| 11 | && | 左から右 |
| 12 | 論理 OR | 左から右 |
| 13 | ?: | 右から左 |
| 14 | = += -= *= /= %= など | 右から左 |
| 15 | , | 左から右 |
実際に効いてくる帰結は次のとおりです。
a + b * c /* a + (b * c) の意味 */
a < b == c < d /* (a < b) == (c < d) の意味 - 意図どおりであることはまずない */
x & 1 == 0 /* x & (1 == 0)、つまり x & 0 の意味 - 典型的なバグ */
*p++ /* *(p++) の意味 - p を参照外ししてから、p を進める */
3 行目は覚える価値があります。== は & より強く結び付くので、ビットマスクの検査にはかっこが要ります。(x & 1) == 0 です。
15 段階すべてを覚えている人はいませんし、その必要もありません。* が + に勝つこと、比較が && に勝ち && が || に勝つこと、そしてそれ以外にはかっこを付けること。これだけ覚えましょう。
if ((flags & MASK) != 0 && (count > 0 || force)) { ... }
この行には冗長なかっこがありますが、あるほうが良いのです。
よくある質問
C の % は何をしますか?
% は剰余演算子で、整数除算の余りを返します。17 % 5 は 2 です。整数にしか使えません。float や double に使うとコンパイルエラーになり、浮動小数点版は math.h の fmod() です。
なぜ C では 5 / 2 が 2 になるのですか?
両方のオペランドが整数なので、C は整数除算を行い小数部分を捨てるからです。2.5 を得るには片方を浮動小数点値にします。5 / 2.0 とするか、(double)a / b のように片方をキャストしてください。
i++ と ++i の違いは何ですか?
どちらも i に 1 を加えます。++i(前置)は先にインクリメントして新しい値を返し、i++(後置)は古い値を返してから後でインクリメントします。単独の文としては同じです。違いが効くのは結果を使うときだけで、int b = a++; と int b = ++a; の対比がその例です。
C の演算子の優先順位とは何ですか?
かっこのない式で、演算子がどの順に結び付くかという順序です。*、/、% は + や - より強く結び付き、それらは比較より、比較は && より、&& は || より、そして最後が代入です。迷ったらかっこを足しましょう。コストはゼロで、疑問が消えます。