第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