🇯🇵 日本語 | 🇺🇸 English
広告スペース

C言語 中級トピック集

基本を終えた人向け。const・enum・typedef・errno・可変長引数・条件コンパイル。

1. const correctness

const は「書き換えない約束」を型で表現する機能。関数の意図を明確にし、誤操作をコンパイル時に検出できる。
// 引数の配列を読むだけで書き換えないことを明示
int sum_array(const int *a, int n) {
    int s = 0;
    for (int i = 0; i < n; i++) s += a[i];
    // a[i] = 0; ← コンパイルエラー(read-only)
    return s;
}

ポインタと const の位置

const int *p → *p は書き換え不可、p は変更可
int * const p → p は変更不可、*p は書き換え可
const int * const p → どちらも不可
コツ: * の位置を基準に、右側が変更可否・左側が中身の可否。

定数の宣言

const int MAX = 100;      // マクロ #define より型安全
const double PI = 3.14159265358979;
推奨: ライブラリの関数シグネチャでは「読み取り専用の引数」には必ず const を付ける。これは呼び出し側への契約になる。

2. enum(列挙型)

関連する整数定数に名前を付けてグループ化する仕組み。switch との相性がよく、コードの可読性が大幅に上がる。
// マジックナンバーを使った良くない例
int state = 2;
if (state == 3) { /* 3 って何? */ }

// enum で意図を明確に
enum Color { RED, GREEN, BLUE };      // 0, 1, 2
enum Color c = GREEN;
if (c == RED) { /* 意味が伝わる */ }

値を明示する

enum HttpStatus {
    HTTP_OK = 200,
    HTTP_NOT_FOUND = 404,
    HTTP_SERVER_ERROR = 500
};

switch と組み合わせる

enum State { STATE_IDLE, STATE_RUN, STATE_STOP };

void handle(enum State s) {
    switch (s) {
        case STATE_IDLE: printf("idle\n"); break;
        case STATE_RUN:  printf("run\n");  break;
        case STATE_STOP: printf("stop\n"); break;
    }
}
gcc/clangの親切機能: -Wswitch-enum を付けると、enum のある値が switch で扱われていないときに警告してくれる。

3. typedef(型に別名)

長くて読みづらい型名にエイリアスを付ける機能。コードの意図を説明するドキュメントにもなる。
// 構造体の typedef
typedef struct {
    int x;
    int y;
} Point;

Point p = {3, 4};           // 'struct Point' と書かなくて良い

// 関数ポインタ
typedef int (*CmpFn)(const void *, const void *);
CmpFn cmp;                    // 関数ポインタをシンプルに宣言

// 配列も可能
typedef char Buffer[256];
Buffer name;                  // char name[256] と同等

標準ライブラリで多用

typedef 型実体例用途
size_tunsigned longサイズ・インデックス
ssize_tlong符号付きサイズ
int32_t / uint64_tint / unsigned longビット幅を明示
time_tlongUNIX時刻
FILEstruct ...ファイルハンドル
やりすぎ注意: typedef を使うとポインタであることが隠れる(例: typedef struct S *SPtr;)。この慣習は可読性を下げるので、ポインタ型は素直に書く派が多い。

4. errno によるエラーハンドリング

多くの標準ライブラリ関数は失敗時にグローバル変数 errno にエラーコードをセットする。perrorstrerror で人間可読なメッセージに変換できる。
#include <stdio.h>
#include <string.h>
#include <errno.h>

int main(void) {
    FILE *fp = fopen("no_such_file.txt", "r");
    if (fp == NULL) {
        // 方法1: perror(メッセージを標準エラーに)
        perror("fopen");
        // → fopen: No such file or directory

        // 方法2: strerror(エラーコード)
        fprintf(stderr, "error %d: %s\n", errno, strerror(errno));
        return 1;
    }
    fclose(fp);
    return 0;
}

よく出るエラー定数

定数意味
ENOENTファイルが存在しない
EACCESアクセス権がない
ENOMEMメモリ不足
EINVAL引数が無効
EAGAINリソース一時不足(再試行可)
EINTRシグナルで中断された
注意点:
errno は関数が失敗を返したときだけ見る。成功時の値は未定義。
② ライブラリ関数を複数呼ぶと errno が上書きされるので、必要なら早めに変数に保存する。
③ 呼ぶ前に errno = 0; とリセットする習慣を付けると誤解が減る(特に strtol など)。

5. 可変長引数(va_list)

printf のように「引数の数が事前に決まらない」関数を自作する仕組み。<stdarg.h> を使う。
#include <stdio.h>
#include <stdarg.h>

// int を可変個受け取って合計を返す
// 第1引数は「いくつあるか」を渡す
int sum(int count, ...) {
    va_list ap;
    va_start(ap, count);          // 可変長引数の読み取り開始
    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(ap, int);    // 1個取り出す
    }
    va_end(ap);                    // 後片付け
    return total;
}

int main(void) {
    printf("%d\n", sum(3, 10, 20, 30));  // 60
    printf("%d\n", sum(5, 1, 2, 3, 4, 5));  // 15
}
重要な制約:
① 引数の型と個数をランタイムに知る方法がない。呼び出し側と受け取り側で必ず事前に決める(個数を第1引数にする、NULL終端、printfのようにフォーマット文字列を解析する、など)。
va_arg の第2引数には実際の型を書く(va_arg(ap, int))。型を間違えるとUB。
③ char や short は自動的に int に昇格されるので va_arg(ap, char) ではなく va_arg(ap, int)

ログ関数の定番パターン

// printfに渡すラッパー。vfprintfに可変長引数をそのまま渡す
void log_info(const char *fmt, ...) {
    va_list ap;
    va_start(ap, fmt);
    fprintf(stderr, "[INFO] ");
    vfprintf(stderr, fmt, ap);   // va_list版のfprintf
    fprintf(stderr, "\n");
    va_end(ap);
}

// 使い方:
log_info("user %s logged in at %d", name, ts);

6. 条件コンパイル(#ifdef / #if)

環境やビルド設定によって有効にするコードを切り替える仕組み。クロスプラットフォーム対応やデバッグコード挿入で多用。

#ifdef / #ifndef

#ifdef DEBUG
    printf("x = %d\n", x);
#endif

// gcc -DDEBUG を付けてコンパイルすると上のprintfが有効
// デフォルトでは消える

#if で数値比較

#define VERSION 3

#if VERSION >= 2
    // v2以降の機能
#else
    // v1の機能
#endif

プラットフォーム分岐

#if defined(_WIN32)
    #include <windows.h>
    Sleep(1000);
#elif defined(__APPLE__) || defined(__linux__)
    #include <unistd.h>
    sleep(1);
#else
    #error "unsupported platform"
#endif

プリプロセッサの定番マクロ

マクロ意味
__FILE__現在のファイル名
__LINE__現在の行番号
__func__関数名(C99以降)
__DATE__ / __TIME__コンパイル日時
__STDC_VERSION__C規格バージョン
// デバッグ用の便利マクロ
#define LOG(...) \
    fprintf(stderr, "[%s:%d %s] ", __FILE__, __LINE__, __func__), \
    fprintf(stderr, __VA_ARGS__), \
    fprintf(stderr, "\n")

LOG("x = %d", x);
// 出力: [main.c:42 main] x = 10
#pragma once との違い: include guard としての #ifndef XXX_H / #define / #endif は標準だが、#pragma once はコンパイラ拡張(ただし広くサポートされる)。