第3章: 演算子と式
3.1 式の評価
式とは
式(Expression)は、値を生成する構文要素である。C言語では、ほぼ全ての構文が式として評価される。
42 /* リテラル式: 42 */
x /* 識別子式: xの値 */
x + y /* 算術式: xとyの和 */
x = 42 /* 代入式: 42(代入後のxの値) */
func() /* 関数呼び出し式: funcの戻り値 */
x++ /* 後置インクリメント式: 元のxの値 */
x, y, z /* カンマ式: zの値 */
副作用と評価順序
副作用(Side Effect)は、式の評価による状態変更である:
x = 10; /* 副作用: xへの代入 */
x++; /* 副作用: xのインクリメント */
printf(); /* 副作用: 出力 */
シーケンスポイント(Sequence Point):
C言語では、シーケンスポイント間の副作用の順序は未規定である。
/* 未定義動作! */
i = i++; /* iに対する2つの変更がシーケンスポイントなしで発生 */
/* 未定義動作! */
a[i] = i++;
/* 未定義動作! */
printf("%d %d\n", i++, i++);
シーケンスポイントの位置:
;(文の終わり)&&,||,?:,,の評価後- 関数呼び出しの引数評価後
- 完全式の評価後
3.2 算術演算子
二項算術演算子
int a = 10, b = 3;
int sum = a + b; /* 13: 加算 */
int diff = a - b; /* 7: 減算 */
int prod = a * b; /* 30: 乗算 */
int quot = a / b; /* 3: 除算(整数除算) */
int rem = a % b; /* 1: 剰余 */
整数除算の注意点:
/* 整数同士の除算は整数結果 */
int result = 10 / 3; /* 3(切り捨て) */
double d1 = 10 / 3; /* 3.0(整数除算の結果をdoubleに) */
double d2 = 10.0 / 3; /* 3.333...(浮動小数点除算) */
double d3 = (double)10 / 3; /* 3.333... */
負の数の除算と剰余(C99以降):
/* C99: 0方向への切り捨て */
int q1 = -10 / 3; /* -3 */
int r1 = -10 % 3; /* -1 */
/* 恒等式: (a/b)*b + a%b == a */
/* (-10/3)*3 + (-10%3) = -9 + (-1) = -10 ✓ */
単項算術演算子
int x = 10;
int neg = -x; /* -10: 符号反転 */
int pos = +x; /* 10: 符号保持(ほぼ意味なし) */
/* 前置インクリメント/デクリメント */
int a = ++x; /* x=11, a=11: 先に増加、増加後の値 */
int b = --x; /* x=10, b=10: 先に減少、減少後の値 */
/* 後置インクリメント/デクリメント */
int c = x++; /* c=10, x=11: 元の値を返し、その後増加 */
int d = x--; /* d=11, x=10: 元の値を返し、その後減少 */
3.3 比較演算子
int a = 10, b = 20;
int eq = (a == b); /* 0: 等しい */
int ne = (a != b); /* 1: 等しくない */
int lt = (a < b); /* 1: より小さい */
int gt = (a > b); /* 0: より大きい */
int le = (a <= b); /* 1: 以下 */
int ge = (a >= b); /* 0: 以上 */
/* 結果は常に0または1 */
浮動小数点の比較:
double a = 0.1 + 0.2;
double b = 0.3;
/* 危険!浮動小数点誤差 */
if (a == b) { } /* falseになる可能性 */
/* イプシロン比較 */
#include <math.h>
#define EPSILON 1e-9
if (fabs(a - b) < EPSILON) { }
3.4 論理演算子
int a = 1, b = 0;
int and = a && b; /* 0: 論理AND */
int or = a || b; /* 1: 論理OR */
int not = !a; /* 0: 論理NOT */
短絡評価(Short-Circuit Evaluation):
/* &&: 左がfalseなら右は評価されない */
if (ptr && *ptr == 42) { } /* ptrがNULLなら*ptrは評価されない */
/* ||: 左がtrueなら右は評価されない */
if (a || expensive_check()) { } /* aがtrueならチェックされない */
/* 副作用の利用 */
file = fopen(name, "r") || die("Cannot open file");
3.5 ビット演算子
ビット単位演算子
unsigned int a = 0b11001100;
unsigned int b = 0b10101010;
unsigned int and = a & b; /* 0b10001000: ビットAND */
unsigned int or = a | b; /* 0b11101110: ビットOR */
unsigned int xor = a ^ b; /* 0b01100110: ビットXOR */
unsigned int not = ~a; /* 0b00110011: ビット反転(実際は全ビット) */
シフト演算子
unsigned int x = 0b00001111;
unsigned int left = x << 2; /* 0b00111100: 左シフト */
unsigned int right = x >> 2; /* 0b00000011: 右シフト */
/* 左シフトは2^nの乗算に等しい */
int y = 5;
int y_times_8 = y << 3; /* 40 */
/* 右シフトは2^nの除算に等しい(符号なしの場合) */
unsigned int z = 40;
unsigned int z_div_8 = z >> 3; /* 5 */
符号付き右シフトの注意:
int negative = -8;
int shifted = negative >> 1; /* 実装依存! */
/* 算術シフト: -4(符号ビットで埋める) */
/* 論理シフト: 大きな正の数(0で埋める) */
ビット操作のイディオム
/* ビットフラグの設定 */
#define FLAG_READ (1 << 0) /* 0001 */
#define FLAG_WRITE (1 << 1) /* 0010 */
#define FLAG_EXEC (1 << 2) /* 0100 */
unsigned int flags = 0;
/* フラグを立てる */
flags |= FLAG_READ;
/* フラグを下ろす */
flags &= ~FLAG_READ;
/* フラグをトグル */
flags ^= FLAG_WRITE;
/* フラグをチェック */
if (flags & FLAG_EXEC) { }
/* 複数フラグを同時に設定 */
flags |= (FLAG_READ | FLAG_WRITE);
/* その他のビット操作 */
/* 最下位の1を消す */
x & (x - 1)
/* 2のべき乗判定 */
(x & (x - 1)) == 0
/* 最下位の1だけを取り出す */
x & (-x)
/* スワップ(XOR swap) */
a ^= b;
b ^= a;
a ^= b;
3.6 代入演算子
単純代入
int x;
x = 42; /* 代入 */
/* 代入式の値は代入された値 */
int y = (x = 10); /* x=10, y=10 */
/* 連鎖代入 */
int a, b, c;
a = b = c = 0; /* 右から評価: a=(b=(c=0)) */
複合代入演算子
int x = 10;
x += 5; /* x = x + 5; → 15 */
x -= 3; /* x = x - 3; → 12 */
x *= 2; /* x = x * 2; → 24 */
x /= 4; /* x = x / 4; → 6 */
x %= 4; /* x = x % 4; → 2 */
x <<= 2; /* x = x << 2; */
x >>= 1; /* x = x >> 1; */
x &= 0xFF; /* x = x & 0xFF; */
x |= 0x01; /* x = x | 0x01; */
x ^= 0x01; /* x = x ^ 0x01; */
3.7 その他の演算子
条件演算子(三項演算子)
int max = (a > b) ? a : b;
/* ネスト可能 */
int sign = (x > 0) ? 1 : (x < 0) ? -1 : 0;
/* 左辺値にもなれる(非標準的) */
*(condition ? &a : &b) = 10;
カンマ演算子
/* カンマ演算子: 左を評価、右の値を返す */
int x = (1, 2, 3); /* x = 3 */
/* forループでよく使われる */
for (int i = 0, j = 10; i < j; i++, j--) { }
/* 関数の引数のカンマとは異なる */
func(a, b); /* これはカンマ演算子ではない */
func((a, b)); /* これはカンマ演算子(bがfuncに渡される) */
sizeof演算子
/* 型に対して */
size_t s1 = sizeof(int); /* 4(通常) */
size_t s2 = sizeof(char); /* 必ず1 */
/* 式に対して(式は評価されない) */
int x = 0;
size_t s3 = sizeof(x++); /* xは増加しない */
/* 配列とポインタ */
int arr[10];
int *ptr = arr;
size_t s4 = sizeof(arr); /* 40: 配列全体 */
size_t s5 = sizeof(ptr); /* 8: ポインタのサイズ */
_Alignof演算子(C11)
/* 型のアラインメント要件を返す */
size_t align1 = _Alignof(int); /* 4(通常) */
size_t align2 = _Alignof(double); /* 8(通常) */
struct S {
char c;
double d;
};
size_t align3 = _Alignof(struct S); /* 8(最大メンバのアラインメント) */
_Generic(C11)
/* 型に基づく選択 */
#define type_name(x) _Generic((x), \
int: "int", \
float: "float", \
double: "double", \
char *: "char *", \
default: "unknown")
printf("%s\n", type_name(42)); /* "int" */
printf("%s\n", type_name(3.14)); /* "double" */
printf("%s\n", type_name("hello")); /* "char *" */
3.8 演算子の優先順位と結合性
優先順位表
| 優先度 | 演算子 | 結合性 |
|--------|--------|--------|
| 1(最高) | () [] -> . ++後置 --後置 | 左から右 |
| 2 | ++前置 --前置 +単項 -単項 ! ~ 間接 &アドレス sizeof _Alignof (type) | 右から左 |
| 3 | / % | 左から右 |
| 4 | + - | 左から右 |
| 5 | << >> | 左から右 |
| 6 | < <= > >= | 左から右 |
| 7 | == != | 左から右 |
| 8 | &ビット | 左から右 |
| 9 | ^ | 左から右 |
| 10 | \| | 左から右 |
| 11 | && | 左から右 |
| 12 | \|\| | 左から右 |
| 13 | ?: | 右から左 |
| 14 | = += -= など | 右から左 |
| 15(最低) | , | 左から右 |
よくある間違い
/* 間違い1: ビット演算と比較 */
if (flags & FLAG == FLAG) { } /* (flags & (FLAG == FLAG)) */
if ((flags & FLAG) == FLAG) { } /* 正しい */
/* 間違い2: シフトと加算 */
int x = 1 << 2 + 3; /* 1 << 5 = 32 */
int y = (1 << 2) + 3; /* 4 + 3 = 7 */
/* 間違い3: 代入と比較 */
if (x = 0) { } /* 代入、常にfalse */
if (x == 0) { } /* 比較 */
if (0 == x) { } /* Yoda記法: 代入するとエラー */
3.9 未定義動作と未規定動作
未定義動作(Undefined Behavior)
未定義動作は、C標準が何の要件も課さない動作である。何が起きるか予測不能。
/* 符号付き整数オーバーフロー */
int x = INT_MAX;
x = x + 1; /* UB */
/* NULLポインタの参照 */
int *p = NULL;
int y = *p; /* UB */
/* 配列の境界外アクセス */
int arr[10];
int z = arr[100]; /* UB */
/* シーケンスポイント違反 */
int i = 0;
i = i++ + ++i; /* UB */
/* 初期化されていない変数の使用 */
int uninit;
printf("%d\n", uninit); /* UB */
未規定動作(Unspecified Behavior)
未規定動作は、複数の動作が許容され、どれが選ばれるか規定されない動作である。
/* 関数引数の評価順序 */
int i = 0;
printf("%d %d\n", i++, i++); /* 評価順序は未規定 */
/* 部分式の評価順序 */
int x = func1() + func2(); /* どちらが先か未規定 */
処理系定義動作(Implementation-Defined Behavior)
処理系定義動作は、処理系が動作を文書化する必要がある動作である。
/* charの符号 */
char c = -1; /* signed charかunsigned charか処理系定義 */
/* int のサイズ */
sizeof(int); /* 2, 4, 8など処理系定義 */
/* 符号付き右シフト */
int x = -8 >> 1; /* 算術シフトか論理シフトか処理系定義 */
3.10 まとめ
本章では、C言語の演算子と式について学んだ:
- 式の評価: 副作用とシーケンスポイント
- 算術演算子: 整数除算の注意点
- 比較演算子: 浮動小数点の比較
- 論理演算子: 短絡評価
- ビット演算子: フラグ操作、シフト
- 代入演算子: 複合代入
- その他: 条件、カンマ、sizeof、_Generic
- 優先順位: よくある間違い
- 未定義動作: 避けるべきパターン
- ISO/IEC 9899:2011, Programming languages — C
- Seacord, R. C. (2014). "The CERT C Coding Standard", 2nd Edition, Addison-Wesley
- Harbison, S. P., & Steele, G. L. (2002). "C: A Reference Manual", 5th Edition, Prentice Hall
- Koenig, A. (1989). "C Traps and Pitfalls", Addison-Wesley
次章では、制御構造について学ぶ。
---