第4章: 制御構造
4.1 構造化プログラミングの理論
Böhm-Jacopiniの定理
1966年、Corrado BöhmとGiuseppe Jacopiniは、重要な定理を証明した:
> 任意のアルゴリズムは、順次、選択、反復の3つの基本構造のみで表現できる。
この定理は構造化プログラミングの理論的基盤となった。
3つの基本構造:
1. 順次(Sequence):
文1 → 文2 → 文3
2. 選択(Selection):
┌── true ──→ 文A
条件
└── false ─→ 文B
3. 反復(Iteration):
┌──────────────────┐
↓ │
条件 ── true ──→ 文 ─┘
│
false
↓
Dijkstraとgoto文
1968年、Edsger Dijkstraは"Go To Statement Considered Harmful"を発表し、goto文の使用を批判した。
Dijkstraの主張:
- goto文はプログラムの制御フローを複雑にする
- 構造化された制御構造を使えば、プログラムは証明可能になる
- コードの可読性と保守性が向上する
C言語はgoto文を持つが、現代では限定的な使用に留まる。
4.2 選択文
if文
if (expression)
statement
if (expression)
statement1
else
statement2
if (expression1)
statement1
else if (expression2)
statement2
else
statement3
ダングリングelse問題:
/* 曖昧なコード */
if (a)
if (b)
x = 1;
else
x = 2;
/* C言語の解釈: elseは最も近いifに結合 */
if (a) {
if (b)
x = 1;
else
x = 2;
}
/* 意図が異なる場合は明示的にブレース */
if (a) {
if (b)
x = 1;
}
else {
x = 2;
}
switch文
switch (expression) {
case constant1:
statements;
break;
case constant2:
statements;
break;
default:
statements;
break;
}
フォールスルー:
/* 意図的なフォールスルー */
switch (c) {
case 'a':
case 'e':
case 'i':
case 'o':
case 'u':
printf("vowel\n");
break;
default:
printf("consonant\n");
break;
}
/* Duffのデバイス(最適化テクニック) */
void copy(char *to, char *from, int count)
{
int n = (count + 7) / 8;
switch (count % 8) {
case 0: do { *to++ = *from++;
case 7: *to++ = *from++;
case 6: *to++ = *from++;
case 5: *to++ = *from++;
case 4: *to++ = *from++;
case 3: *to++ = *from++;
case 2: *to++ = *from++;
case 1: *to++ = *from++;
} while (--n > 0);
}
}
C23のattribute:
switch (x) {
case 1:
do_something();
[[fallthrough]]; /* 意図的なフォールスルーを明示 */
case 2:
do_more();
break;
}
4.3 反復文
while文
while (expression)
statement
/* 基本的な使用 */
int i = 0;
while (i < 10) {
printf("%d\n", i);
i++;
}
/* 無限ループ */
while (1) {
/* 永久に実行 */
}
/* イディオム: ファイル読み込み */
int c;
while ((c = getchar()) != EOF) {
putchar(c);
}
do-while文
do
statement
while (expression);
/* 最低1回実行 */
int input;
do {
printf("Enter positive number: ");
scanf("%d", &input);
} while (input <= 0);
/* イディオム: 複数行マクロ */
#define SAFE_FREE(p) do { free(p); (p) = NULL; } while (0)
for文
for (init; condition; update)
statement
/* 基本 */
for (int i = 0; i < 10; i++) {
printf("%d\n", i);
}
/* 複数の変数 */
for (int i = 0, j = 10; i < j; i++, j--) {
printf("i=%d, j=%d\n", i, j);
}
/* 無限ループ */
for (;;) {
/* 永久に実行 */
}
/* イディオム: ポインタを使った配列走査 */
for (int *p = arr; p < arr + n; p++) {
printf("%d\n", *p);
}
/* イディオム: 連結リストの走査 */
for (node *n = head; n != NULL; n = n->next) {
process(n);
}
4.4 ジャンプ文
break文
/* ループを抜ける */
for (int i = 0; i < 100; i++) {
if (arr[i] == target) {
found = i;
break; /* ループを終了 */
}
}
/* switchを抜ける */
switch (x) {
case 1:
action();
break; /* switchを終了 */
}
continue文
/* 次の反復へスキップ */
for (int i = 0; i < 100; i++) {
if (arr[i] < 0)
continue; /* 負の数をスキップ */
process(arr[i]);
}
goto文
/* 基本構文 */
goto label;
/* ... */
label:
statement;
goto文の適切な使用場面:
/* 1. ネストしたループからの脱出 */
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
if (matrix[i][j] == target) {
found_i = i;
found_j = j;
goto found;
}
}
}
found:
printf("Found at (%d, %d)\n", found_i, found_j);
/* 2. エラー処理とクリーンアップ */
int process_file(const char *filename)
{
FILE *fp = NULL;
char *buffer = NULL;
int result = -1;
fp = fopen(filename, "r");
if (!fp)
goto cleanup;
buffer = malloc(BUFFER_SIZE);
if (!buffer)
goto cleanup;
/* 処理 */
result = 0;
cleanup:
free(buffer);
if (fp)
fclose(fp);
return result;
}
gotoを避けるべき場面:
/* 悪い例: gotoによるスパゲッティコード */
if (cond1) goto label1;
/* ... */
label2:
/* ... */
goto label3;
label1:
/* ... */
goto label2;
label3:
/* ... */
return文
/* 値を返す */
int add(int a, int b)
{
return a + b;
}
/* void関数での早期リターン */
void process(int *p)
{
if (!p)
return; /* 早期リターン */
/* 処理 */
}
4.5 制御構造のパターン
番兵(Sentinel)
/* 番兵を使った配列検索 */
int find_sentinel(int arr[], int n, int target)
{
int last = arr[n];
arr[n] = target; /* 番兵を設置 */
int i = 0;
while (arr[i] != target)
i++;
arr[n] = last; /* 元に戻す */
return (i < n) ? i : -1;
}
ガード節(Guard Clause)
/* ガード節で早期リターン */
int divide(int a, int b)
{
if (b == 0)
return -1; /* ガード */
if (a == 0)
return 0; /* ガード */
return a / b;
}
/* vs ネストした条件 */
int divide_nested(int a, int b)
{
int result;
if (b != 0) {
if (a != 0) {
result = a / b;
} else {
result = 0;
}
} else {
result = -1;
}
return result;
}
状態機械(State Machine)
typedef enum {
STATE_IDLE,
STATE_RUNNING,
STATE_PAUSED,
STATE_STOPPED
} state_t;
void state_machine(event_t event)
{
static state_t state = STATE_IDLE;
switch (state) {
case STATE_IDLE:
if (event == EVENT_START) {
state = STATE_RUNNING;
start_process();
}
break;
case STATE_RUNNING:
if (event == EVENT_PAUSE) {
state = STATE_PAUSED;
pause_process();
} else if (event == EVENT_STOP) {
state = STATE_STOPPED;
stop_process();
}
break;
case STATE_PAUSED:
if (event == EVENT_RESUME) {
state = STATE_RUNNING;
resume_process();
} else if (event == EVENT_STOP) {
state = STATE_STOPPED;
stop_process();
}
break;
case STATE_STOPPED:
/* 終了状態 */
break;
}
}
4.6 複合文とブロック
ブロックスコープ
{
int x = 10; /* このブロック内でのみ有効 */
/* ... */
}
/* xはここでは見えない */
/* ブロック内での変数シャドウイング */
int x = 1;
{
int x = 2; /* 外側のxを隠す */
printf("%d\n", x); /* 2 */
}
printf("%d\n", x); /* 1 */
複数の宣言を含むfor(C99)
for (int i = 0; i < n; i++) {
/* iはこのループ内でのみ有効 */
}
/* iはここでは見えない */
4.7 条件式のイディオム
真偽値の扱い
/* 冗長なコード */
if (condition == true) { }
if (condition != false) { }
/* 簡潔なコード */
if (condition) { }
if (!condition) { }
/* ポインタのNULLチェック */
if (ptr != NULL) { }
if (ptr) { } /* より簡潔 */
if (ptr == NULL) { }
if (!ptr) { } /* より簡潔 */
短絡評価の利用
/* NULLチェックと参照を1行で */
if (ptr && ptr->value > 0) { }
/* デフォルト値の設定 */
int value = get_value() || default_value;
/* エラーチェックの連鎖 */
if (init() && configure() && start()) {
/* 全て成功 */
}
assert
#include <assert.h>
void process(int *arr, int n)
{
assert(arr != NULL);
assert(n > 0);
/* ... */
}
/* リリースビルドでは無効化 */
/* gcc -DNDEBUG ... */
4.8 エラー処理パターン
戻り値によるエラー処理
/* エラーコードを返す */
int read_file(const char *filename, char **content)
{
FILE *fp = fopen(filename, "r");
if (!fp)
return -1;
/* 処理 */
fclose(fp);
return 0;
}
/* 呼び出し側 */
if (read_file("test.txt", &content) != 0) {
/* エラー処理 */
}
errnoによるエラー処理
#include <errno.h>
#include <string.h>
FILE *fp = fopen(filename, "r");
if (!fp) {
fprintf(stderr, "Error: %s\n", strerror(errno));
return -1;
}
gotoによるクリーンアップ
int complex_operation(void)
{
int ret = -1;
int *arr1 = NULL, *arr2 = NULL;
FILE *fp = NULL;
arr1 = malloc(sizeof(int) * 100);
if (!arr1)
goto error;
arr2 = malloc(sizeof(int) * 200);
if (!arr2)
goto error;
fp = fopen("data.txt", "r");
if (!fp)
goto error;
/* 処理 */
ret = 0;
error:
free(arr1);
free(arr2);
if (fp)
fclose(fp);
return ret;
}
4.9 まとめ
本章では、C言語の制御構造について学んだ:
次章では、関数について学ぶ。
---