Skip to content

C チートシート

システム、組み込み、OS 開発を支える低レベル言語。

01

はじめに

Hello World

すべての C プログラムは main() から始まります。#include <stdio.h> は標準 I/O ライブラリ(printf、scanf)を取り込みます。main は成功時に 0、失敗時に非ゼロを返します。void キーワードは main がパラメータを取らないことを明示的に宣言します。

c
#include <stdio.h>

int main(void) {
    printf("Hello, World!\n");
    return 0;
}

変数と型

C は静的型付けです。一般的な型:int、float、double、char。float には f サフィックスが必要です。char[] は文字列です(ヌル終端配列)。long と short はサイズ修飾子です。unsigned は非負を意味します。サイズはプラットフォームにより異なります;固定幅には <stdint.h> を使用してください。

c
int age = 30;
float height = 5.7f;
double pi = 3.14159;
char grade = 'A';
char name[] = "Alice";
long big = 100000L;
unsigned int count = 42;
printf("%s is %d\n", name, age);

入力と出力

scanf は入力を保存する変数のアドレス(&)が必要です。バッファオーバーフローを防ぐため、文字列入力の長さを常に制限してください(50文字バッファには %49s)。scanf は空白で文字列の読み取りを停止します;完全な行には fgets を使用してください。

c
int n;
printf("Enter a number: ");
scanf("%d", &n);
printf("You entered %d\n", n);

char name[50];
printf("Enter name: ");
scanf("%49s", name);  // limit to prevent overflow
printf("Hi, %s!\n", name);

printf フォーマット指定子

フォーマット指定子が出力を制御します:%d 整数、%f 浮動小数点、%c 文字、%s 文字列、%x 16進数、%p ポインタ。幅と精度(例:%5.2f)が配置と小数を制御します。指定子と型の不一致は未定義動作を引き起こします。

c
printf("%d\n", 42);        // integer
printf("%f\n", 3.14);       // float/double
printf("%.2f\n", 3.14159);  // 3.14 (2 decimals)
printf("%c\n", 'A');        // char
printf("%s\n", "hello");    // string
printf("%x\n", 255);        // ff (hex)
printf("%5d\n", 42);        // right-aligned, width 5
printf("%-5d|\n", 42);      // left-aligned

プリプロセッサとヘッダ

プリプロセッサはコンパイル前に実行されます。#include はヘッダファイルを貼り付け、#define はマクロと定数を作成します。二重インクルードを防ぐため、ヘッダには常にインクルードガード(#ifndef/#define/#endif)を使用してください。マクロはテキスト置換です — パラメータの周りに括弧を使用してください。

c
#include <stdio.h>   // system header
#include "myheader.h"  // local header

#define PI 3.14159
#define SQUARE(x) ((x) * (x))

#ifndef GUARD_H
#define GUARD_H
// header content
#endif
02

文字列と String.h

文字列の基礎

C の文字列はヌル終端 char 配列です。strlen は '\0' の前の文字数をカウントします;sizeof はバッファサイズを返します。strcpy はヌルターミネータまでコピーします — オーバーフローを避けるため、宛先が十分に大きいことを常に確認してください。

c
#include <string.h>
char s[20] = "Hello";
printf("Length: %zu\n", strlen(s));   // 5
printf("Size: %zu\n", sizeof(s));     // 20

char dest[20];
strcpy(dest, s);   // copy
printf("%s\n", dest);  // Hello

連結と比較

strcat は追加します(宛先に余裕が必要)。strcmp は辞書順に比較します:等しければ 0、最初 < 2番目なら負、最初 > 2番目なら正を返します。文字列の比較に == を使用しないでください(内容ではなくポインタを比較します)。

c
#include <string.h>
char s[30] = "Hello";
strcat(s, ", World!");   // s = "Hello, World!"
printf("%s\n", s);

int cmp = strcmp("apple", "banana");
// returns <0 if a<b, 0 if equal, >0 if a>b
if (strcmp(s, "Hello") == 0) {
    printf("Equal!\n");
}

sprintf と sscanf

sprintf は文字列バッファにフォーマットします(printf のようなものですが文字列へ)。sscanf は文字列から変数に解析します(scanf のようなものですが文字列から)。最大サイズを指定してバッファオーバーフローを防ぐため、sprintf の代わりに snprintf を使用してください。

c
char buf[100];
int age = 30;
char name[] = "Alice";
sprintf(buf, "%s is %d years old", name, age);
printf("%s\n", buf);

int a, b;
sscanf("10 20", "%d %d", &a, &b);
printf("a=%d, b=%d\n", a, b);  // a=10, b=20

strchr、strstr と strtok

strchr は文字を検索し、strstr は部分文字列を検索します。strtok は区切り文字で文字列を分割しますが、元の文字列を変更し(ヌルターミネータを挿入)、スレッドセーフではありません — トークン化を続けるには後続の呼び出しで NULL を渡します。

c
#include <string.h>
char s[] = "Hello, World!";
char *p = strchr(s, 'W');   // find first 'W'
printf("%s\n", p);          // World!

char *sub = strstr(s, "World");
printf("%s\n", sub);        // World!

char tokens[] = "a,b,c";
char *tok = strtok(tokens, ",");
while (tok) {
    printf("%s\n", tok);
    tok = strtok(NULL, ",");
}

fgets と安全な入力

fgets は文字列を読み取る安全な方法です — オーバーフローを防ぐためのサイズ制限を取ります。scanf とは異なり、スペースを読み取ります。改行は結果に含まれます;strcspn で見つけて削除します。gets(C11 で削除)より常に fgets を優先してください。

c
char line[100];
printf("Enter text: ");
fgets(line, sizeof(line), stdin);
// removes trailing newline
line[strcspn(line, "\n")] = 0;
printf("You said: %s\n", line);
03

数値と数学

整数型と制限

正確なサイズが必要な場合は固定幅型(int32_t、int64_t)に <stdint.h> を使用してください。<limits.h> はプラットフォーム固有の境界の INT_MAX、INT_MIN などを提供します。LL サフィックスは long long リテラルをマークします。int/long のサイズはプラットフォームにより異なります。

c
#include <stdint.h>
#include <limits.h>
int32_t a = 100;
int64_t big = 9223372036854775807LL;
uint8_t byte = 255;

printf("INT_MAX = %d\n", INT_MAX);     // 2147483647
printf("INT_MIN = %d\n", INT_MIN);     // -2147483648
printf("UINT_MAX = %u\n", UINT_MAX);   // 4294967295

浮動小数点

float は 4 バイト(6-7 桁精度)、double は 8 バイト(15-16 桁)です。丸めエラーのため float を == で比較しないでください — fabs(a - b) < epsilon を使用します。<float.h> は DBL_MAX、DBL_EPSILON を提供します。

c
#include <float.h>
double d = 3.141592653589793;
float f = 3.14f;

printf("DBL_MAX = %e\n", DBL_MAX);
printf("DBL_EPSILON = %e\n", DBL_EPSILON);

if (d == 0.1 + 0.2) {
    // likely false! floating point imprecision
}

数学関数

<math.h> は標準数学関数を提供します。pow と sqrt は double を返します。fabs は abs の浮動小数点版です(abs は int 用)。一部のシステムでは -lm でリンクします。金融計算では浮動小数点を避け — 整数セントを使用してください。

c
#include <math.h>
double x = 2.5;
pow(x, 3);      // 15.625
sqrt(x);        // 1.581
fabs(-5.0);     // 5.0
floor(3.7);     // 3.0
ceil(3.2);      // 4.0
fmod(10.5, 3);  // 1.5
exp(1);         // 2.718 (e^1)
log(2.718);     // 1.0 (natural log)

乱数

rand() は 0 から RAND_MAX の疑似乱数 int を返します。プログラム開始時に srand() で一度シードします(time(NULL) を使用)。rand() % N はモジュロバイアスがあり品質が低いです;本格的な使用には /dev/urandom を読むかサードパーティ PRNG ライブラリを使用してください。

c
#include <stdlib.h>
#include <time.h>

srand(time(NULL));  // seed once at start
int r = rand() % 100;       // 0-99
int dice = rand() % 6 + 1;  // 1-6

float fr = (float)rand() / RAND_MAX;  // 0.0 - 1.0

型変換とキャスト

キャストは (type)value です。整数除算は切り捨てられます — float 結果を得るには float オペランドを使用してください。atoi/atof は文字列を数値に変換しますがエラーチェックをしません;errno で解析エラーを報告する strtol/strtod を優先してください。

c
int i = 65;
char c = (char)i;        // 'A'
double d = 3.99;
int truncated = (int)d;  // 3

// Implicit promotion
int a = 5;
double result = a / 2.0;  // 2.5 (promoted to double)
int bad = a / 2;          // 2 (integer division)

// String to number
int n = atoi("42");
double f = atof("3.14");
04

制御フロー

If / Else

if/else if/else が標準の条件分岐です。C は 0 を false、非ゼロを true として扱います。後から行を追加する時のバグを防ぐため、単一文でもブレースを使用してください。C89 には boolean 型がありません;C99 は _Bool と <stdbool.h> を追加します。

c
int score = 85;
if (score >= 90) {
    printf("A\n");
} else if (score >= 80) {
    printf("B\n");
} else if (score >= 70) {
    printf("C\n");
} else {
    printf("F\n");
}

Switch

switch はマッチする case ラベルにジャンプします。フォールスルーを防ぐため常に break を使用してください(case 6 と 7 は意図的にコードを共有)。default はマッチしない値を処理します。switch は整数型と char 型のみで動作し、文字列や float では動作しません。

c
int day = 3;
switch (day) {
    case 1: printf("Mon\n"); break;
    case 2: printf("Tue\n"); break;
    case 3: printf("Wed\n"); break;
    case 6:
    case 7: printf("Weekend\n"); break;
    default: printf("Invalid\n");
}

For ループ

for ループは init; condition; update を持ちます。sizeof(nums)/sizeof(nums[0]) はコンパイル時に配列長を計算します。for ループ内で i を宣言するには C99 以降が必要です。条件が最初から false の場合、ループ本体はゼロ回実行されます。

c
for (int i = 0; i < 5; i++) {
    printf("%d\n", i);
}

// Iterate an array
int nums[] = {10, 20, 30};
int n = sizeof(nums) / sizeof(nums[0]);
for (int i = 0; i < n; i++) {
    printf("%d\n", nums[i]);
}

While と Do-While

while は実行前にチェックします(ゼロ回実行の可能性)。do-while は本体を先に実行し、その後チェックします(少なくとも1回実行)。do-while は入力検証とメニューループに最適です — プロンプトが条件チェックの前に表示される必要があるため。

c
int count = 0;
while (count < 3) {
    printf("%d\n", count++);
}

int x;
do {
    printf("Enter positive: ");
    scanf("%d", &x);
} while (x <= 0);  // runs at least once

Break、Continue と goto

break は最も近いループ/switch を終了;continue は次の反復にスキップします。C にはラベル付き break がないため、深くネストされたループから抜けるには goto が慣用的な方法です。goto はそれ以外では推奨されませんが、クリーンアップパターンとネストループ脱出には許容されます。

c
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;  // skip 3
    if (i == 7) break;     // stop at 7
    printf("%d ", i);      // 0 1 2 4 5 6
}

// goto for breaking nested loops
for (int i = 0; i < n; i++) {
    for (int j = 0; j < m; j++) {
        if (found) goto done;
    }
}
done: printf("exited\n");
05

関数

定義と呼び出し

関数は使用前に宣言(プロトタイプ)または定義しなければなりません。void 戻り値型は戻り値がないことを意味します。const char *name は関数が文字列を変更しないことを意味します。C は値渡しで引数を渡します;参照渡しをシミュレートするにはポインタを使用します。

c
int add(int a, int b) {
    return a + b;
}

void greet(const char *name) {
    printf("Hello, %s!\n", name);
}

int main(void) {
    int sum = add(3, 4);
    greet("Alice");
    return 0;
}

再帰

再帰はより小さい入力で自身を呼び出します。すべての再帰関数には停止するベースケースが必要です。上記の単純な fib は O(2^n) です — 指数関数的です。効率のためにメモ化または反復を使用してください。深い再帰はコールスタックをオーバーフローする可能性があります。

c
int factorial(int n) {
    if (n <= 1) return 1;       // base case
    return n * factorial(n - 1); // recursive case
}

int fib(int n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}
// factorial(5) == 120

関数ポインタ

関数ポインタは関数のアドレスを格納し、コールバックと動的ディスパッチを可能にします。構文 int (*op)(int, int) は2つの int を取り int を返す関数へのポインタを宣言します。qsort、イベントハンドラ、プラグインシステムで使用されます。

c
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int (*op)(int, int) = add;
printf("%d\n", op(3, 4));  // 7

op = sub;
printf("%d\n", op(3, 4));  // -1

// As a parameter
int apply(int (*f)(int, int), int a, int b) {
    return f(a, b);
}

可変長引数関数

可変長引数関数は <stdarg.h> を使用して可変個の引数を受け取ります。va_start が初期化し、va_arg が次の引数を取得し、va_end がクリーンアップします。カウントを知る方法が必要です(例:カウントパラメータやセンチネル値)。printf はこのように動作します。

c
#include <stdarg.h>
int sum(int count, ...) {
    va_list args;
    va_start(args, count);
    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int);
    }
    va_end(args);
    return total;
}
// sum(3, 10, 20, 30) == 60

static と inline

関数/変数の static は現在の翻訳単位(ファイル)に制限します。ローカル変数の static は呼び出し間で持続させます(グローバルのようにスコープされます)。inline はコンパイラに関数本体を埋め込むことを提案します;モダンコンパイラはそれを無視し、自分で決定します。

c
// static: internal linkage (file-local)
static int counter = 0;
static int next_id(void) { return ++counter; }

// inline: hint to expand inline
static inline int square(int x) { return x * x; }

// static local: persists across calls
int call_count(void) {
    static int n = 0;
    return ++n;
}
06

配列とポインタ

配列

配列は固定サイズ、ゼロインデックス、メモリに連続して格納されます。sizeof(arr)/sizeof(arr[0]) は長さを計算しますが、実際の配列でのみ動作し、ポインタでは動作しません(配列は関数に渡されるとポインタに崩壊し、サイズ情報を失います)。

c
int nums[5] = {1, 2, 3, 4, 5};
printf("%d\n", nums[0]);     // 1
printf("%d\n", nums[4]);     // 5

int len = sizeof(nums) / sizeof(nums[0]);  // 5

// Array of strings
char *fruits[] = {"apple", "banana", "cherry"};
printf("%s\n", fruits[1]);   // banana

ポインタ

ポインタはメモリアドレスを格納します。& はアドレスを取得し、* はデリファレンスします。ポインタは常に初期化してください(未割り当てなら NULL を使用)。NULL や未初期化ポインタのデリファレンスは未定義動作です(通常クラッシュ)。デリファレンス前に NULL チェックしてください。

c
int x = 10;
int *ptr = &x;     // ptr holds address of x
printf("%p\n", (void*)ptr);  // address
printf("%d\n", *ptr);        // 10 (dereference)

*ptr = 20;         // modify x through pointer
printf("%d\n", x);  // 20

int *p = NULL;     // null pointer (points to nothing)
if (p) { /* safe to dereference */ }

ポインタ演算

ポインタ演算は要素サイズでスケールします:p+1 は次のバイトではなく次の要素に移動します。これにより p[i] は *(p+i) と等価になります。同じ配列への2つのポインタの減算は要素数を与えます。ポインタ演算は配列内でのみ有効です。

c
int arr[] = {10, 20, 30, 40, 50};
int *p = arr;       // points to arr[0]

printf("%d\n", *p);       // 10
printf("%d\n", *(p + 1)); // 20
printf("%d\n", *(p + 2)); // 30

p += 3;              // now points to arr[3]
printf("%d\n", *p);  // 40

int diff = (p - arr); // 3 (number of elements)

配列とポインタ

配列名は関数に渡される時や式で使用される時にポインタに崩壊し、サイズ情報を失います。これが配列長を別途渡さなければならない理由です。sizeof(arr) は arr がポインタではなく真の配列の場合にのみ完全な配列サイズを与えます。

c
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;  // arr decays to &arr[0]

// These are equivalent
printf("%d\n", arr[2]);
printf("%d\n", ptr[2]);
printf("%d\n", *(arr + 2));

// But sizeof differs
printf("%zu\n", sizeof(arr));   // 20 (5 * 4 bytes)
printf("%zu\n", sizeof(ptr));   // 8 (pointer size)

多次元配列

2次元配列は配列の配列で、行優先で格納されます。grid[i][j] は行 i、列 j にアクセスします。関数に渡す場合、列数を指定しなければなりません:void foo(int arr[][3], int rows)。動的2次元配列にはポインタの配列を使用します。

c
int grid[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

printf("%d\n", grid[0][1]);  // 2
printf("%d\n", grid[1][2]);  // 6

for (int i = 0; i < 2; i++) {
    for (int j = 0; j < 3; j++) {
        printf("%d ", grid[i][j]);
    }
    printf("\n");
}
07

構造体とユニオン

構造体

構造体は異なる型の関連変数をグループ化します。メンバはドット演算子(.)でアクセスします。ブレース記法で初期化します。構造体は値渡しされます(コピー);コピーを避け元を変更するにはポインタ渡し(struct Point *)を使用します。

c
struct Point {
    int x;
    int y;
};

struct Point p = {3, 4};
printf("(%d, %d)\n", p.x, p.y);  // (3, 4)

p.x = 10;
p.y = 20;
printf("(%d, %d)\n", p.x, p.y);  // (10, 20)

typedef

typedef は型のエイリアスを作成し、struct Student の代わりに Student と書けます。構造体でよく使用され構文を簡素化します。typedef は関数ポインタ型にもエイリアスでき、コールバックをはるかに読みやすくします。

c
typedef struct {
    char name[50];
    int age;
    float gpa;
} Student;

Student s = {"Alice", 20, 3.8};
printf("%s: %d, GPA %.1f\n", s.name, s.age, s.gpa);

// typedef for other types
typedef unsigned long ulong;
typedef int (*CompareFn)(const void*, const void*);

構造体へのポインタ

構造体へのポインタがある場合、メンバにアクセスするにはアロー演算子(->)を使用します。ptr->x は (*ptr).x の短縮形です。効率のため(大きな構造体のコピーを避ける)と変更を許可するために関数に構造体ポインタを渡します。

c
typedef struct {
    int x, y;
} Point;

Point p = {3, 4};
Point *ptr = &p;

// Arrow operator (->) for pointer members
printf("%d\n", ptr->x);   // 3
ptr->y = 10;
printf("%d\n", p.y);      // 10

// Equivalent: (*ptr).x

ユニオン

ユニオンは複数の型を同じメモリに重ね合わせます — 一度に有効なメンバは1つだけです。あるメンバを設定すると他を上書きします。タイプパニング(ビットの再解釈)と、複数の型のうち1つだけが一度に必要な場合のメモリ節約に便利です。

c
union Value {
    int i;
    float f;
    char bytes[4];
};

union Value v;
v.i = 65;
printf("%d\n", v.i);          // 65
printf("%c\n", v.bytes[0]);   // 'A' (same memory)

v.f = 3.14f;
printf("%d\n", v.i);  // reinterpreted bits!

ビットフィールドと列挙型

ビットフィールドは複数の小さなフィールドを1つの int にパックし、メモリを節約します(プロトコルとハードウェアレジスタで一般的)。列挙型は名前付き整数定数を定義します(デフォルトで 0、1、2...)。より良いデバッグと型安全性のために #define の代わりに列挙型を使用してください。

c
struct Flags {
    unsigned int bold : 1;
    unsigned int italic : 1;
    unsigned int size : 6;  // 0-63
};

struct Flags f = {1, 0, 12};
printf("bold=%d, size=%d\n", f.bold, f.size);

enum Color { RED, GREEN, BLUE };
enum Color c = GREEN;
printf("%d\n", c);  // 1
08

メモリ管理

malloc と free

malloc はヒープメモリを割り当て void ポインタを返します(失敗時は NULL)。常に NULL をチェックしてください。メモリリークを避けるため、すべての malloc は free とペアにしなければなりません。free 後にポインタを NULL に設定すると use-after-free バグを防ぎます。

c
#include <stdlib.h>
int *arr = malloc(5 * sizeof(int));
if (arr == NULL) {
    fprintf(stderr, "malloc failed\n");
    return 1;
}
for (int i = 0; i < 5; i++) arr[i] = i * 2;
free(arr);  // release memory
arr = NULL; // avoid dangling pointer

calloc と realloc

calloc はメモリを割り当てゼロクリアします(ゴミがある malloc より安全)。realloc はサイズ変更します:ブロックを移動し新しいポインタを返す場合があります。realloc が失敗した場合 NULL を返しますが元のブロックは有効 — リークを避けるため一時ポインタを使用します。

c
#include <stdlib.h>
// calloc: zero-initialized
int *arr = calloc(5, sizeof(int));  // all zeros

// realloc: resize
arr = realloc(arr, 10 * sizeof(int));
if (!arr) { /* handle failure, original still valid */ }

free(arr);

スタックとヒープ

スタックメモリは自動的(関数呼び出しで割り当て/解放)で高速ですが制限があります(多くの場合 1-8 MB)。ヒープメモリは malloc/free で手動管理、はるかに大きいですが、遅くリークしやすいです。小さく短命なデータにはスタックを;大きく長命なデータにはヒープを使用します。

c
// Stack: automatic, fast, limited size
int local_var = 42;
int arr[100];  // on the stack

// Heap: manual, large, slower
int *heap_arr = malloc(1000000 * sizeof(int));

// Stack frame is freed when function returns
// Heap memory persists until explicitly freed

メモリリークとダングリングポインタ

メモリリークは割り当てられたメモリへの唯一のポインタを失った時に発生します(解放できない)。ダングリングポインタは解放されたメモリを指します — デリファレンスは未定義動作です。二重解放も未定義です。Valgrind や AddressSanitizer のようなツールがこれらのバグを検出します。

c
// Memory leak: lost the pointer, can't free
void leak(void) {
    int *p = malloc(100 * sizeof(int));
    // function returns without free -> leaked!
}

// Dangling pointer: using freed memory
int *p = malloc(sizeof(int));
free(p);
*p = 42;  // UNDEFINED BEHAVIOR!

// Double free: also undefined
free(p);  // crash likely

動的配列と文字列

動的割り当てにより実行時にサイズが決まる文字列/配列を作成できます。呼び出し元がメモリを解放する責任があります。文字列には常に strlen+1 を割り当ててください(ヌルターミネータ)。このパターン(割り当て、返却、呼び出し元が解放)は C API で一般的です。

c
#include <stdlib.h>
#include <string.h>

// Dynamic string copy
char *dup_str(const char *s) {
    char *copy = malloc(strlen(s) + 1);  // +1 for null
    if (copy) strcpy(copy, s);
    return copy;  // caller must free
}

char *name = dup_str("Alice");
printf("%s\n", name);
free(name);
09

ファイル I/O

fopen と fclose

fopen はファイルを開き FILE ポインタを返します(失敗時は NULL)。モード:r(読み取り)、w(書き込み/切り詰め)、a(追記)、r+(読み書き)、b(バイナリ)。常に NULL をチェックしてください。fclose はバッファをフラッシュしファイルを閉じます。fgets は1行を安全に読み取ります。

c
#include <stdio.h>
FILE *f = fopen("data.txt", "r");
if (!f) {
    perror("fopen failed");
    return 1;
}

char line[256];
while (fgets(line, sizeof(line), f)) {
    printf("%s", line);
}

fclose(f);

fprintf と fscanf

fprintf と fscanf は printf/scanf のように動作しますがファイルに対して。fscanf は脆弱です — フォーマットの不一致が問題を引き起こします。堅牢な解析には、fgets で行を読み取り sscanf で解析します。バッファをフラッシュしリソースを解放するため、完了時に常にファイルを閉じてください。

c
FILE *f = fopen("output.txt", "w");
fprintf(f, "Name: %s\n", "Alice");
fprintf(f, "Age: %d\n", 30);
fclose(f);

FILE *in = fopen("output.txt", "r");
char name[50];
int age;
fscanf(in, "Name: %49s\n", name);
fscanf(in, "Age: %d\n", &age);
printf("%s, %d\n", name, age);
fclose(in);

fread と fwrite(バイナリ)

fread/fwrite は生バイトを読み書きします — バイナリデータと構造体に最適。引数は:バッファ、要素サイズ、カウント、ファイル。バイナリファイルはコンパクトですがアーキテクチャ間でポータブルではありません(エンディアン、構造体パディング)。バイナリファイルは常に 'b' モードで開いてください。

c
typedef struct { int id; float score; } Record;

Record r = {1, 95.5f};
FILE *f = fopen("data.bin", "wb");
fwrite(&r, sizeof(Record), 1, f);
fclose(f);

Record r2;
FILE *in = fopen("data.bin", "rb");
fread(&r2, sizeof(Record), 1, in);
printf("id=%d, score=%.1f\n", r2.id, r2.score);
fclose(in);

fseek、ftell と rewind

fseek はファイル位置を移動します:SEEK_SET(先頭から)、SEEK_CUR(相対)、SEEK_END(末尾から)。ftell は現在位置を返します。rewind は fseek(f, 0, SEEK_SET) の短縮形です。これらはファイルのランダムアクセスを可能にし、データベースやインデックス付きルックアップに便利です。

c
FILE *f = fopen("data.txt", "r");
fseek(f, 0, SEEK_END);   // jump to end
long size = ftell(f);     // get position = file size
printf("Size: %ld bytes\n", size);

rewind(f);                // back to start
// or: fseek(f, 0, SEEK_SET);

fseek(f, 10, SEEK_SET);   // 10 bytes from start
char c = fgetc(f);
printf("Char at 10: %c\n", c);
fclose(f);

stderr と標準ストリーム

すべての C プログラムには3つのストリームがあります:stdin(キーボード)、stdout(画面)、stderr(画面、非バッファ)。エラーを stderr に書き込むことで通常出力から分離し、リダイレクトを可能にします:program 2> errors.log。stderr は非バッファのためクラッシュ前にメッセージが表示されます。

c
#include <stdio.h>
// Three standard streams: stdin, stdout, stderr
fprintf(stdout, "Normal output\n");
fprintf(stderr, "Error: something went wrong\n");

int c;
while ((c = fgetc(stdin)) != EOF) {
    fputc(c, stdout);  // echo input
}

// stderr is unbuffered (appears immediately)
// stdout is line-buffered (flushes on newline)
10

プリプロセッサとマクロ

#define 定数とマクロ

#define はテキスト置換マクロを作成します。PI のような定数は可読性と保守性を向上させます。関数のようなマクロは優先順位バグを避けるためパラメータを括弧で囲む必要があります:括弧なしの SQUARE(2+3) は 2+3*2+3=11 になります。マクロより const 変数と inline 関数を優先してください。

c
#define MAX_SIZE 100
#define PI 3.14159
#define VERSION "2.0"

// Function-like macro
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int area = SQUARE(5);    // 25
int big = MAX(3, 7);     // 7

条件付きコンパイル

条件付きコンパイル(#if、#ifdef、#ifndef)はコンパイル時にコードを含める/除外します。これはプラットフォーム固有コード、デバッグビルド、フィーチャーフラグに使用されます。#ifdef はマクロが定義されているかチェック;#if はその値を評価します。#elif と #else が代替を提供します。

c
#define DEBUG 1

#if DEBUG
    printf("Debug: x=%d\n", x);
#endif

#ifdef _WIN32
    // Windows-specific code
#elif defined(__linux__)
    // Linux-specific code
#endif

#ifndef BUFFER_SIZE
#define BUFFER_SIZE 1024
#endif

インクルードガード

インクルードガードはヘッダの二重インクルードを防ぎ、これが再定義エラーを引き起こします。#ifndef/#define/#endif パターンが標準 C です。#pragma once はよりシンプルで広くサポートされた代替です(標準ではありませんがすべての主要コンパイラで動作します)。

c
// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H

struct Point { int x, y; };
void init_point(struct Point *p);

#endif // MYHEADER_H

// Alternative (non-standard but widely supported):
#pragma once

#pragma とコンパイラヒント

#pragma はコンパイラ固有のディレクティブを提供します。#pragma once はシンプルなインクルードガードです。#pragma pack は構造体のメモリレイアウトを制御します(バイナリプロトコルに重要)。__attribute__(GCC/Clang)は最適化、非推奨、警告のために関数にアノテートします。

c
#pragma once              // include guard
#pragma pack(1)            // struct packing (no padding)
#pragma GCC diagnostic ignored "-Wunused-variable"

// Common pragmas
#pragma message("Compiling " __FILE__)

// C99 __attribute__ (GCC/Clang)
__attribute__((deprecated)) void old_func(void);
__attribute__((noreturn)) void fatal(void);

文字列化とトークン貼り付け

#(文字列化)はマクロ引数を文字列リテラルに変換します。##(トークン貼り付け)はトークンを新しい識別子に連結します。2レベルの STR/XSTR パターンは文字列化前にマクロが展開されることを保証します。これらはコード生成とロギングマクロで使用されます。

c
#define STR(x) #x
#define XSTR(x) STR(x)
#define CONCAT(a, b) a##b

printf("%s\n", STR(Hello World));  // "Hello World"
printf("%s\n", XSTR(VERSION));      // expands VERSION first

int CONCAT(foo, bar) = 42;  // creates variable foobar
printf("%d\n", foobar);     // 42
11

ビット操作

基本的なビット演算子

ビット演算子は個々のビットを操作します。AND(&)はビットをマスク(設定されたビットのみ保持)、OR(|)はビットを設定、XOR(^)はビットをトグル、NOT(~)はすべてのビットを反転。左シフト(<<)は2のべき乗で乗算、右シフト(>>)は除算(符号なしの場合)。ビット操作には常に符号なし型を使用してください — 符号付き右シフトは実装定義です(符号拡張の可能性あり)。ビット操作は非常に高速(単一 CPU サイクル)で、フラグ、ハードウェアレジスタ、圧縮、暗号で使用されます。バイナリリテラル(0b プレフィックス)は C23/C++14 です;古い C では hex(0x)または10進数を使用してください。

c
#include <stdio.h>

int main() {
    unsigned int a = 0b1100;  // 12
    unsigned int b = 0b1010;  // 10

    // AND: both bits must be 1
    printf("%u\n", a & b);   // 8  (0b1000)

    // OR: either bit is 1
    printf("%u\n", a | b);   // 14 (0b1110)

    // XOR: bits differ (exclusive or)
    printf("%u\n", a ^ b);   // 6  (0b0110)

    // NOT: flip all bits
    printf("%u\n", ~a);      // 4294967283 (on 32-bit)

    // Left shift: multiply by 2^n
    printf("%u\n", a << 2);  // 48 (12 * 4)

    // Right shift: divide by 2^n (unsigned)
    printf("%u\n", a >> 1);  // 6  (12 / 2)

    return 0;
}

ビットの設定、クリアとトグル

ビットフラグは複数の boolean オプションを1つの整数にパックし、メモリを節約します。3つのコア操作:SET(|= mask)、CLEAR(&= ~mask)、TOGGLE(^= mask)、CHECK(& mask)。読みやすいフラグ名に #define で (1 << n) を使用します。このパターンはシステムプログラミングで遍在します(ファイル権限、デバイス制御、設定オプション)。例えば、Unix ファイル権限(rwxr-xr-x = 0755)はビットフラグを使用します。符号拡張の問題を避けるため、フラグには常に符号なし整数を使用してください。これは bool の配列(フラグごとに8ビット)よりメモリ効率が良いです(1ビット)。

c
#include <stdio.h>

// Flag definitions (powers of 2)
#define FLAG_READ    (1 << 0)  // 0b0001
#define FLAG_WRITE   (1 << 1)  // 0b0010
#define FLAG_EXECUTE (1 << 2)  // 0b0100
#define FLAG_ADMIN   (1 << 3)  // 0b1000

int main() {
    unsigned int permissions = 0;

    // SET a bit (OR with mask)
    permissions |= FLAG_READ | FLAG_WRITE;  // 0b0011

    // CHECK if a bit is set (AND, compare to 0)
    if (permissions & FLAG_READ) {
        printf("Read permission granted\n");
    }

    // CLEAR a bit (AND with inverted mask)
    permissions &= ~FLAG_WRITE;  // 0b0001

    // TOGGLE a bit (XOR with mask)
    permissions ^= FLAG_EXECUTE;  // 0b0101 (execute now on)
    permissions ^= FLAG_EXECUTE;  // 0b0001 (execute now off)

    // SET multiple bits at once
    permissions = FLAG_READ | FLAG_EXECUTE | FLAG_ADMIN;

    printf("Permissions: 0x%X\n", permissions);  // 0xD
    return 0;
}

ビット操作のトリック

ビットトリックは速度のためにバイナリ表現を利用します。x & 1 は奇偶をテストします(モジュロより高速)。x & (x-1) は最下位セットビットをクリア — 2のべき乗チェックとビットカウントに便利。__builtin_popcount(GCC/Clang)や __popcnt(MSVC)はモダン CPU で1命令でセットビットをカウントします。XOR スワップ(a^=b; b^=a; a^=b)は一時変数を回避しますが、モダン CPU では遅く可読性が低い — 避けてください。「2のべき乗に切り上げ」トリックは最高セットビットをすべての下位ビットに伝播し、1を加算します。これらのトリックは組み込みシステム、ゲームエンジン、パフォーマンスクリティカルなコードで便利です。

c
#include <stdio.h>

int main() {
    int x = 42;

    // Check if odd/even (faster than x % 2)
    if (x & 1) printf("odd\n"); else printf("even\n");

    // Check if power of 2 (only one bit set)
    // x & (x-1) clears the lowest set bit
    if (x && !(x & (x - 1))) printf("power of 2\n");

    // Count set bits (popcount / Hamming weight)
    unsigned int n = 0b10110110;
    int count = 0;
    while (n) { count += n & 1; n >>= 1; }
    printf("Set bits: %d\n", count);  // 6
    // Or use __builtin_popcount(n) (GCC/Clang)

    // Swap two values without temp (XOR swap)
    int a = 5, b = 10;
    a ^= b; b ^= a; a ^= b;
    // a=10, b=5 (avoid in practice — less readable)

    // Get lowest set bit
    unsigned int lowest = x & (-x);  // isolates lowest 1-bit

    // Round up to next power of 2
    unsigned int v = 5;
    v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++;

    return 0;
}

構造体内のビットフィールド

ビットフィールドは複数の小さな値を1つの構造体にパックし、メモリを節約します。コロン構文(unsigned int field : N)がビット幅を指定します。コンパイラがビット抽出/挿入を自動的に処理します。これはメモリ制約システム、ネットワークプロトコル、ハードウェアレジスタマッピングに便利です。ただし、ビットフィールドレイアウトは実装定義です(バイト順、アライメント、パディング) — クロスプラットフォームのバイナリ互換性にはビットフィールドを使用しないでください。ポータブルなバイナリフォーマットには明示的なビットマスキング(#define + & |)を使用します。名前のないフィールド(: 5)がパディングを追加します。合計サイズは構造体のアライメントに切り上げられます。

c
#include <stdio.h>

// Bit fields: pack multiple small fields into one int
struct Date {
    unsigned int day   : 5;   // 0-31  (5 bits)
    unsigned int month : 4;   // 0-15  (4 bits)
    unsigned int year  : 12;  // 0-4095 (12 bits)
    unsigned int is_leap : 1; // 0 or 1 (1 bit)
};  // Total: 22 bits (padded to 32)

struct Flags {
    unsigned int visible  : 1;
    unsigned int editable : 1;
    unsigned int locked   : 1;
    unsigned int          : 5;  // unnamed padding (5 bits)
    unsigned int priority : 4;  // 0-15
};

int main() {
    struct Date d = { 15, 6, 2024, 0 };
    printf("Size: %zu bytes\n", sizeof(d));  // 4 bytes
    printf("Date: %u/%u/%u\n", d.day, d.month, d.year);

    struct Flags f = { .visible = 1, .editable = 0, .locked = 1, .priority = 7 };
    printf("Size: %zu bytes\n", sizeof(f));  // 4 bytes

    return 0;
}

実用的なビット操作(RGB カラー)

複数の値を1つの整数にパックすることは、グラフィックス、ネットワーキング、組み込みシステムで一般的です。RGB カラーは3つの8ビットチャネルを24ビットにパックします(0xRRGGBB)。左シフト(<<)で各チャネルを配置し、OR(|)で結合します。右シフト(>>)とマスキング(& 0xFF)で個別チャネルを抽出します。これはメモリを節約し(3バイトに対して1 int)、原子的操作を可能にします。同じパターンがネットワークバイト順序、ハードウェアレジスタアクセス、データ圧縮に適用されます。ポータビリティのために常に固定幅型(uint8_t、uint32_t)を使用してください — int サイズはプラットフォームにより異なります。

c
#include <stdio.h>
#include <stdint.h>

// Pack RGB into a single 32-bit integer (0xRRGGBB)
uint32_t make_color(uint8_t r, uint8_t g, uint8_t b) {
    return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}

// Extract components
uint8_t get_red(uint32_t color)   { return (color >> 16) & 0xFF; }
uint8_t get_green(uint32_t color) { return (color >> 8)  & 0xFF; }
uint8_t get_blue(uint32_t color)  { return color & 0xFF; }

// Blend two colors (50/50 mix)
uint32_t blend(uint32_t c1, uint32_t c2) {
    uint8_t r = (get_red(c1) + get_red(c2)) / 2;
    uint8_t g = (get_green(c1) + get_green(c2)) / 2;
    uint8_t b = (get_blue(c1) + get_blue(c2)) / 2;
    return make_color(r, g, b);
}

int main() {
    uint32_t red   = make_color(255, 0, 0);    // 0xFF0000
    uint32_t blue  = make_color(0, 0, 255);    // 0x0000FF
    uint32_t purple = blend(red, blue);          // 0x7F007F

    printf("Red:   0x%06X\n", red);
    printf("Blue:  0x%06X\n", blue);
    printf("Mix:   0x%06X (R=%d G=%d B=%d)\n",
           purple, get_red(purple), get_green(purple), get_blue(purple));

    return 0;
}
12

シグナル処理

基本的なシグナル処理

シグナルはプロセスに送られるソフトウェア割り込みです(例:Ctrl+C は SIGINT を送信、ゼロ除算は SIGFPE を送信)。signal() はハンドラ関数を登録します。ハンドラ内では、async-signal-safe 関数のみが許可されます — printf、malloc、ほとんどの stdlib 関数は安全ではありません(メインプログラムが呼び出し中に中断される可能性があるため)。出力には write() を使用します。一般的なシグナル:SIGINT(Ctrl+C)、SIGTERM(終了要求)、SIGKILL(強制終了、キャッチ不可)、SIGSEGV(セグメンテーション違反)、SIGALRM(タイマー)。ポータビリティと制御のために signal() より sigaction() を優先してください。

c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

// Signal handler function (must match signature)
void handler(int sig) {
    // WARNING: only async-signal-safe functions allowed here!
    // printf is NOT safe — use write() instead
    const char *msg = "Caught SIGINT\n";
    write(STDOUT_FILENO, msg, 14);
}

int main() {
    // Register handler for Ctrl+C (SIGINT)
    signal(SIGINT, handler);

    // Ignore SIGINT entirely
    // signal(SIGINT, SIG_IGN);

    // Reset to default behavior (terminate)
    // signal(SIGINT, SIG_DFL);

    printf("PID %d waiting. Press Ctrl+C...\n", getpid());
    while (1) {
        sleep(1);
    }
    return 0;
}

sigaction(ポータブルなシグナル処理)

sigaction() はモダンでポータブルなシグナル処理方法です(signal() の動作はプラットフォームにより異なります)。sa_sigaction ハンドラは詳細を含む siginfo_t を受け取ります:si_pid(送信者 PID)、si_uid(送信者 UID)、si_signo(シグナル番号)、si_code(理由)。SA_SIGINFO フラグが3引数ハンドラを有効にします。sa_mask はハンドラ実行中に指定シグナルをブロックします(ネストされた割り込みを防止)。他のフラグ:SA_RESTART(中断されたシステムコールを自動再開)、SA_NOCLDWAIT(ゾンビ子プロセスなし)。本番コードでは常に sigaction() を使用してください — signal() は一部のプラットフォームで信頼性がありません。

c
#include <stdio.h>
#include <signal.h>
#include <string.h>

void handler(int sig, siginfo_t *info, void *context) {
    // siginfo_t provides details about the signal
    printf("Signal %d from PID %d\n", sig, info->si_pid);
}

int main() {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));

    sa.sa_sigaction = handler;  // use sa_sigaction (not sa_handler)
    sa.sa_flags = SA_SIGINFO;   // enable siginfo_t parameter

    // Block other signals during handler execution
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGQUIT);  // block SIGQUIT during handler

    // Register (more portable than signal())
    sigaction(SIGINT, &sa, NULL);

    // Send a signal to self
    raise(SIGINT);  // like kill(getpid(), SIGINT)

    printf("Done\n");
    return 0;
}

シグナルの送信とアラーム

alarm(seconds) は指定時間後に SIGALRM をスケジュールします — タイムアウトに便利。pause() はシグナルが到着するまでブロックします。volatile sig_atomic_t はシグナルハンドラとメインコード間でデータを共有する唯一の安全な方法 — volatile はコンパイラ最適化を防ぎ、sig_atomic_t は原子的アクセスを保証します。kill(pid, signal) は別のプロセスにシグナルを送信します。raise(sig) は自分自身にシグナルを送信します。SIGKILL(9)と SIGSTOP はキャッチまたは無視できません — 常に動作します。SIGTERM(15)は丁寧な終了要求です(プログラムはクリーンアップのためにキャッチできます)。シンプルなタイムアウトには alarm() を;より制御には setitimer()/timer_create() を使用します。

c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

volatile sig_atomic_t got_alarm = 0;

void alarm_handler(int sig) {
    got_alarm = 1;  // safe: sig_atomic_t is atomic
}

int main() {
    signal(SIGALRM, alarm_handler);

    // Set a timer: deliver SIGALRM after 3 seconds
    alarm(3);
    printf("Waiting for alarm...\n");

    // Wait for the alarm
    while (!got_alarm) {
        pause();  // sleep until any signal arrives
    }
    printf("Alarm fired!\n");

    // Send signal to another process
    // kill(pid, SIGTERM);  // request termination
    // kill(pid, SIGKILL);  // force kill (can't be caught)

    // Send signal to self
    raise(SIGUSR1);

    return 0;
}

一般的なシグナルリファレンス

シグナルの理解は Unix プログラミングに不可欠です。SIGKILL(9)と SIGSTOP はキャッチできません — 最後の手段です。SIGTERM は標準のグレースフルシャットダウンシグナルです(状態を保存するためにキャッチ)。SIGINT は Ctrl+C です(インタラクティブな割り込み)。SIGCHLD は子プロセス終了時に発火 — wait() しないと子はゾンビになります。SIGCHLD を SIG_IGN に設定すると子を自動回収します(または SA_NOCLDWAIT を使用)。SIGPIPE はクローズされたパイプ/ソケットへの書き込み時に発火 — ほとんどのサーバーは無視し(signal(SIGPIPE, SIG_IGN))、代わりに write() の戻り値をチェックします。シグナルハンドラでは exit() ではなく _exit() を使用してください — exit() はシグナルセーフでない可能性のある atexit ハンドラを実行します。

c
#include <signal.h>

// Common POSIX signals:
// SIGINT  (2)  - Ctrl+C interrupt (terminate)
// SIGQUIT (3)  - Ctrl+\ quit (core dump)
// SIGKILL (9)  - Force kill (CANNOT be caught/ignored)
// SIGSEGV (11) - Segmentation fault (invalid memory access)
// SIGPIPE (13) - Write to broken pipe
// SIGTERM (15) - Termination request (graceful shutdown)
// SIGSTOP (19) - Pause process (CANNOT be caught/ignored)
// SIGCONT (18) - Resume paused process
// SIGCHLD (17) - Child process exited
// SIGALRM (14) - Timer alarm
// SIGUSR1 (10) - User-defined signal 1
// SIGUSR2 (12) - User-defined signal 2
// SIGFPE  (8)  - Arithmetic error (divide by zero)
// SIGBUS  (7)  - Bus error (misaligned access)

// Graceful shutdown pattern:
void cleanup_handler(int sig) {
    // Save state, close files, release resources
    // Then exit cleanly
    _exit(0);  // use _exit in signal handlers (not exit)
}

// In main:
signal(SIGTERM, cleanup_handler);
signal(SIGINT, cleanup_handler);

// Prevent zombie children:
signal(SIGCHLD, SIG_IGN);  // auto-reap children

セルフパイプトリック(シグナルセーフなウェイクアップ)

セルフパイプトリックは根本的な問題を解決します:シグナルハンドラは安全に複雑な作業ができないが、メインループでシグナルに応答する必要がある。解決策:ハンドラがパイプにバイトを書き込み、メインループが select()/poll() でそれを検出します。これによりシグナルがイベントループに安全に統合されます。ハンドラは write() のみを呼び出します(async-signal-safe)。モダンな代替:signalfd()(Linux 固有、シグナルを直接ファイル記述子に変換)または pselect()(select 中に原子的にシグナルをブロック)。このパターンはイベント駆動サーバー(nginx、Redis)でレース条件なしにシグナルを処理するために使用されます。

c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>

int pipe_fd[2];  // [0]=read, [1]=write

void handler(int sig) {
    // Write one byte to the pipe — wakes up select()/poll()
    write(pipe_fd[1], &sig, sizeof(sig));
}

int main() {
    pipe(pipe_fd);
    // Make read end non-blocking
    fcntl(pipe_fd[0], F_SETFL, O_NONBLOCK);

    signal(SIGINT, handler);
    signal(SIGTERM, handler);

    printf("Waiting (select-based)...\n");

    while (1) {
        fd_set readfds;
        FD_ZERO(&readfds);
        FD_SET(pipe_fd[0], &readfds);

        // select() blocks until pipe is writable (signal received)
        int ready = select(pipe_fd[0] + 1, &readfds, NULL, NULL, NULL);
        if (ready > 0 && FD_ISSET(pipe_fd[0], &readfds)) {
            int sig;
            read(pipe_fd[0], &sig, sizeof(sig));
            printf("Handled signal %d in main loop\n", sig);
            if (sig == SIGTERM) break;
        }
    }
    return 0;
}
13

プロセスの Fork と Exec

fork() の基礎

fork() は現在のプロセスの正確なコピーを作成します — 唯一の違いは戻り値です:子では 0、親では子の PID。両プロセスが fork() 呼び出しから続行します。子は親のメモリのコピーを取得します(copy-on-write がこれを最適化)。3つのケースすべてを常にチェックしてください:pid < 0(エラー)、pid == 0(子)、pid > 0(親)。waitpid() は子が終了するまでブロックしステータスを取得します。WIFEXITED は正常終了したかチェック、WEXITSTATUS は終了コードを取得。wait() しないと子は回収されるまでゾンビになります。

c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();  // create a child process

    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // CHILD process (fork returned 0)
        printf("Child: PID=%d, Parent PID=%d\n",
               getpid(), getppid());
        sleep(2);
        printf("Child exiting\n");
        return 42;  // child exit code
    } else {
        // PARENT process (fork returned child's PID)
        printf("Parent: PID=%d, Child PID=%d\n",
               getpid(), pid);

        int status;
        waitpid(pid, &status, 0);  // wait for child

        if (WIFEXITED(status)) {
            printf("Child exited with code %d\n",
                   WEXITSTATUS(status));
        }
    }
    return 0;
}

exec ファミリ(プロセスイメージの置換)

exec は現在のプロセスイメージを新しいプログラムで置換します — PID は同じままですが、コード、データ、スタックが置換されます。exec は失敗時にのみ戻ります。命名規則:'l' = 引数リスト(可変長、NULL 終端)、'v' = 引数のベクター/配列、'p' = PATH を検索、'e' = カスタム環境。最初の引数は慣習的にプログラム名(argv[0])です。fork()+exec() が Unix でプログラムを起動する方法です — fork がプロセスを作成、exec が新しいプログラムを読み込み。この分離により fork と exec の間にファイル記述子、環境、シグナルを設定できます。

c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        // CHILD: replace self with a new program
        // exec never returns on success (only on failure)

        // execlp: search PATH, list arguments
        execlp("ls", "ls", "-la", "/tmp", NULL);

        // execvp: search PATH, array of arguments
        char *args[] = {"ls", "-la", "/tmp", NULL};
        execvp("ls", args);

        // execl: full path, list arguments
        execl("/bin/ls", "ls", "-la", NULL);

        // Only reached if exec failed
        perror("exec failed");
        _exit(1);
    } else {
        wait(NULL);  // parent waits for child
        printf("Child finished\n");
    }
    return 0;
}

// exec variants:
// execl  (path, arg1, arg2, ..., NULL)        — list args, full path
// execlp (file, arg1, arg2, ..., NULL)        — list args, search PATH
// execv  (path, argv[])                        — array args, full path
// execvp (file, argv[])                        — array args, search PATH
// execve (path, argv[], envp[])                — array args, custom env

ゾンビと孤児プロセス

ゾンビは子が終了したが親が wait() を呼んでいない時に発生 — カーネルは回収されるまでプロセステーブルエントリ(PID、終了ステータス)を保持します。ゾンビは PID を浪費しプロセステーブルを枯渇させる可能性があります。修正:常に子を wait() するか、SIGCHLD を SIG_IGN に設定(カーネルが自動回収)。孤児は親が子より先に終了した時に発生 — init/systemd(PID 1)が孤児を引き取り終了時に回収します。ダブルフォークパターン(fork、子が再度 fork、最初の子が終了)は init に自動的に再親化されるデーモンを作成し、端末から切り離します。'ps aux | grep Z' または 'top' でゾンビを監視します。

c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        printf("Child PID=%d\n", getpid());
        _exit(0);  // child exits immediately
    }

    // If parent doesn't wait(), child becomes a ZOMBIE
    // (process table entry remains until reaped)
    sleep(5);  // parent sleeps — child is now a zombie
    // Run 'ps' during this window to see the zombie (state 'Z')

    // Reap the zombie:
    int status;
    waitpid(pid, &status, 0);
    printf("Zombie reaped\n");

    // Orphan: if parent exits before child
    pid_t pid2 = fork();
    if (pid2 == 0) {
        sleep(3);  // parent will exit first
        printf("Orphan adopted by init (PID 1), new parent=%d\n",
               getppid());
        _exit(0);
    }
    // Parent exits immediately — child becomes orphan
    // init/systemd (PID 1) adopts and reaps it

    // Prevent zombies: ignore SIGCHLD
    // signal(SIGCHLD, SIG_IGN);  // kernel auto-reaps children
    // Or use SA_NOCLDWAIT with sigaction

    return 0;
}

デーモンプロセスの作成

デーモンは端末なしで実行されるバックグラウンドプロセスです(例:Web サーバー、データベース)。デーモン化ステップ:シェルから切り離すために fork+exit、新しいセッションを作成するために setsid()(制御端末なし)、安全のため再度 fork、ファイルシステムを保持しないように chdir('/')、予測可能なファイル権限のために umask を設定、stdio を /dev/null にクローズ/リダイレクト。ダブルフォークは open() でデーモンが端末を再取得するのを防ぐ Unix の慣習です。モダンなシステムはデーモン管理のために systemd サービスファイルを提供しますが、手動デーモン化の理解は組み込みシステムとポータブルコードに依然として重要です。stdout は /dev/null のため、ファイルにログします。

c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

void daemonize() {
    // 1. Fork and exit parent (child continues in background)
    pid_t pid = fork();
    if (pid > 0) exit(0);  // parent exits
    if (pid < 0) exit(1);

    // 2. Create new session (detach from controlling terminal)
    setsid();

    // 3. Fork again (prevent reacquiring a terminal)
    pid = fork();
    if (pid > 0) exit(0);
    if (pid < 0) exit(1);

    // 4. Change working directory to / (don't hold filesystem)
    chdir("/");

    // 5. Set umask to 0 (full control over file permissions)
    umask(0);

    // 6. Close standard file descriptors (detach from terminal)
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    // 7. Redirect them to /dev/null (in case code writes to them)
    open("/dev/null", O_RDWR);  // fd 0 = stdin
    dup(0);                      // fd 1 = stdout
    dup(0);                      // fd 2 = stderr
}

int main() {
    daemonize();
    // Now running as a daemon (background, no terminal)
    while (1) {
        // Daemon work here (e.g., log to file, listen on socket)
        sleep(60);
    }
    return 0;
}

プロセス間通信(IPC)の概要

IPC はプロセス間通信を可能にします。パイプが最もシンプル(親子、単方向)。名前付きパイプ(FIFO)はファイルシステムパス経由で無関係なプロセス間で動作します。共有メモリが最速(ゼロコピー)ですが同期が必要(セマフォ/ミューテックス)。ソケットが最も柔軟(双方向、ネットワーク対応)。メッセージキューは構造化されたメッセージ境界通信を提供します。シグナルは最小限(単なる番号)。ニーズに基づいて選択:シンプルな親子にはパイプ、高性能データ共有には共有メモリ、ネットワーク通信にはソケット。System V IPC(shmget、semget)は古い;POSIX IPC(shm_open、sem_open)はよりクリーンですが普遍的に利用可能ではありません。

c
#include <stdio.h>
// C provides several IPC mechanisms:

// 1. PIPES: unidirectional byte stream between parent/child
//    pipe(fd) creates fd[0]=read, fd[1]=write
//    Only works between related processes (fork)

// 2. NAMED PIPES (FIFOs): like pipes but have a filesystem path
//    mkfifo("/tmp/myfifo", 0666);
//    Works between unrelated processes

// 3. SHARED MEMORY: fastest IPC (both processes access same RAM)
//    shmget/shmat (System V) or shm_open/mmap (POSIX)

// 4. MESSAGE QUEUES: structured messages (not byte stream)
//    msgget/msgsnd/msgrcv (System V) or mq_open (POSIX)

// 5. SEMAPHORES: synchronization (not data transfer)
//    semget/semop (System V) or sem_open (POSIX)

// 6. SOCKETS: bidirectional, works across machines (network)
//    socket/bind/listen/accept/connect

// 7. SIGNALS: minimal data (just signal number)
//    kill(pid, SIGUSR1)

// Choosing IPC:
// - Same machine, related processes → pipes
// - Same machine, unrelated processes → named pipes, shared memory
// - Different machines → sockets
// - Need synchronization → semaphores, mutexes
// - Need structured messages → message queues
14

パイプと IPC

匿名パイプ(親子)

パイプは関連プロセス(fork で作成)間の単方向通信を提供します。pipe(fd) は2つのファイル記述子を作成:読み取り用 fd[0]、書き込み用 fd[1]。重要:各プロセスで未使用端をクローズ — 親は読み取り端を、子は書き込み端をクローズ。書き込み端がクローズされないと、子の read() は永遠にブロックします(更多データを待機)。read() はすべての書き込み端がクローズされた時にのみ 0(EOF)を返します。パイプは固定バッファ(通常 64KB)を持ちます — バッファが満杯の場合 write() はブロックします。パイプは親子通信とシェルコマンドのパイプ(ls | grep)に最適です。

c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int fd[2];  // fd[0]=read end, fd[1]=write end
    pipe(fd);   // create pipe

    pid_t pid = fork();

    if (pid == 0) {
        // CHILD: read from pipe
        close(fd[1]);  // close unused write end

        char buf[256];
        int n = read(fd[0], buf, sizeof(buf));
        buf[n] = '\0';
        printf("Child received: %s", buf);

        close(fd[0]);
    } else {
        // PARENT: write to pipe
        close(fd[0]);  // close unused read end

        const char *msg = "Hello from parent!\n";
        write(fd[1], msg, strlen(msg));

        close(fd[1]);  // close write end → child's read returns 0 (EOF)
        wait(NULL);
    }
    return 0;
}

名前付きパイプ(FIFO)

名前付きパイプ(FIFO)はファイルシステム名を持つパイプです — 無関係なプロセス間で動作します。mkfifo() がパイプファイルを作成;open() は読み取り側と書き込み側の両方が接続されるまでブロックします(組み込みの同期)。FIFO は unlink() されるまで永続します(プロセス終了時に消える匿名パイプとは異なります)。別々のプログラム間のシンプルな IPC に便利です。open() のブロック動作により、書き込み側は読み取り側の準備ができるまで開始しません。非ブロッキングオープンには O_NONBLOCK を使用します。FIFO は単方向です — 双方向通信には2つの FIFO またはソケットを使用します。名前付きパイプはシェルスクリプトとシステムサービスでよく使用されます。

c
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>

// Process A (writer):
int writer_main() {
    mkfifo("/tmp/myfifo", 0666);  // create named pipe

    int fd = open("/tmp/myfifo", O_WRONLY);
    write(fd, "Hello via FIFO!", 15);
    close(fd);
    return 0;
}

// Process B (reader) — can be a completely separate program:
int reader_main() {
    int fd = open("/tmp/myfifo", O_RDONLY);
    char buf[256];
    int n = read(fd, buf, sizeof(buf));
    buf[n] = '\0';
    printf("Received: %s\n", buf);
    close(fd);
    return 0;
}

// Named pipes persist in the filesystem (use unlink to remove):
// unlink("/tmp/myfifo");

// open() blocks until BOTH a reader and writer are connected
// (unless O_NONBLOCK is used)

共有メモリ(最速の IPC)

共有メモリは最速の IPC です — 両プロセスが同じ物理 RAM をマップするため、データ転送はゼロコピーです。shmget() がセグメントを作成、shmat() がプロセスのアドレス空間にアタッチ、shmdt() がデタッチ、shmctl(IPC_RMID) が破棄。重要な注意:共有メモリは同期を提供しません — 両プロセスが同時にアクセスするとデータ競合が発生します。アクセスを調整するためにセマフォ、ミューテックス、または他の同期を使用しなければなりません。ftok() がファイルパスからキーを生成(両プロセスがキーに同意する必要があります)。完了時に常に共有メモリを破棄してください(プロセス終了後も永続し、メモリをリークします)。POSIX 共有メモリ(shm_open/mmap)はよりクリーンな代替です。

c
#include <stdio.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <string.h>
#include <unistd.h>

#define SHM_SIZE 1024

int main() {
    key_t key = ftok("/tmp/shmfile", 65);  // generate unique key

    // Create shared memory segment
    int shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT);

    pid_t pid = fork();

    if (pid == 0) {
        // CHILD: attach and read
        char *shared = (char *)shmat(shmid, NULL, 0);
        sleep(1);  // wait for parent to write
        printf("Child reads: %s\n", shared);
        shmdt(shared);  // detach
    } else {
        // PARENT: attach and write
        char *shared = (char *)shmat(shmid, NULL, 0);
        strcpy(shared, "Hello from shared memory!");
        printf("Parent wrote to shared memory\n");
        shmdt(shared);
        wait(NULL);

        // Destroy shared memory after use
        shmctl(shmid, IPC_RMID, NULL);
    }
    return 0;
}

// WARNING: shared memory has NO synchronization!
// Use semaphores or mutexes to prevent race conditions.

dup2 とリダイレクト

dup2(oldfd, newfd) は newfd を oldfd のコピーにします — これがシェルリダイレクトの仕組みです。stdout をパイプにリダイレクト:dup2(pipe_write, STDOUT_FILENO) — これで stdout への printf/write がパイプに入ります。stdin をパイプからリダイレクト:dup2(pipe_read, STDIN_FILENO) — これで stdin からの scanf/read がパイプから来ます。これがシェルがパイプ(ls | sort)、リダイレクト(ls > file)、入力(sort < file)を実装する方法です。dup2 後、元の fd をクローズします(複製されました)。このパターンは Unix パイプラインをプログラムで構築する基盤であり、シェル、popen()、プロセス管理ライブラリで使用されます。

c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>

// Implement shell-like pipeline: ls | sort
int main() {
    int pipefd[2];
    pipe(pipefd);

    pid_t pid1 = fork();
    if (pid1 == 0) {
        // First child: ls (writes to pipe instead of stdout)
        close(pipefd[0]);                    // close read end
        dup2(pipefd[1], STDOUT_FILENO);      // stdout → pipe write
        close(pipefd[1]);                    // close original (dup'd)
        execlp("ls", "ls", NULL);
    }

    pid_t pid2 = fork();
    if (pid2 == 0) {
        // Second child: sort (reads from pipe instead of stdin)
        close(pipefd[1]);                    // close write end
        dup2(pipefd[0], STDIN_FILENO);       // stdin → pipe read
        close(pipefd[0]);                    // close original (dup'd)
        execlp("sort", "sort", NULL);
    }

    // Parent: close both ends and wait
    close(pipefd[0]);
    close(pipefd[1]);
    waitpid(pid1, NULL, 0);
    waitpid(pid2, NULL, 0);

    return 0;
}

popen(高レベルパイプ)

popen() は fork+pipe+exec+shell の高レベルラッパーです — /bin/sh 経由でコマンドを実行し、出力読み取り('r')または入力書き込み('w')用の FILE* を返します。手動 fork/pipe/exec よりはるかにシンプルですが、シェル経由で実行されるため、信頼できない入力を絶対に渡さないでください(シェルインジェクションリスク)。返された FILE* で通常ファイルのように fgets/fprintf を使用します。pclose() がパイプをクローズし子の終了を待機します(ステータスを返します)。信頼できない入力には、直接 fork+execvp を使用します(シェルなし)。popen はクイックスクリプト、システム管理ツール、コマンド出力の読み取りに最適です。双方向通信には socketpair() または2つのパイプを使用します。

c
#include <stdio.h>
#include <stdlib.h>

int main() {
    // popen opens a process with a pipe (like shell command | ...)
    FILE *fp = popen("ls -la /tmp", "r");
    if (fp == NULL) {
        perror("popen failed");
        return 1;
    }

    // Read command output line by line
    char buf[256];
    while (fgets(buf, sizeof(buf), fp) != NULL) {
        printf(">> %s", buf);
    }

    pclose(fp);  // closes pipe and waits for child

    // Writing to a process (like ... | command):
    FILE *wp = popen("grep hello", "w");
    fprintf(wp, "hello world\n");
    fprintf(wp, "goodbye\n");
    pclose(wp);  // grep outputs "hello world"

    return 0;
}

// popen is simpler than fork+pipe+exec but:
// - Runs via /bin/sh (shell injection risk with user input!)
// - Less control over the child process
// - Use fork+exec directly for untrusted input
15

Makefile とビルドツール

基本的な Makefile 構造

Make はコンパイルを自動化します。Makefile にはルールがあります:ターゲット(ビルドするファイル)、前提条件(依存関係)、レシピ(シェルコマンド、TAB インデント)。変数(CC、CFLAGS)が設定を一元化します。自動変数:$@(ターゲット名)、$<(最初の前提条件)、$^(すべての前提条件)。パターンルール(%.o: %.c)がすべてのソースファイルのコンパイルを一般化します。.PHONY はファイルではないターゲット(clean、all、install)を宣言します。最初のルールがデフォルトです(引数なしの make は 'all' をビルド)。Make はファイルのタイムスタンプを追跡 — 前提条件がターゲットより新しい場合のみ再ビルドします。この段階的ビルドが大きなプロジェクトで時間を節約します。

c
# Makefile — build automation for C/C++ projects
# Rule syntax: target: prerequisites
#               recipe (must start with TAB, not spaces)

# Variables
CC = gcc
CFLAGS = -Wall -Wextra -g -O2
TARGET = myapp
SRCS = main.c utils.c parser.c
OBJS = $(SRCS:.c=.o)  # substitute .c with .o

# Default target (first rule)
all: $(TARGET)

# Link object files into executable
$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^

# Compile each .c to .o
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# Clean build artifacts
clean:
	rm -f $(OBJS) $(TARGET)

# Run the program
run: $(TARGET)
	./$(TARGET)

# Phony targets (not files)
.PHONY: all clean run

# Usage:
# $ make        — builds 'all' (the default)
# $ make clean  — removes build files
# $ make run    — builds and runs
# $ make -j4    — parallel build (4 jobs)

自動変数とパターンルール

自動変数により Makefile が簡潔で保守しやすくなります。$@(ターゲット)、$<(最初の前提条件)、$^(すべての前提条件)が最も一般的です。パターンルール(%.o: %.c)で1つのルールですべてのソースファイルを記述できます — % は任意の文字列にマッチします。$(wildcard) が glob にマッチするファイルを見つけ、$(patsubst) が文字列を変換 — 一緒に使うとソースを自動発見します。@ プレフィックスがコマンドのエコーを抑制します。静的パターンルール(target: %.o: %.c)は特定のリストに適用されます。これらの機能を理解すると反復的なルールが排除され、Makefile が大きなプロジェクトにスケールします。レシピのインデントには常に(スペースではなく)TAB を使用してください — Make はこれに厳格です。

c
# Automatic variables in recipes:
# $@   — the target filename
# $<   — the first prerequisite
# $^   — all prerequisites (no duplicates)
# $+   — all prerequisites (with duplicates)
# $?   — prerequisites newer than the target
# $*   — the stem (matching % part)

# Example showing all automatic variables:
program: main.o utils.o
	@echo "Target: $@"        # program
	@echo "First dep: $<"     # main.o
	@echo "All deps: $^"      # main.o utils.o
	@echo "Newer deps: $?"    # (whichever changed)
	gcc -o $@ $^

# Pattern rule: compile any .c to .o
%.o: %.c
	gcc -c $< -o $@
# $< = source (.c file), $@ = target (.o file)

# Static pattern rule (specific files):
$(OBJS): %.o: %.c
	gcc -c $< -o $@

# Built-in functions:
SRCS = $(wildcard src/*.c)           # find all .c files
OBJS = $(patsubst src/%.c,build/%.o,$(SRCS))  # path substitution
DIRS = $(sort $(dir $(SRCS)))         # unique directories

依存関係とヘッダファイル

ヘッダの依存関係の追跡は重要です — これがないと、.h ファイルを変更してもそれをインクルードする .c ファイルの再コンパイルがトリガーされず、古いビルドになります。解決策:gcc -MMD -MP がすべての依存関係(ヘッダを含む)をリストした .d ファイルを生成します。-include がこれらを Makefile に取り込みます。-MP がヘッダの偽ターゲットを追加します(ヘッダが削除された場合のエラーを防止)。これが C/C++ プロジェクトの標準アプローチです。これがないと、すべてのヘッダ依存関係を手動でリストしなければなりません — 大きなプロジェクトでは管理不能です。最初のビルドには .d ファイルがありません(-include の '-' がエラーを抑制);コンパイル中に作成され、後続ビルドで使用されます。

c
# When a header file changes, dependent .c files must recompile
# Make doesn't track this automatically — use gcc -MMD

CC = gcc
CFLAGS = -Wall -MMD -MP  # generate .d dependency files

SRCS = $(wildcard src/*.c)
OBJS = $(SRCS:.c=.o)
DEPS = $(OBJS:.o=.d)    # dependency files

all: myapp

myapp: $(OBJS)
	$(CC) -o $@ $^

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# Include auto-generated dependency files
-include $(DEPS)  # '-' means don't error if missing

clean:
	rm -f $(OBJS) $(DEPS) myapp

.PHONY: all clean

# How it works:
# 1. gcc -MMD creates main.d next to main.o
# 2. main.d contains: main.o: main.c utils.h parser.h
# 3. -include pulls these in, so Make knows header dependencies
# 4. If utils.h changes, main.o rebuilds automatically

マルチディレクトリプロジェクトの Makefile

実際のプロジェクトは複数のディレクトリにまたがります。この Makefile はソースを自動発見(wildcard)し、ビルドディレクトリにマッピング(patsubst)し、必要に応じてディレクトリを作成します。| 構文が order-only 前提条件を作成 — $(BUILDDIR) はコンパイル前に作成されますが、そのタイムスタンプ変更は再ビルドをトリガーしません(| なしではディレクトリ作成が毎回すべてを再ビルドさせます)。-Iinclude が gcc にヘッダの場所を伝えます。-MMD がビルドディレクトリに依存関係ファイルを生成します。この構造はソース、ビルド、バイナリディレクトリを分離 — クリーンが簡単(rm -rf build)でソースツリーを汚染しません。非常に大きなプロジェクトには CMake または Meson を検討してください。

c
# Project structure:
# project/
#   src/       — source files
#   include/   — header files
#   build/     — object files (created by make)
#   bin/       — final executable

CC = gcc
CFLAGS = -Wall -Iinclude -g
SRCDIR = src
INCDIR = include
BUILDDIR = build
BINDIR = bin

TARGET = $(BINDIR)/myapp
SRCS = $(wildcard $(SRCDIR)/*.c)
OBJS = $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(SRCS))
DEPS = $(OBJS:.o=.d)

all: $(TARGET)

$(TARGET): $(OBJS) | $(BINDIR)
	$(CC) -o $@ $^

# Order-only prerequisite: create build dir before compiling
$(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR)
	$(CC) $(CFLAGS) -MMD -c $< -o $@

$(BUILDDIR):
	mkdir -p $(BUILDDIR)

$(BINDIR):
	mkdir -p $(BINDIR)

-include $(DEPS)

clean:
	rm -rf $(BUILDDIR) $(BINDIR)

.PHONY: all clean

# Order-only prerequisites (| syntax) create directories
# without triggering rebuilds when the dir timestamp changes

CMake の基礎(Make の代替)

CMake はメタビルドシステムです — CMakeLists.txt ファイルから Makefile(または Ninja、Visual Studio、Xcode プロジェクト)を生成します。クロスプラットフォームコンパイル、依存関係検出、IDE 統合を処理するため、C/C++ プロジェクトの事実上の標準です。主要コマンド:project() がプロジェクト名を設定、add_executable() がビルドターゲットを定義、target_include_directories() がヘッダパスを追加、target_link_libraries() がライブラリをリンク。ソース外ビルド(mkdir build && cd build && cmake ..)がソースツリーをクリーンに保ちます。CMake はプラットフォームごとにコンパイラとフラグを自動検出します。新しい C/C++ プロジェクトには、生の Makefile より CMake を優先してください — より保守しやすくポータブルです。

c
# CMakeLists.txt — CMake is a cross-platform build generator
# It generates Makefiles (or Ninja, VS, Xcode projects)

cmake_minimum_required(VERSION 3.10)
project(MyApp C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra")

# Add executable from source files
add_executable(myapp src/main.c src/utils.c src/parser.c)

# Include directory
target_include_directories(myapp PRIVATE include)

# Link a library
target_link_libraries(myapp m)  # math library (-lm)

# Build type flags
set(CMAKE_C_FLAGS_DEBUG "-g -O0")
set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG")

# Usage:
# $ mkdir build && cd build
# $ cmake ..            # generate Makefiles
# $ make                # build
# $ make install        # install (optional)
# $ cmake .. -DCMAKE_BUILD_TYPE=Debug  # debug build

# Out-of-source builds keep source tree clean
# CMake is the standard for C/C++ cross-platform projects
16

関数ポインタの深掘り

関数ポインタの構文と使用法

関数ポインタは関数のアドレスを格納し、ランタイムディスパッチを可能にします。構文 int (*fp)(int, int) は悪名高く紛らわしい — 'fp は (int, int) を取り int を返す関数へのポインタ' と読みます。typedef がこれを簡素化します:typedef int (*math_func)(int, int) が読みやすいエイリアスを作成。関数名はポインタに崩壊するため(配列名のように)'add' と '&add' は等価です。関数ポインタはコールバック、イベントハンドラ、ストラテジーパターン、ディスパッチテーブル(switch のようなディスパッチ用の関数ポインタ配列)を可能にします。これらは qsort のコンパレータと GUI イベントシステムの基盤です。

c
#include <stdio.h>

// Function pointer syntax: return_type (*name)(param_types)
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }

int main() {
    // Declare a function pointer
    int (*operation)(int, int);

    // Assign (function name decays to pointer)
    operation = add;          // or &add
    printf("5 + 3 = %d\n", operation(5, 3));  // 8

    operation = subtract;
    printf("5 - 3 = %d\n", operation(5, 3));  // 2

    // typedef for readability
    typedef int (*math_func)(int, int);
    math_func fn = add;
    printf("Result: %d\n", fn(10, 20));

    // Array of function pointers (dispatch table)
    math_func ops[] = {add, subtract};
    printf("op[0](4,2)=%d  op[1](4,2)=%d\n",
           ops[0](4, 2), ops[1](4, 2));

    return 0;
}

コールバック(qsort の例)

qsort はコールバックとしての関数ポインタの古典的な例です。コンパレータは const void* ポインタ(ジェネリック)を受け取り、順序を示す整数を返します。qsort はコンパレータを呼び出して要素の順序を決定します — 異なる関数を渡すことでソート動作を制御します。これが C でのストラテジーパターンです:アルゴリズム(qsort)は固定ですが、比較ロジックが注入されます。void* がジェネリックプログラミングを可能にします(任意の型をソート)。コンパレータは純粋関数(副作用なし)で一貫性が必要です(a<b かつ b<c なら a<c)。このパターンは C 標準ライブラリ全体で使用されます(bsearch、atexit、signal)。

c
#include <stdio.h>
#include <stdlib.h>

// Comparator function for qsort
// Returns: negative if a<b, 0 if equal, positive if a>b
int compare_asc(const void *a, const void *b) {
    return (*(int *)a - *(int *)b);
}

int compare_desc(const void *a, const void *b) {
    return (*(int *)b - *(int *)a);
}

int main() {
    int arr[] = {5, 2, 8, 1, 9, 3, 7, 4, 6};
    int n = sizeof(arr) / sizeof(arr[0]);

    // qsort takes a function pointer as the comparator
    qsort(arr, n, sizeof(int), compare_asc);
    printf("Ascending: ");
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");

    // Same array, different comparator (descending)
    qsort(arr, n, sizeof(int), compare_desc);
    printf("Descending: ");
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");

    return 0;
}

関数ポインタ付き構造体(C での OOP)

C は関数ポインタ付き構造体で OOP をシミュレートできます — これが C++ の vtable が内部で動作する方法です。構造体は関数ポインタを保持する 'vtable'(仮想関数テーブル)へのポインタを含みます。各 'サブクラス'(Circle、Square)は独自の実装を持つ独自の vtable を持ちます。Circle* を Shape* にキャストするとポリモーフィズムが可能 — print_shape() が vtable を通じて正しい area() 関数を呼び出します。このパターンは実際の C コードで使用されます:Linux カーネル(デバイスドライバ)、GObject(GTK)、SQLite。カプセル化、継承(構造体埋め込み経由)、ポリモーフィズムを提供します。C++ に比べて冗長ですが、メモリレイアウトと仮想ディスパッチの完全な制御を与えます。

c
#include <stdio.h>

// Simulating OOP with structs + function pointers
typedef struct Shape Shape;

// Virtual function table (vtable)
typedef struct {
    double (*area)(Shape *);
    double (*perimeter)(Shape *);
    void (*describe)(Shape *);
} ShapeVTable;

struct Shape {
    const ShapeVTable *vtable;  // pointer to virtual functions
    char name[32];
};

// Circle implementation
typedef struct {
    Shape base;      // inherit from Shape
    double radius;
} Circle;

double circle_area(Shape *s) {
    return 3.14159 * ((Circle *)s)->radius * ((Circle *)s)->radius;
}

double circle_perimeter(Shape *s) {
    return 2 * 3.14159 * ((Circle *)s)->radius;
}

static const ShapeVTable circle_vtable = {
    circle_area, circle_perimeter, NULL
};

Circle *circle_create(double r) {
    Circle *c = malloc(sizeof(Circle));
    c->base.vtable = &circle_vtable;
    strcpy(c->base.name, "Circle");
    c->radius = r;
    return c;
}

// Polymorphic function (works with any Shape)
void print_shape(Shape *s) {
    printf("%s: area=%.2f, perimeter=%.2f\n",
           s->name, s->vtable->area(s), s->vtable->perimeter(s));
}

コールバックによるイベント駆動プログラミング

関数ポインタは C でイベント駆動アーキテクチャを可能にします — パブリッシュ/サブスクライブパターン。ハンドラは on_event() で登録し(サブスクライブ)、emit_event() がすべての登録ハンドラを呼び出します(パブリッシュ)。これによりイベントプロデューサーとコンシューマーが切り離されます — エミッターはハンドラが何をするか知りません。このパターンは GUI フレームワーク(ボタンクリック → ハンドラ)、ゲームエンジン(衝突 → コールバック)、非同期 I/O(データ準備完了 → 読み取りハンドラ)の基盤です。ハンドラシグネチャ(イベント名 + void* データ)は任意のイベント型に十分ジェネリックです。本番では、エラー処理(ハンドラがクラッシュしたら?)、優先順序付け、サブスクライブ解除機能を追加します。これが libuv、libevent、Node.js の根底で動作する仕組みです。

c
#include <stdio.h>

// Event system using function pointers
typedef void (*EventHandler)(const char *event, void *data);

// Simple event emitter
#define MAX_HANDLERS 10
static EventHandler handlers[MAX_HANDLERS];
static int handler_count = 0;

void on_event(EventHandler handler) {
    if (handler_count < MAX_HANDLERS) {
        handlers[handler_count++] = handler;
    }
}

void emit_event(const char *event, void *data) {
    for (int i = 0; i < handler_count; i++) {
        handlers[i](event, data);  // call each registered handler
    }
}

// Concrete handlers
void log_handler(const char *event, void *data) {
    printf("[LOG] Event: %s\n", event);
}

void alert_handler(const char *event, void *data) {
    if (strcmp(event, "error") == 0) {
        printf("[ALERT] Error occurred!\n");
    }
}

int main() {
    // Register handlers (subscribe)
    on_event(log_handler);
    on_event(alert_handler);

    // Emit events (publish)
    emit_event("click", NULL);
    emit_event("error", NULL);
    emit_event("scroll", NULL);

    return 0;
}

関数ポインタの落とし穴

関数ポインタにはいくつかの落とし穴があります。NULL 関数ポインタの呼び出しはクラッシュします(セグメンテーション違反) — 呼び出し前に常に NULL チェックしてください。間違ったシグネチャへのキャストは未定義動作です(呼び出し規約が異なる場合があります)。関数ポインタの等価比較は有効です(同じ関数)が、順序付け(<、>)は未定義です。typedef を一貫して使用してください — 関数ポインタ構文はエラーを起こしやすく、typedef が宣言を読みやすく保守しやすくします。C では、関数ポインタはランタイムポリモーフィズムとコールバックを達成する唯一の方法なので、習得が不可欠です。C++ はより安全な代替として std::function、ラムダ、仮想関数を追加します。

c
#include <stdio.h>

// PITFALL 1: Calling a NULL function pointer (crash!)
void bad_call() {
    void (*fp)(void) = NULL;
    fp();  // SEGFAULT — always check for NULL
    if (fp) fp();  // safe
}

// PITFALL 2: Wrong signature (undefined behavior)
void takes_int(int x) { printf("%d\n", x); }
void wrong_sig() {
    void (*fp)(void) = (void (*)(void))takes_int;  // WRONG cast
    fp();  // UB: missing argument, garbage value
}

// PITFALL 3: Function pointer to a local function (dangling)
typedef int (*callback_t)(int);
callback_t get_callback() {
    // Returning pointer to local function is OK (functions aren't local)
    // But returning a pointer to a local VARIABLE is not
    return NULL;  // functions have static storage, safe to return
}

// PITFALL 4: Comparing function pointers
int f1(int x) { return x; }
int f2(int x) { return x; }
void compare_fps() {
    int (*p1)(int) = f1;
    int (*p2)(int) = f1;
    if (p1 == p2) printf("Same function\n");  // OK
    // Comparing p1 == f2 is valid but they're different functions
}

// GOOD: Always use typedef for complex function pointers
// typedef int (*comparator_t)(const void *, const void *);
// This makes declarations readable and consistent
17

可変長引数(varargs)

基本的な可変長引数関数(stdarg)

可変長引数関数は stdarg.h を使用して可変個の引数を受け取ります。va_list が引数リストを保持、va_start が初期化(... の前の最後の名前付きパラメータが必要)、va_arg が指定型で次の引数を取得、va_end がクリーンアップ。関数はいくつ引数を読むかを知る必要があります — カウントパラメータ(printf のフォーマット文字列など)またはセンチネル値(NULL 終端)経由。'...' は常に最後のパラメータでなければなりません。va_arg は型チェックをしません — 間違った型を渡すと未定義動作です。これが printf、fprintf、execl の動作方法です。

c
#include <stdio.h>
#include <stdarg.h>

// Variadic function: takes variable number of arguments
// The '...' must be the LAST parameter
int sum(int count, ...) {
    va_list args;           // argument list type
    va_start(args, count);  // initialize (needs last named param)

    int total = 0;
    for (int i = 0; i < count; i++) {
        int val = va_arg(args, int);  // get next argument (as int)
        total += val;
    }

    va_end(args);  // cleanup
    return total;
}

int main() {
    printf("%d\n", sum(3, 10, 20, 30));      // 60
    printf("%d\n", sum(5, 1, 2, 3, 4, 5));   // 15
    printf("%d\n", sum(0));                    // 0
    return 0;
}

// The 'count' parameter tells the function how many args follow.
// Without it, the function can't know when to stop.

カスタム printf の実装

vprintf/vfprintf/vsprintf は ... の代わりに va_list を取る可変長引数ヘルパーです — カスタム printf 風関数を構築できます。log_msg の例は printf をログレベルプレフィックスでラップします。print_values の例は混合型の処理方法を示します:各値の前に型タグを渡し、タグで switch して正しい型で va_arg を呼び出します。va_arg が正確な型を必要とするためこれが必要です — ランタイム型情報がありません。型タグパターンはポリモーフィック C API で使用されます(例:SQLite の bind 関数)。va_arg 型を正確に一致させてください — int vs long、float vs double(float は varargs で double に昇格)。

c
#include <stdio.h>
#include <stdarg.h>
#include <string.h>

// Custom logging function with format string
void log_msg(const char *level, const char *format, ...) {
    printf("[%s] ", level);

    va_list args;
    va_start(args, format);

    // vprintf: like printf but takes va_list instead of ...
    vprintf(format, args);

    va_end(args);
    printf("\n");
}

// Variadic function with mixed types
void print_values(int count, ...) {
    va_list args;
    va_start(args, count);

    for (int i = 0; i < count; i++) {
        int type = va_arg(args, int);  // type tag
        switch (type) {
            case 0:  // int
                printf("int: %d\n", va_arg(args, int));
                break;
            case 1:  // double
                printf("double: %f\n", va_arg(args, double));
                break;
            case 2:  // string
                printf("string: %s\n", va_arg(args, char *));
                break;
        }
    }
    va_end(args);
}

int main() {
    log_msg("INFO", "User %s logged in from %s", "Alice", "192.168.1.1");
    log_msg("ERROR", "Failed to open %s (code %d)", "config.txt", 13);

    print_values(2, 0, 42, 2, "hello", 1, 3.14);
    return 0;
}

センチネル終端の可変長引数関数

センチネル終端の可変長引数関数は、カウントの代わりに特別な値(通常 NULL)で引数の終わりをマークします。これは文字列中心の API によりクリーンです — 呼び出し側が引数をカウントする必要がありません。exec ファミリ(execl、execlp)はセンチネルとして NULL を使用します。欠点:呼び出し側が NULL を忘れた場合、関数はゴミメモリを読みます(未定義動作)。一部のコンパイラ(GCC)は __attribute__((sentinel)) でセンチネル欠落を警告します。NULL が必要なことを常に文書化してください。バッファサイズパラメータがバッファオーバーフローを防ぎます — 常に宛先サイズを渡し、strcat 前に境界をチェックしてください。

c
#include <stdio.h>
#include <stdarg.h>
#include <string.h>

// Sentinel-terminated: last argument is NULL
void concat_strings(char *dest, size_t size, ...) {
    va_list args;
    va_start(args, size);

    dest[0] = '\0';  // start with empty string
    size_t used = 0;

    while (1) {
        char *s = va_arg(args, char *);
        if (s == NULL) break;  // sentinel — stop reading

        size_t len = strlen(s);
        if (used + len < size) {
            strcat(dest, s);
            used += len;
        }
    }

    va_end(args);
}

int main() {
    char result[256];
    concat_strings(result, sizeof(result),
                   "Hello, ", "world", "! ", "How are you?", NULL);
    printf("%s\n", result);
    // Hello, world! How are you?

    // Common C APIs using sentinels:
    // execl("/bin/ls", "ls", "-l", NULL);  // exec family
    // sqlite3_exec(db, sql, callback, NULL, NULL);
    return 0;
}

可変長引数の転送

可変長引数の転送には va_copy が必要です(代入ではなく)— va_list は = でコピーできない不透明型の場合があります。va_copy で引数リストを複数回トラバースできます(例:最初に計測、次に印刷)。LOG マクロは __VA_ARGS__ を使用してすべての引数を fprintf に転送します。##__VA_ARGS__ GCC 拡張は可変長引数がない場合に先行カンマを削除します(LOG("msg") が末尾カンマなしで動作)。このパターンは C ロギングマクロで遍在します。C99 は ... の前に少なくとも1つの引数を要求;C11/C23 と GCC はゼロを許可します。C++ での型安全な代替には、可変長テンプレートまたは std::format を使用してください。

c
#include <stdio.h>
#include <stdarg.h>

// Wrapper that forwards varargs to another function
// Use va_copy for the copy (needed for multiple passes)
void custom_printf(const char *format, ...) {
    va_list args1, args2;
    va_start(args1, format);
    va_copy(args2, args1);  // copy for second use

    // First pass: count characters that would be printed
    int len = vsnprintf(NULL, 0, format, args1);
    printf("[len=%d] ", len);

    // Second pass: actually print
    vprintf(format, args2);

    va_end(args1);
    va_end(args2);
}

// Macro forwarding (common pattern for logging)
#define LOG(fmt, ...) \
    fprintf(stderr, "[LOG] %s:%d: " fmt "\n", \
            __FILE__, __LINE__, ##__VA_ARGS__)

// The ## operator removes the comma if __VA_ARGS__ is empty
// (GCC extension, widely supported)

int main() {
    custom_printf("Value: %d, Name: %s\n", 42, "test");
    LOG("Simple message");           // no extra args
    LOG("With value: %d", 100);      // with args
    return 0;
}

可変長マクロ(C99)

C99 可変長マクロは __VA_ARGS__ を使用してマクロ定義の ... にマッチするすべての引数をキャプチャします。##__VA_ARGS__(GCC 拡張、C20 で標準化)は可変長引数が渡されない場合にカンマを削除します。COUNT マクロは巧妙なトリックを使用します:N 個の引数を N、5、4、3、2、1 にマッピングし、N番目の位置がカウントを与えます。ASSERT の do { ... } while (0) イディオムはマクロを単一文のように動作させます(ブレースなしの if/else で安全)。# はマクロ引数を文字列化します。可変長マクロは C でのロギング、デバッグ、ジェネリックプログラミングに不可欠です。これらは多くのライブラリ API と Linux カーネルのロギングシステムの基盤です。

c
#include <stdio.h>

// C99 variadic macros: __VA_ARGS__ captures all extra args
#define DEBUG_PRINT(fmt, ...) \
    printf("DEBUG: " fmt "\n", ##__VA_ARGS__)

#define MAX(...) (max_of(__VA_ARGS__))

// Assert macro with message
#define ASSERT(cond, fmt, ...) \
    do { \
        if (!(cond)) { \
            fprintf(stderr, "Assertion failed: %s\n" fmt "\n", \
                    #cond, ##__VA_ARGS__); \
            exit(1); \
        } \
    } while (0)

// Count arguments (GCC __VA_OPT__ or recursive macros)
#define COUNT(...) COUNT_N(__VA_ARGS__, 5, 4, 3, 2, 1)
#define COUNT_N(_1, _2, _3, _4, _5, N, ...) N

// Stringification of all args
#define STR(...) #__VA_ARGS__

int main() {
    DEBUG_PRINT("x = %d", 42);
    DEBUG_PRINT("no args");  // ## removes comma

    ASSERT(x > 0, "x was %d", x);

    printf("Count: %d\n", COUNT(a, b, c));  // 3
    printf("Stringified: %s\n", STR(hello, world));  // "hello, world"

    return 0;
}
18

ビット操作のトリック

一般的なビットトリック

ビット操作はバイナリ表現に直接作用します。n & 1 は最下位ビットで奇偶をチェックします。左シフト(<<)は2で乗算;右シフト(>>)は除算。XOR スワップは一時変数を回避しますが可読性が低いです。n & (n-1) は最下位セットビットをクリアし、2のべき乗チェックと popcount に便利です。これらのトリックは高速ですが、アプリケーションコードでは可読性を優先してください。

c
// Check if odd
int is_odd(int n) { return n & 1; }

// Multiply/divide by powers of 2
int doubled = x << 1;    // x * 2
int halved = x >> 1;     // x / 2
int times8 = x << 3;     // x * 8

// Swap without temp variable
void swap(int *a, int *b) {
    *a ^= *b; *b ^= *a; *a ^= *b;
}

// Check if power of 2
int is_pow2(int n) { return n > 0 && (n & (n-1)) == 0; }

// Count set bits (population count)
int popcount(unsigned int n) {
    int count = 0;
    while (n) { n &= (n-1); count++; }
    return count;
}

ビットフラグとマスク

ビットフラグは複数の boolean オプションを1つの整数にパックし、メモリを節約します。各フラグは2のべき乗(1ビット)です。OR(|)でフラグを設定、AND(&)でチェック、XOR(^)でトグル、AND NOT(&= ~)でクリア。このパターンはシステムプログラミングで遍在します:ファイル権限(O_RDONLY、O_CREAT)、ソケットオプション、GPU 状態。可読性のために名前付き定数を使用してください。

c
#define FLAG_READ    (1 << 0)  // 0x01
#define FLAG_WRITE   (1 << 1)  // 0x02
#define FLAG_EXECUTE (1 << 2)  // 0x04

// Set flags
unsigned int perms = FLAG_READ | FLAG_WRITE;

// Check if flag is set
if (perms & FLAG_WRITE) { /* write allowed */ }

// Toggle a flag
perms ^= FLAG_EXECUTE;

// Clear a flag
perms &= ~FLAG_APPEND;

// Check if ALL flags in mask are set
int has_all = (perms & (FLAG_READ|FLAG_WRITE))
            == (FLAG_READ|FLAG_WRITE);

構造体内のビットフィールド

ビットフィールドは小さな値を構造体内の最小ビット数にパックします。コロン構文がビット幅を指定します。これは多くの小さなフィールド(日付、フラグ、ハードウェアレジスタ)を持つデータ構造のメモリを節約します。ただし、ビットフィールドレイアウトは実装依存です:バイト順、パディング、アライメントがコンパイラ間で異なります。ポータブルなデータフォーマットにはビットフィールドを避け、代わりに明示的なビットマスクを使用してください。

c
// Pack multiple small fields into one int
struct Date {
    unsigned int day   : 5;   // 0-31 (5 bits)
    unsigned int month : 4;   // 0-15 (4 bits)
    unsigned int year  : 23;  // 0-8M (23 bits)
};  // Total: 32 bits = 4 bytes

struct Date today = { 21, 6, 2025 };
printf("Size: %zu bytes\n", sizeof(today));  // 4

today.day = 15;
if (today.month == 12) {
    today.year++;
    today.month = 1;
}

エンディアン変換

エンディアンはバイト順を決定します:リトルエンディアン(x86、ARM デフォルト)は LSB を最初に格納;ビッグエンディアン(ネットワーク、一部 MIPS)は MSB を最初に格納。ネットワークプロトコルはビッグエンディアン(ネットワークバイト順)を使用します。ポータブルなネットワークコードには htonl/ntohl を使用します。シフトとマスクによる手動バイトスワップは任意のプラットフォームで動作します。最適化されたコードパスのために __BYTE_ORDER__ でコンパイル時にエンディアンを検出します。

c
#include <arpa/inet.h>  // htonl, ntohl

// Host to network byte order (big-endian) and back
uint32_t host_val = 0x12345678;
uint32_t net_val = htonl(host_val);
uint32_t back = ntohl(net_val);

// Manual byte swap (portable)
uint32_t swap32(uint32_t v) {
    return ((v & 0xFF000000) >> 24) |
           ((v & 0x00FF0000) >> 8)  |
           ((v & 0x0000FF00) << 8)  |
           ((v & 0x000000FF) << 24);
}

// Detect endianness at compile time
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
    printf("Little-endian system\n");
#endif

ビット操作ハック

ブランチレスビットハックはタイトループのパフォーマンスのために条件ジャンプを回避します。abs トリックは算術右シフトでマスクを作成します。next_pow2 は最高セットビットより下のすべてのビットを埋め、1を加算します。ビット反転は分割統治を使用:ニブル、次にペア、次に単一ビットをスワップ。これらは暗号、ハッシュ、DSP で便利です。モダン CPU にはより高速な組み込み命令(POPCNT、LZCNT)がよくあります。

c
// Absolute value without branching
int abs_val(int n) {
    int mask = n >> (sizeof(int)*8 - 1);
    return (n ^ mask) - mask;
}

// Round up to next power of 2
unsigned int next_pow2(unsigned int n) {
    n--;
    n |= n >> 1;  n |= n >> 2;
    n |= n >> 4;  n |= n >> 8;
    n |= n >> 16;
    return n + 1;
}

// Reverse bits in a byte
uint8_t reverse_byte(uint8_t b) {
    b = (b >> 4) | (b << 4);
    b = ((b & 0xCC) >> 2) | ((b & 0x33) << 2);
    b = ((b & 0xAA) >> 1) | ((b & 0x55) << 1);
    return b;
}
19

シグナル処理の高度なトピック

シグナルセットとブロック

sigprocmask はシグナルをブロックし、キューイング(紛失ではなく)され後で配信されるようにします。これがクリティカルセクションを中断から保護します。SIG_BLOCK はマスクに追加、SIG_UNBLOCK は削除、SIG_SETMASK は置換。キューイングされたシグナルをチェックするには sigpending を使用します。シグナルは短時間のみブロック;長いブロックは重要なイベントを見逃す可能性があります。シグナルマスクはプロセス単位で fork を通じて継承されます。

c
#include <signal.h>

sigset_t mask, oldmask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);

// Block signals (they will be queued)
sigprocmask(SIG_BLOCK, &mask, &oldmask);

// Critical section - signals are deferred
do_critical_work();

// Unblock - pending signals are now delivered
sigprocmask(SIG_SETMASK, &oldmask, NULL);

// Check for pending signals
sigpending(&mask);
if (sigismember(&mask, SIGINT)) {
    printf("SIGINT is pending\n");
}

安全なシグナルハンドラ

シグナルハンドラは async-signal-safe でなければなりません:再入可能な関数(write、_exit、signal)のみ使用。printf、malloc、ほとんどのライブラリ関数は避けてください — 操作中に中断され状態を破壊する可能性があります。ハンドラ設定フラグには volatile sig_atomic_t を使用します。SA_RESTART が中断されたシステムコールを自動再開します。ポータブルで明確に定義された動作のために signal より sigaction を優先します。

c
#include <signal.h>
#include <unistd.h>

// Only safe type in handlers
volatile sig_atomic_t got_signal = 0;

void handler(int sig) {
    // ONLY use async-signal-safe functions!
    // write() is safe; printf() is NOT
    const char msg[] = "Signal received\n";
    write(STDERR_FILENO, msg, sizeof(msg)-1);
    got_signal = 1;
}

int main() {
    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;  // Restart interrupted syscalls
    sigaction(SIGINT, &sa, NULL);

    while (!got_signal) pause();
    return 0;
}

セルフパイプと signalfd

signalfd(Linux)はシグナルをファイル記述子に変換し、イベントループ(epoll、select)に統合します。最初にシグナルをブロックし、次に signalfd を作成します。セルフパイプトリックはポータブルです:ハンドラがパイプにバイトを書き込み、メインループがそれを読み取ります。両アプローチともシグナル処理を制限されたハンドラコンテキストから通常のコードに移し、そこで任意の関数を安全に呼び出せます。

c
#include <sys/signalfd.h>

// signalfd: handle signals as file descriptors (Linux)
int setup_signalfd() {
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGTERM);
    sigprocmask(SIG_BLOCK, &mask, NULL);  // Must block first

    int fd = signalfd(-1, &mask, SFD_CLOEXEC);
    return fd;
}

// In event loop:
struct signalfd_siginfo si;
read(signalfd_fd, &si, sizeof(si));
printf("Got signal %d\n", si.ssi_signo);

// Self-pipe trick (portable):
// Write to pipe in handler, read in main loop
int pipefd[2];
void pipe_handler(int sig) {
    write(pipefd[1], &sig, sizeof(sig));
}

タイマーと SIGALRM

setitimer は間隔で SIGALRM を配信します。ITIMER_REAL は実時間を使用;ITIMER_VIRTUAL は CPU 時間を使用;ITIMER_PROF は CPU + システム時間を使用します。タイマーはキャンセルされるまで繰り返します。モダンコードでは、スレッドごとのタイマーのために SIGEV_THREAD 付きの timer_create、またはイベントループとの統合のために専用タイマー fd(Linux の timerfd_create)を優先します。デフォルト終了を避けるため常にシグナルを処理してください。

c
#include <sys/time.h>

volatile sig_atomic_t timer_fired = 0;

void timer_handler(int sig) { timer_fired = 1; }

int main() {
    struct sigaction sa = {0};
    sa.sa_handler = timer_handler;
    sigaction(SIGALRM, &sa, NULL);

    // Set interval timer: 2 sec initial, 1 sec repeat
    struct itimerval timer;
    timer.it_value.tv_sec = 2;
    timer.it_interval.tv_sec = 1;
    setitimer(ITIMER_REAL, &timer, NULL);

    int count = 0;
    while (count < 5) {
        pause();
        if (timer_fired) {
            printf("Timer %d\n", ++count);
            timer_fired = 0;
        }
    }
    return 0;
}

シグナルの送信

kill は PID でプロセスにシグナルを送信します。kill(0, sig) はプロセスグループ全体に送信します。raise は呼び出しプロセスにシグナルを送信します。sigqueue は添付データ付きでシグナルを送信します(siginfo)。子プロセスを回収し終了ステータスをチェックするには常に waitpid を使用します。WIFSIGNALED はシグナル死亡と正常終了を区別します。SIGKILL ではなく SIGTERM を送信するとグレースフルシャットダウンを可能にします。

c
#include <signal.h>
#include <sys/wait.h>

pid_t child = fork();
if (child == 0) {
    while (1) { sleep(1); }
} else {
    sleep(3);
    kill(child, SIGTERM);  // Send SIGTERM to child

    int status;
    waitpid(child, &status, 0);
    if (WIFSIGNALED(status))
        printf("Killed by signal %d\n", WTERMSIG(status));
}

// Send signal to self
raise(SIGSTOP);  // Stop (resume with SIGCONT)

// Send to process group
kill(0, SIGUSR1);  // 0 = own process group

// Send with data (sigqueue)
union sigval value = { .sival_int = 42 };
sigqueue(child, SIGUSR1, value);
20

プロセス管理の高度なトピック

fork と exec のパターン

fork-exec パターンは子プロセスを作成し(fork)、そのイメージを新しいプログラムで置換します(exec)。fork がプロセスを複製;exec が新しいプログラムを読み込み。子は親バッファのフラッシュを避けるため exec 失敗時に exit ではなく _exit を呼び出さなければなりません。waitpid は子が終了するまでブロックします。WEXITSTATUS が終了コードを抽出します。これがシェルがコマンドを実行する方法です。

c
#include <unistd.h>
#include <sys/wait.h>

int run_program(const char *path, char *const args[]) {
    pid_t pid = fork();
    if (pid < 0) { perror("fork"); return -1; }

    if (pid == 0) {
        // Child: exec replaces process image
        execvp(path, args);
        perror("execvp");  // Only on failure
        _exit(127);
    }

    // Parent: wait for child
    int status;
    waitpid(pid, &status, 0);
    return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}

// Usage
char *args[] = {"ls", "-la", "/tmp", NULL};
int result = run_program("ls", args);

デーモン化

setsid は新しいセッションとプロセスグループを作成し、制御端末から切り離します。ダブルフォークパターンはデーモンが端末を再取得するのを防ぎます。デーモン化後、標準ファイル記述子をクローズし /dev/null またはログファイルにリダイレクトします。chdir to / はアンマウントのブロックを防ぎます。umask(0) は予測可能なファイル権限を保証します。これが標準的なデーモンパターンです。

c
#include <unistd.h>

int main() {
    pid_t child = fork();
    if (child > 0) _exit(0);  // Parent exits
    if (child < 0) return 1;

    // First child: create new session
    setsid();

    // Second fork (prevent reacquiring terminal)
    pid_t grandchild = fork();
    if (grandchild > 0) _exit(0);

    // Daemon process
    chdir("/");
    umask(0);
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    while (1) {
        sleep(60);
        // Do daemon work
    }
    return 0;
}

posix_spawn の代替

posix_spawn は MMU なしのシステム(組み込み)や大きなメモリフットプリント(fork はページテーブルをコピー)のシステムで fork+exec より効率的な代替です。プロセス作成と exec を1呼び出しで組み合わせ、ファイルアクション(リダイレクト、クローズ)を原子的に適用します。fork と exec の間で子状態を変更する必要がない場合に posix_spawn を使用します。POSIX 標準で Linux、macOS、ほとんどの Unix システムで利用可能です。

c
#include <spawn.h>
#include <sys/wait.h>

extern char **environ;

int spawn_child(const char *cmd, char *const argv[]) {
    pid_t pid;
    posix_spawn_file_actions_t actions;
    posix_spawn_file_actions_init(&actions);
    posix_spawn_file_actions_addclose(&actions, STDIN_FILENO);

    posix_spawnattr_t attr;
    posix_spawnattr_init(&attr);

    int ret = posix_spawnp(&pid, cmd, &actions, &attr, argv, environ);

    posix_spawn_file_actions_destroy(&actions);
    posix_spawnattr_destroy(&attr);

    if (ret != 0) return -1;

    int status;
    waitpid(pid, &status, 0);
    return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}

ゾンビの防止

ゾンビプロセスは親が wait を呼ぶ前に子が終了した時に発生します。以下で防止:(1) ループで waitpid で SIGCHLD を処理(WNOHANG でブロック回避)、(2) SIGCHLD を SIG_IGN に設定(カーネルが自動回収)、または (3) ダブルフォークして孫を孤児にし init(PID 1)に引き取らせ、自動回収させます。シグナルハンドラでは常に errno を保存して復元してください。

c
#include <sys/wait.h>
#include <signal.h>

// Reap zombies with SIGCHLD handler
void sigchld_handler(int sig) {
    int saved_errno = errno;
    while (waitpid(-1, NULL, WNOHANG) > 0) {
        // Reap all available zombies
    }
    errno = saved_errno;
}

// Setup: handle SIGCHLD
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);

// Alternative: explicitly ignore (auto-reap)
// signal(SIGCHLD, SIG_IGN);

// Double-fork to prevent zombies
pid_t inter = fork();
if (inter == 0) {
    if (fork() == 0) {
        // Grandchild does the work
        execlp("sleep", "sleep", "10", NULL);
        _exit(1);
    }
    _exit(0);  // First child exits immediately
}
waitpid(inter, NULL, 0);

リソース制限

setrlimit はプロセスにリソース制限を課します:CPU 時間(RLIMIT_CPU)、仮想メモリ(RLIMIT_AS)、ファイルサイズ(RLIMIT_FSIZE)、オープンファイル数(RLIMIT_NOFILE)、スタックサイズ、コアダンプサイズ。ソフト制限が強制;ハード制限が上限。CPU 時間超過は SIGXCPU を送信;メモリ超過は malloc を失敗させます。バグや攻撃からのリソース枯渇を防ぐため子プロセスで制限を使用します。

c
#include <sys/resource.h>

void set_limits() {
    struct rlimit lim;

    // Limit CPU time to 10 seconds
    lim.rlim_cur = 10; lim.rlim_max = 10;
    setrlimit(RLIMIT_CPU, &lim);

    // Limit memory to 256 MB
    lim.rlim_cur = 256*1024*1024;
    lim.rlim_max = 256*1024*1024;
    setrlimit(RLIMIT_AS, &lim);

    // Limit open files
    lim.rlim_cur = 32; lim.rlim_max = 64;
    setrlimit(RLIMIT_NOFILE, &lim);
}

void show_limits() {
    struct rlimit lim;
    getrlimit(RLIMIT_NOFILE, &lim);
    printf("Max files: soft=%lu, hard=%lu\n",
           lim.rlim_cur, lim.rlim_max);
}
21

パイプと IPC の高度なトピック

匿名パイプ

匿名パイプは親と子プロセス間の単方向通信を提供します。未使用端を常にクローズ:書き込み側は読み取り端を、その逆も同様にクローズしなければなりません。書き込み端のクローズは読み取り側に EOF をシグナルします(read が 0 を返す)。パイプは固定バッファ(通常 64KB)を持ちます;満杯の場合書き込みはブロックします。パイプは関連プロセス(親子)のみです。無関係なプロセスには、名前付きパイプ(FIFO)またはソケットを使用します。

c
#include <unistd.h>

int main() {
    int pipefd[2];
    pipe(pipefd);

    pid_t pid = fork();
    if (pid == 0) {
        // Child: read from pipe
        close(pipefd[1]);  // Close unused write end
        char buf[256];
        ssize_t n = read(pipefd[0], buf, sizeof(buf)-1);
        buf[n] = '\0';
        printf("Child received: %s", buf);
        close(pipefd[0]);
    } else {
        // Parent: write to pipe
        close(pipefd[0]);  // Close unused read end
        const char *msg = "Hello from parent!\n";
        write(pipefd[1], msg, strlen(msg));
        close(pipefd[1]);  // EOF signal to reader
        wait(NULL);
    }
    return 0;
}

名前付きパイプ(FIFO)

名前付きパイプ(FIFO)は無関係なプロセス間でパイプとして動作する特別なファイルです。mkfifo がファイルを作成;open は読み取り側と書き込み側の両方が存在するまでブロックします。非ブロッキングオープンには O_NONBLOCK を使用します。FIFO は unlink されるまでファイルシステムに永続します。独立したプログラム間のシンプルな IPC に便利ですが、複雑な通信には Unix ドメインソケットまたはメッセージキューを検討してください。

c
#include <sys/stat.h>
#include <fcntl.h>

// Create a named pipe (persists in filesystem)
mkfifo("/tmp/myfifo", 0666);

// Writer process
int fd = open("/tmp/myfifo", O_WRONLY);
write(fd, "Hello FIFO", 10);
close(fd);

// Reader process (can be unrelated to writer)
int fd = open("/tmp/myfifo", O_RDONLY);
char buf[256];
ssize_t n = read(fd, buf, sizeof(buf));
close(fd);

// Non-blocking open
int fd = open("/tmp/myfifo", O_RDONLY | O_NONBLOCK);

// Clean up
unlink("/tmp/myfifo");

共有メモリ

共有メモリは最速の IPC です:プロセスが同じ物理メモリをアドレス空間にマップします。shm_open が POSIX 共有メモリオブジェクトを作成;mmap がマップします。変更はすべてのマッパーに即座に可視です。同期にはセマフォまたはミューテックス(PTHREAD_PROCESS_SHARED 付き)を使用します。リークを避けるため常にアンマップして unlink してください。共有メモリは大きなデータに最適;オーバーヘッドは初期マッピングのみです。

c
#include <sys/mman.h>
#include <fcntl.h>

// Create shared memory object
int fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 4096);  // Set size

// Map into process address space
char *shared = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                    MAP_SHARED, fd, 0);
close(fd);  // Can close after mmap

// Write data (visible to other processes)
sprintf(shared, "Shared data at %p", (void*)shared);

// Synchronize (flush to backing store)
msync(shared, 4096, MS_SYNC);

// Unmap and clean up
munmap(shared, 4096);
shm_unlink("/my_shm");

Unix ドメインソケット

Unix ドメインソケットは同じマシン上で双方向のストリーム指向 IPC を提供します。TCP より高速(ネットワークオーバーヘッドなし)で、SCM_RIGHTS 経由でプロセス間でファイル記述子を渡すことができます。信頼性のあるストリームには SOCK_STREAM、データグラムには SOCK_DGRAM を使用します。ソケットパスはファイルシステムエントリです;address in use エラーを避けるため bind 前に unlink してください。Unix ソケットは Docker、X11、systemd 通信の基盤です。

c
#include <sys/socket.h>
#include <sys/un.h>

// Server
int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/mysocket");
unlink("/tmp/mysocket");
bind(sfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sfd, 5);

int cfd = accept(sfd, NULL, NULL);
char buf[256];
read(cfd, buf, sizeof(buf));
write(cfd, "Response", 8);
close(cfd);

// Client
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/mysocket");
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
write(sock, "Request", 7);
close(sock);

unlink("/tmp/mysocket");

メッセージキュー

POSIX メッセージキューは優先度順のメッセージベース IPC を提供します。各メッセージには優先度があり;より高い優先度のメッセージが先に受信されます。mq_send と mq_receive は単一メッセージで原子的です。非ブロッキング操作には O_NONBLOCK を、タイムアウトには mq_timedreceive を使用します。メッセージキューはプロセスと共に消えるパイプとは異なり、unlink されるまで永続します。タスクディスパッチとイベント通知に最適です。

c
#include <mqueue.h>

// Create/open a message queue
struct mq_attr attr = {
    .mq_maxmsg = 10,
    .mq_msgsize = 256
};
mqd_t mq = mq_open("/my_queue", O_CREAT | O_RDWR, 0666, &attr);

// Send a message (priority-based)
mq_send(mq, "Hello MQ", 8, 1);  // priority 1

// Receive a message (highest priority first)
char buf[256];
unsigned int prio;
ssize_t n = mq_receive(mq, buf, sizeof(buf), &prio);
printf("Received (priority %u): %.*s\n", prio, (int)n, buf);

// Non-blocking receive
mqd_t mq_nb = mq_open("/my_queue", O_RDONLY | O_NONBLOCK);

// Timed receive
struct timespec ts = { .tv_sec = time(NULL) + 5 };
mq_timedreceive(mq, buf, sizeof(buf), &prio, &ts);

mq_close(mq);
mq_unlink("/my_queue");
22

Makefile とビルドの高度なトピック

自動変数とパターン

自動変数により Makefile が簡潔で保守しやすくなります。パターンルール(%.o: %.c)がパターンにマッチする任意のファイルのビルド方法を定義します。-MM がヘッダ依存関係を追跡する依存関係ファイル(.d)を生成し、ヘッダ編集が依存 .c ファイルの再コンパイルをトリガーします。-include ディレクティブが存在する場合依存関係ファイルをサイレントにインクルードします。これが堅牢な C/C++ ビルドシステムの基盤です。

c
# Automatic variables:
# $@ = target name
# $< = first prerequisite
# $^ = all prerequisites
# $? = prerequisites newer than target

# Pattern rule: compiles any .c to .o
%.o: %.c %.h
	$(CC) $(CFLAGS) -c $< -o $@

OBJS = main.o utils.o parser.o

program: $(OBJS)
	$(CC) $(LDFLAGS) $^ -o $@ $(LDLIBS)

# Automatic dependency generation
%.d: %.c
	@$(CC) -MM $< > $@

-include $(OBJS:.o=.d)

clean:
	rm -f $(OBJS) program

変数と条件分岐

即時評価には := を(より高速、予測可能)、遅延評価には = を(前方参照を許可)使用します。?= は未設定の場合のみ変数を設定し、コマンドラインからのユーザーオーバーライドを許可します。条件分岐(ifeq、ifdef)がデバッグ/リリースビルドを可能にします。Q トリックは VERBOSE が設定されていない限りコマンドエコーを抑制します。MAKECMDGOALS はコマンドラインからのターゲットを含みます。

c
# = recursive (evaluated when used)
# := simple (evaluated when defined)
# ?= conditional (only if not already set)

CC := gcc
CFLAGS ?= -Wall -Wextra -O2
DEBUG := $(filter debug,$(MAKECMDGOALS))

ifeq ($(DEBUG),debug)
    CFLAGS += -g -DDEBUG -O0
else
    CFLAGS += -DNDEBUG
endif

ifdef VERBOSE
    Q =
else
    Q = @
endif

build:
	$(Q)$(CC) $(CFLAGS) -c main.c

関数とテキスト処理

Make 関数がテキスト変換を可能にします:パターン置換の patsubst、ファイル選択の filter、glob の wildcard、反復の foreach。shell 関数はパース時にコマンドを実行 — バージョン情報の埋め込みに便利。置換参照($(VAR:.c=.o))はシンプルなサフィックス変更の patsubst の簡潔な代替です。

c
SRCS = main.c utils.c parser.c

# Substitution
OBJS = $(patsubst %.c,%.o,$(SRCS))  # main.o utils.o parser.o
# Or shorthand:
OBJS = $(SRCS:.c=.o)

# Filter files by extension
C_SRCS = $(filter %.c,$(wildcard *.c))

# Add prefix
FLAGS = $(addprefix -I,$(INCLUDE_DIRS))

# Foreach
DIRS = src lib include
CREATE = $(foreach dir,$(DIRS),mkdir -p $(dir);)

# Shell function
GIT_HASH = $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
CFLAGS += -DGIT_HASH=\"$(GIT_HASH)\"

サブディレクトリと再帰的 Make

再帰的 make(サブディレクトリ Makefile)は伝統的ですが、並列ビルドで遅くエラーが起きやすい場合があります。非再帰的アプローチ(vpath 付き単一 Makefile)が正確性と速度のために推奨されます。再帰的 make を使用する場合、変数を明示的に渡し MAKECMDGOALS でターゲットを伝播します。大きなプロジェクトには、より良い依存関係追跡と IDE サポートのために生の Make の代わりに CMake または Meson を検討してください。

c
SUBDIRS = lib src tests

.PHONY: all $(SUBDIRS) clean test

all: $(SUBDIRS)

# Pass variables to sub-makes
$(SUBDIRS):
	$(MAKE) -C $@ $(MAKECMDGOALS)

# Parallel build: make -j4

clean: $(SUBDIRS)
	rm -f *.o program

# Non-recursive alternative (single Makefile)
vpath %.c src:lib
vpath %.h include

CFLAGS += -Iinclude -Ilib

program: main.o lib/utils.o
	$(CC) $^ -o $@

CMake 統合

CMake は宣言的 CMakeLists.txt から Makefile(または Ninja、VS、Xcode プロジェクト)を生成します。モダン CMake はグローバル変数の代わりにターゲットベースのコマンド(target_include_directories、target_link_libraries)を使用します。ジェネレータ式($<$<CONFIG:Debug>:...)が設定ごとのフラグを可能にします。CMake は C/C++ プロジェクトの事実上の標準で、生の Makefile より良い IDE 統合とクロスプラットフォームサポートを提供します。

c
# CMakeLists.txt (modern CMake 3.15+)
cmake_minimum_required(VERSION 3.15)
project(MyApp C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

add_executable(myapp src/main.c src/utils.c)

target_include_directories(myapp PRIVATE include)
target_compile_options(myapp PRIVATE -Wall -Wextra)

# Per-configuration flags
target_compile_options(myapp PRIVATE
    $<$<CONFIG:Debug>:-g -O0 -DDEBUG>
    $<$<CONFIG:Release>:-O2 -DNDEBUG>
)

# Find and link library
find_package(MATH REQUIRED)
target_link_libraries(myapp PRIVATE m)

install(TARGETS myapp DESTINATION bin)
23

GDB によるデバッグ

開始とブレークポイント

-g でコンパイルしてデバッグシンボルを埋め込み、-O0 で最適化を無効にします(そうしないと変数が最適化で消える可能性があります)。break は関数、行、条件にブレークポイントを設定します。watch(データブレークポイント)は変数が変更された時にトリガー — メモリ破壊の発見に強力。条件付きブレークポイント(break func if cond)は条件が true の時のみ発火し、ループに便利です。tbreak はワンショットブレークポイントです。

c
# Compile with debug symbols
gcc -g -O0 -o program program.c

# Start GDB
gdb ./program
gdb --args ./program arg1 arg2

# Breakpoints
(gdb) break main           # Function
(gdb) break 42             # Line 42
(gdb) break file.c:50      # Specific file
(gdb) break func if x > 10 # Conditional
(gdb) tbreak main          # Temporary (removed after hit)
(gdb) watch x              # Break when x changes

# Manage breakpoints
(gdb) info breakpoints
(gdb) delete 2             # Delete breakpoint #2
(gdb) disable 1            # Temporarily disable
(gdb) enable 1

ステップ実行と検査

next は関数呼び出しをステップオーバー;step はステップインします。finish は現在の関数の終わりまで実行します。print フォーマット:/x(hex)、/c(char)、/s(文字列)、/t(バイナリ)。@ 演算子は配列スライスを印刷:arr@5 は5要素を表示。display は各停止で変数を自動印刷。backtrace はコールスタックを表示;frame N はそのフレームの検査にコンテキストを切り替え。

c
(gdb) run                  # Start program
(gdb) continue             # Continue to next breakpoint
(gdb) next                 # Step over (no function entry)
(gdb) step                 # Step into (enters functions)
(gdb) finish               # Run until function returns
(gdb) until 50             # Run until line 50

# Inspect variables
(gdb) print x              # Print variable
(gdb) print *ptr           # Dereference pointer
(gdb) print arr[0]@5       # First 5 elements
(gdb) print/x x            # Print in hex
(gdb) display x            # Auto-print at each stop
(gdb) info locals          # All local variables

# Backtrace
(gdb) backtrace            # Call stack
(gdb) frame 2              # Switch to frame #2
(gdb) up / down            # Navigate stack

メモリの検査

examine(x)コマンドは生メモリを検査します。フォーマットはカウント、表示フォーマット、ユニットサイズを指定します。x/10i $pc はプログラムカウンタから10命令を逆アセンブル。x/s はメモリをヌル終端文字列として扱います。info proc mappings は仮想メモリレイアウト(text、data、heap、stack、共有ライブラリ)を表示。これはバッファオーバーフローとメモリ破壊のデバッグに不可欠です。

c
# x/[count][format][size] address
(gdb) x/10x &arr           # 10 words in hex
(gdb) x/20cb &str          # 20 bytes as chars
(gdb) x/4xw ptr            # 4 words in hex
(gdb) x/s str              # String

# Format: x(hex) d(decimal) u(unsigned) o(octal)
#         t(binary) f(float) a(addr) i(instr) c(char) s(string)
# Size: b(byte) h(half/2) w(word/4) g(giant/8)

# Disassemble
(gdb) disassemble main
(gdb) disassemble /r main  # With raw bytes

# Memory map
(gdb) info proc mappings   # Process memory layout

コアダンプ

コアダンプは事後解析のためにクラッシュ時のプロセス状態をキャプチャします。ulimit -c unlimited で有効にします。gdb program core でコアファイルを読み込みます。backtrace はクラッシュ発生場所を表示;info locals は変数値を表示。マルチスレッドプログラムでは、thread apply all bt がすべてのスレッド状態を表示 — デッドロック分析に不可欠。

c
# Enable core dumps
ulimit -c unlimited
echo "core.%p" > /proc/sys/kernel/core_pattern

# Run program until it crashes
./program                  # Produces core.12345

# Analyze core dump
gdb ./program core.12345

(gdb) bt                   # Backtrace at crash
(gdb) bt full              # With local variables
(gdb) frame 0              # Top frame (crash location)
(gdb) info locals          # Variables at crash
(gdb) print *ptr           # What was the pointer?

# Multithreaded
(gdb) info threads         # List all threads
(gdb) thread 3             # Switch to thread 3
(gdb) thread apply all bt  # Backtraces for ALL threads

GDB スクリプトと自動化

.gdbinit は起動時の共通設定を自動化します。define は反復タスクのカスタムコマンドを作成します。commands はブレークポイントにアクションを付加します(例:変数をログして続行)。GDB は複雑な分析のために Python スクリプトをサポート:テスト実行の自動化、データ構造の可視化、統計の抽出。Python スクリプトは gdb モジュール経由で GDB 内部にアクセスできます。スクリプトを使用してチーム間でデバッグワークフローを標準化します。

c
# .gdbinit file (loaded on startup)
set pagination off
set print pretty on
set print element 0        # No limit on string display

# Define custom commands
define print_array
    set $i = 0
    while $i < $arg0
        printf "[%d] = %d\n", $i, $arg1[$i]
        set $i = $i + 1
    end
end
# Usage: print_array 10 my_array

# Commands attached to breakpoints
break main
commands 1
  silent
  printf "x = %d\n", x
  continue
end

# Python scripting (GDB 7+)
python
import gdb
gdb.execute("break main")
gdb.execute("run")
val = gdb.parse_and_eval("x")
print(f"x = {int(val)}")
end
24

メモリアライメントとビットフィールド

構造体のアライメントとパディング

コンパイラは各メンバが自然にアライメントされるようパディングを挿入します(通常はサイズへ:char=1、short=2、int=4、double=8)。メンバを最大から最小に並べ替えるとパディングを最小化します。offsetof でレイアウトを検査します。64ビットシステムではポインタは8バイトアライメントが必要です。過剰なパディングはメモリを浪費しキャッシュパフォーマンスを低下させます。構造体メンバは常にサイズ降順に並べてください。

c
#include <stddef.h>

// Without alignment consideration (padded)
struct Bad {
    char a;     // 1 byte + 7 padding
    double b;   // 8 bytes
    char c;     // 1 byte + 7 padding
};  // sizeof = 24

// Reordered for efficiency
struct Good {
    double b;   // 8 bytes
    char a;     // 1 byte
    char c;     // 1 byte + 6 padding
};  // sizeof = 16

printf("Bad: a=%zu b=%zu c=%zu total=%zu\n",
    offsetof(struct Bad, a),
    offsetof(struct Bad, b),
    offsetof(struct Bad, c),
    sizeof(struct Bad));

アライメントの制御

C11 alignas は型や変数の最小アライメントを指定します — SIMD(16/32バイトアライメント)と DMA に便利。パック構造体(__attribute__((packed)) または #pragma pack)はすべてのパディングを削除し、スペースを節約しますがアクセスが遅くなる可能性があります(一部のアーキテクチャではアライメントされていないメモリアクセスがフォールトする可能性)。正確なレイアウトが重要なネットワークプロトコルとファイルフォーマットに packed を使用します。高速アクセスが必要な構造体は絶対にパックしないでください。

c
#include <stdalign.h>

// C11 explicit alignment
struct alignas(16) Aligned16 {
    int data[4];  // 16 bytes, 16-byte aligned
};

// Check alignment
printf("Alignment of int: %zu\n", alignof(int));     // 4
printf("Alignment of struct: %zu\n",
       alignof(struct Aligned16));  // 16

// Packed struct (no padding) - GCC/Clang
struct __attribute__((packed)) Packed {
    char a;
    int b;    // No padding after a
    char c;
};  // sizeof = 6

// MSVC packed
#pragma pack(push, 1)
struct PackedMSVC { char a; int b; char c; };
#pragma pack(pop)

フレキシブル配列メンバ

フレキシブル配列メンバ(C99)は構造体が最後のメンバとして可変長配列を持てるようにします。malloc(sizeof(struct) + desired_length) で割り当てます。配列は単一割り当てを共有するため、1回の free ですべて解放します。これは別々のポインタ + malloc より効率的でクリーンです。動的配列、文字列、ネットワークパケットヘッダで一般的。sizeof(struct) はフレキシブル配列を除外します。

c
#include <stdlib.h>

// C99 flexible array member (last member with no size)
struct Buffer {
    size_t size;
    char data[];  // Flexible array
};

// Allocate with space for data
size_t data_len = 1024;
struct Buffer *buf = malloc(sizeof(struct Buffer) + data_len);
buf->size = data_len;
memset(buf->data, 0, data_len);

// Use data
strcpy(buf->data, "Hello");

// Single free (no separate allocation for data)
free(buf);

ユニオンのタイプパニング

ユニオンはメンバを同じメモリに重ね合わせ、タイプパニング(ビットを別の型として再解釈)を可能にします。最後に書き込まれたもの以外のユニオンメンバを読み取ることは C で許可されます(実装定義)。ポインタキャストとは異なり、ユニオンタイプパニングは厳密なエイリアシングの下で合法です。匿名ユニオン(C11)はメンバ名なしでメンバを直接公開します。タグ付きバリアントと IEEE 754 浮動小数点内部へのアクセスにユニオンを使用します。

c
#include <stdio.h>

// Union: members share the same memory
union Data {
    int i;
    float f;
    char bytes[4];
};

int main() {
    union Data d;
    d.i = 0x41424344;

    printf("As int: %d\n", d.i);
    printf("As float: %f\n", d.f);
    printf("As bytes: %02x %02x %02x %02x\n",
           d.bytes[0], d.bytes[1], d.bytes[2], d.bytes[3]);

    // Safe type punning (legal in C)
    union { float f; int i; } u;
    u.f = 3.14f;
    printf("Float bits: 0x%08x\n", u.i);

    // Anonymous unions (C11)
    struct Variant {
        int type;
        union { int i; float f; char *s; };
    };
}

メモリレイアウトとエンディアン

エンディアンはメモリ内のバイト順を決定します:リトルエンディアン(x86、ARM デフォルト)は LSB を最初に格納;ビッグエンディアンは MSB を最初に格納。ポータブルなバイナリフォーマットを書く時、memcpy の代わりに明示的なシフトでシリアライズします。デバッグ中に dump_hex で生メモリを検査します。ネットワークプロトコルはビッグエンディアン(ネットワークバイト順)を使用します;ポータブルコードには htonl/ntohl を使用します。ポータブルなバイナリ I/O を書く時は両エンディアンでテストしてください。

c
#include <stdint.h>

void dump_hex(void *ptr, size_t len) {
    unsigned char *bytes = ptr;
    for (size_t i = 0; i < len; i++) {
        printf("%02x ", bytes[i]);
        if ((i + 1) % 16 == 0) printf("\n");
    }
}

int main() {
    uint32_t val = 0x12345678;
    printf("Value: 0x%08x\n", val);
    printf("Bytes: ");
    dump_hex(&val, sizeof(val));

    // Little-endian: 78 56 34 12 (LSB first)
    // Big-endian:    12 34 56 78 (MSB first)

    // Serialize to big-endian (network order)
    unsigned char be[4];
    be[0] = (val >> 24) & 0xFF;
    be[1] = (val >> 16) & 0xFF;
    be[2] = (val >> 8)  & 0xFF;
    be[3] = val & 0xFF;

    // Deserialize from big-endian
    uint32_t recovered = (be[0]<<24)|(be[1]<<16)|(be[2]<<8)|be[3];
    printf("Recovered: 0x%08x\n", recovered);
    return 0;
}
25

可変長引数関数の高度なトピック

va_list の基礎

可変長引数関数は va_list を使用して可変引数にアクセスします。va_start が最後の名前付きパラメータでリストを初期化します。va_arg が指定型で次の引数を取得します。va_end がクリーンアップします。呼び出し側がカウントと型を伝えなければなりません(例:printf はフォーマット指定子を使用)。可変長引数関数は型安全性がありません — 型の不一致は未定義動作を引き起こします。

c
#include <stdarg.h>

// Variadic function: variable number of arguments
int sum(int count, ...) {
    va_list args;
    va_start(args, count);  // Initialize with last named param

    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int);  // Get next argument
    }

    va_end(args);  // Cleanup
    return total;
}

// Usage
int result = sum(3, 10, 20, 30);  // 60
int result2 = sum(5, 1, 2, 3, 4, 5);  // 15

センチネル終端の可変長引数

センチネル値(多くの場合 NULL)が引数リストの終わりをマークし、カウントパラメータの必要性を排除します。これは execl のような C API で一般的です。GCC __attribute__((sentinel)) は最後の引数が NULL でない場合警告します。期待されるセンチネルを常に文書化してください。欠点はセンチネルが有効なデータ値として出現できないことです。

c
#include <stdarg.h>

// Sentinel value marks the end
void log_messages(const char *first, ...) {
    va_list args;
    va_start(args, first);

    const char *msg = first;
    while (msg != NULL) {  // NULL is the sentinel
        printf("%s\n", msg);
        msg = va_arg(args, const char *);
    }

    va_end(args);
}

// Usage: NULL terminates the list
log_messages("Error 1", "Error 2", "Error 3", NULL);

// GCC attribute to enforce sentinel
void log_messages(const char *first, ...)
    __attribute__((sentinel));

vfprintf とフォーマット文字列

vprintf/vfprintf/vsnprintf は ... の代わりに va_list を受け取り、カスタム printf 風関数を可能にします。バッファオーバーフローを防ぐため vsprintf の代わりに常に vsnprintf(境界付き)を使用してください。va_list を直接転送します。このパターンはロギングライブラリ、エラー報告、カスタムフォーマッタで使用されます。フォーマット文字列の脆弱性(ユーザー制御のフォーマット)はセキュリティリスクです — ユーザー入力をフォーマットとして絶対に渡さないでください。

c
#include <stdarg.h>
#include <stdio.h>

// Custom printf-like function
void logf(const char *format, ...) {
    va_list args;
    va_start(args, format);

    // Add timestamp prefix
    printf("[LOG] ");
    vprintf(format, args);  // Pass va_list to vprintf
    printf("\n");

    va_end(args);
}

// Write to string with vsnprintf
void format_msg(char *buf, size_t size, const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    vsnprintf(buf, size, fmt, args);  // Safe: bounded
    va_end(args);
}

// Usage
logf("User %s logged in (id=%d)", username, id);
char msg[256];
format_msg(msg, sizeof(msg), "Error %d: %s", code, text);

関数ポインタとコールバック

関数ポインタは C でコールバックとポリモーフィズムを可能にします。構文 return_type (*name)(params) が関数へのポインタを宣言します。qsort はジェネリックソートのために比較コールバックを使用します。関数ポインタの配列はディスパッチテーブルを実装します(switch の代替)。コールバックシグネチャが正確に一致することを常に確認してください。関数ポインタはイベントハンドラ、プラグイン、C でのストラテジーパターンの基盤です。

c
#include <stdlib.h>

// Function pointer type
typedef int (*compare_fn)(const void *, const void *);

// Comparison function for qsort
int cmp_int(const void *a, const void *b) {
    return *(const int *)a - *(const int *)b;
}

int main() {
    int arr[] = {5, 2, 8, 1, 9, 3};
    size_t n = sizeof(arr) / sizeof(arr[0]);

    // qsort takes a function pointer
    qsort(arr, n, sizeof(int), cmp_int);

    // Array of function pointers
    double (*ops[])(double, double) = {
        add, subtract, multiply, divide
    };
    double result = ops[2](10.0, 3.0);  // multiply

    // Function pointer as parameter
    void apply(int *arr, size_t n, int (*fn)(int)) {
        for (size_t i = 0; i < n; i++) arr[i] = fn(arr[i]);
    }
    apply(arr, n, square);
    return 0;
}

可変長マクロ

可変長マクロ(__VA_ARGS__)は可変引数を受け取り、ロギングとデバッグに便利です。__VA_OPT__(C2x)はカンマを条件付きで含めることでゼロ引数ケースを処理します。##__VA_ARGS__ GCC 拡張は引数が渡されない場合に先行カンマを削除します。リリースビルドで何もコンパイルしないデバッグマクロはコード変更なしでオーバーヘッドを排除します。フォーマット文字列攻撃を防ぐためフォーマット文字列を常にガードしてください。

c
// C99 variadic macros
#define LOG(fmt, ...) printf("[LOG] " fmt "\n", __VA_ARGS__)

// Usage
LOG("Value: %d", x);
LOG("User %s, age %d", name, age);

// GCC __VA_OPT__ (C99/C2x): handle zero arguments
#define LOG2(fmt, ...)     printf("[LOG] " fmt "\n" __VA_OPT__(,) __VA_ARGS__)

// Count arguments (GCC extension)
#define COUNT(...) NARG_(__VA_ARGS__, 5, 4, 3, 2, 1)
#define NARG_(_1, _2, _3, _4, _5, N, ...) N

// Debug macro that compiles out in release
#ifdef NDEBUG
    #define DEBUG(fmt, ...) ((void)0)
#else
    #define DEBUG(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__)
#endif

// ## removes comma if no variadic args (GCC extension)
DEBUG("just a message");
DEBUG("value = %d", x);

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.