C

目次

主要項目のみを表示しています。詳細な小見出しは本文内で確認できます。

概要

まず、この章の中心構造を図で確認します。細部に入る前に、どの概念がどこへつながるかをつかむための地図です。

flowchart LR A["Cソース"] --> B["プリプロセッサ"] B --> C["コンパイル"] C --> D["アセンブル"] D --> E["リンク"] E --> F["実行ファイル"] F --> G["プロセス"] G --> H["スタック"] G --> I["ヒープ"] G --> J["静的領域"]
コード例の読み方

コード例は、そのまま写すためだけのものではありません。直前の本文で「何を確かめる例か」を押さえ、直後の説明で「どの性質が見えるか」を確認してください。実務では、ここに入力の境界、失敗時の挙動、依存する実行環境を足して読むと判断しやすくなります。

要点

Cは、メモリと実行モデルを直接意識しながら、小さな抽象でシステムを記述する言語です。

このページでは、Cのコンパイル単位、型、ポインタ、メモリ管理、未定義動作、標準ライブラリを、低レイヤの見え方と結びつけて整理します。

1. Cとは何か

Cは 「システムプログラミング言語の祖」。Unix・LinuxWindowsmacOSカーネル、組み込み機器、データベース、ネットワークスタック、コンパイラ、JVM ─ ほぼあらゆる 下位層のソフトウェアがCで書かれていると言って過言ではありません。

Cは、**「静的型付け・手続き型・低レベル制御可能・高速」**な言語です。1972年にDennis RitchieがAT&Tベル研究所で開発しました。現在は次のような領域で広く使われています。

  • OSカーネル: LinuxWindowsmacOS、FreeBSDのすべてがCベース
  • 組み込み: マイコン、IoT、車載、医療機器
  • 言語処理系: CPython、Ruby(CRuby)、PHP、Lua
  • データベース: PostgreSQL、SQLite、MySQL、Redis
  • ネットワーク: nginx、curl、OpenSSL
  • 科学技術計算: NumPyの内部、HPC

1-1. Cの歴史(B言語からC23まで)

B言語とUnix(1969〜1972)

1969年、AT&Tベル研究所でKen Thompsonが B言語を開発し、それでUnixの前身を書きました。BはBCPLの派生で、型が無い言語でした。

1972年、Dennis RitchieがB言語を 「型を持たせ、ハードウェアの抽象化を強化」 する形で改造し、C言語が誕生しました。同時にUnixがCで書き直され、OSを高級言語で書く」という当時としては革命的なアプローチを実現しました。

K&R C(1978)

1978年、Brian KernighanとDennis Ritchieが『The C Programming Language』(通称 K&R)を出版。これが事実上の最初のC言語仕様になりました。プログラミング書籍の金字塔として今も読まれています。

// K&R第1版の有名なhello world
main()
{
    printf("hello, world\n");
}

戻り値の型を省略すると int になるなど、現代から見ると緩い言語でした。

ANSI C / C89 / C90(1989〜1990)

1989年、ANSI(米国規格協会)が ANSI C(C89)を制定。1990年にISOが同等の C90 を採択。これが「標準化された最初のC」で、関数プロトタイプ・void 型・列挙型などが整備されました。

C99(1999)

11年後の C99 で大きな改良:

C11(2011)

// C11の主要機能
- _Generic(型ジェネリック)
- _Atomic(アトミック操作)
- _Thread_local(TLS)
- threads.h(標準スレッド)
- 匿名構造体・共用体
- _Static_assert

C17(2018)

C11のbug fixリリース。新機能はほぼなし。

C23(2024)

最新規格。33年ぶりの大型改訂と言える内容:

  • true / false がキーワードに(stdbool.h 不要)
  • nullptr(NULLの型安全な代替)
  • auto 型推論(C++ から)
  • constexpr
  • 2進リテラル 0b1010
  • typeof
  • 属性 [[nodiscard]] [[deprecated]]

Cは 数十年経っても進化を続ける言語です。


1-2. なぜCはこれほど影響力を持つのか

Cの影響範囲は、他のどの言語も追随できないほど広い:

直接的な子孫:
  C++ / Objective-C / D / Rust(思想的)

Cの構文を踏襲:
  Java / C# / JavaScript / TypeScript / Go / Swift / Kotlin / Scala
  → 「{ }」「if」「for」のスタイルはほぼC由来

Cをホストにする:
  CPython / CRuby / PHP / Perl / Lua / R
  → 言語処理系の大半がC実装

ABI(バイナリ互換)の標準:
  どの言語も「C関数を呼び出せる」のがほぼ必須
  → CはOSとアプリの境界の共通言語

Cを理解すると、ほぼすべてのプログラミング言語の設計が見えてくる」と言われる所以です。


1-3. Cの設計哲学

K&Rが掲げたCの設計哲学:

「Trust the programmer.(プログラマを信頼せよ)」

これは「型変換も、メモリアクセスも、危険な操作も、書きたいなら書ける」という思想。安全性チェックを最小限にし、プログラマの意図をそのまま機械に伝えることを優先しました。

Cの特徴的な判断

  • メモリは生で扱う: malloc/freeで直接管理
  • 配列の境界チェックなし: 配列外アクセスで死ぬ
  • 整数オーバーフローはUB(符号付き)
  • ポインタは「整数 + 型情報」: アドレス演算が自由
  • すべての変換が許される: キャストで何でも通る

これらは 諸刃の剣。性能と表現力の代償として、安全性は完全にプログラマの責任になります。これが「Cは危険な言語」と言われる根源で、Rustが登場する遠因にもなりました。

「軽量な抽象化」

int sum(int *arr, size_t n) {
    int total = 0;
    for (size_t i = 0; i < n; i++) total += arr[i];
    return total;
}

Cのループは そのまま機械語に近い形にコンパイルされる。「書いたコードがそのまま動く」という安心感が、システムプログラマに支持される理由です。


1-4. 規格の変遷(K&R → C89 → C99 → C11 → C23)

1972  C誕生
1978  K&R C(『The C Programming Language』)
1989  ANSI C(C89)
1990  ISO C90(C89と同等)
1999  C99(VLA、bool、//コメント、stdint.h)
2011  C11(_Generic、threads.h、_Atomic、匿名struct)
2018  C17(C11のバグ修正)
2024  C23(nullptr、true/false、auto、constexpr、2進リテラル)

実用上は C99 + GCC/Clang拡張を使うのが多数派。C11 / C17の機能(_Atomic_Thread_local_Static_assert)も活用が進んでいます。C23は普及途上。


1-5. このセクションのまとめ

Cの出自:
  1972年Dennis Ritchieがベル研究所で開発
  Unixを書くために生まれ、Unixと共に普及
  K&R本(1978)が事実上の仕様

影響:
  ほぼすべてのOSがCベース
  C++ / Java / JSなど現代言語の構文の祖
  ABIとして言語間の境界の共通言語

哲学:
  「Trust the programmer」
  安全性より表現力と速度
  抽象化を最小限に、機械語に近いコードが書ける

規格:
  K&R → C89 → C99 → C11 → C17 → C23
  実用はC99 + コンパイラ拡張が多数派

次のセクションでは、Cのコンパイル・実行モデルを扱います。


2. コンパイルと実行モデル

Cプログラムが gcc hello.c -o hello && ./hello で動く流れを段階的に追います。プリプロセス・コンパイル・リンクの3段階を理解すると、ヘッダ・宣言・リンクエラーが一気に分かります。


2-1. プリプロセス・コンパイル・リンク

hello.c    ─┐
            ├─→ プリプロセッサ(cpp)─→ 展開された .c
            │   #includeを展開、#defineを置換
            │
            ├─→ コンパイラ(cc1)  ─→ アセンブリ(.s)
            │   構文解析、最適化
            │
            ├─→ アセンブラ(as)   ─→ オブジェクト(.o)
            │   機械語化
            │
            └─→ リンカ(ld)       ─→ 実行ファイル
                複数の .oとlibcを結合

各段階を手動で動かす

gcc -E hello.c -o hello.i      # プリプロセス済み
gcc -S hello.c -o hello.s      # アセンブリ
gcc -c hello.c -o hello.o      # オブジェクト
gcc hello.o -o hello           # リンク

通常 gcc hello.c -o hello は内部でこれらを連続実行している。


2-2. オブジェクトファイルと実行ファイル

.o ファイルは 「シンボル付き機械語」。シンボル(関数名・グローバル変数)の参照と定義が記録され、リンク時に解決されます。

# シンボルを覗く
nm hello.o
# 0000000000000000 T main
#                  U printf

# T = 定義済み(テキストセクション)
# U = 未定義(外部から呼ぶ)

リンクエラーの正体

// foo.c
extern int x;
int main() { return x; }
gcc foo.c -o foo
# undefined reference to 'x'

x を定義した .o が無いとリンクエラー。「リンクエラー = シンボルが解決できていない」と覚える。


2-3. ヘッダと翻訳単位

翻訳単位(Translation Unit)

1つの .c ファイル + そこから #includeされたすべて」が 翻訳単位。これがコンパイルの基本単位。

foo.c (#include <stdio.h>)  →  foo.o    [翻訳単位1]
bar.c (#include "foo.h")    →  bar.o    [翻訳単位2]
                          ↓ リンク
                      実行ファイル

ヘッダの役割

ヘッダ(.h)は 宣言(declaration)の置き場。実装(definition)は .c に書く。

// foo.h
#ifndef FOO_H
#define FOO_H

int add(int a, int b);    // プロトタイプ宣言

#endif
// foo.c
#include "foo.h"

int add(int a, int b) {    // 定義
    return a + b;
}
// main.c
#include "foo.h"

int main() {
    return add(1, 2);
}

ヘッダで宣言を共有し、各 .c が独立にコンパイルされる。リンク時に統合。


2-4. GCC・Clangの主要オプション

# 警告
gcc -Wall -Wextra -Wpedantic     # 推奨3点セット
gcc -Werror                       # 警告をエラー扱い

# 標準
gcc -std=c99 / -std=c11 / -std=c17 / -std=c2x
gcc -ansi                         # C89

# 最適化
gcc -O0 / -O1 / -O2 / -O3 / -Os / -Ofast / -Og

# デバッグ情報
gcc -g                            # gdbで使える情報を埋め込む

# Sanitizer
gcc -fsanitize=address           # AddressSanitizer
gcc -fsanitize=undefined         # UBSan
gcc -fsanitize=thread            # ThreadSanitizer

# インクルードパス・ライブラリ
gcc -I/path/to/headers
gcc -L/path/to/libs -lname

-Wall -Wextra -O2 -g をデフォルト」が現代Cプログラマの基本セット。


2-5. このセクションのまとめ

コンパイルの3段階:
  プリプロセス → コンパイル → アセンブル → リンク
  -E / -S / -c / リンクで段階的に実行可

オブジェクトファイル:
  シンボル付き機械語
  nmでシンボル確認
  「リンクエラー = シンボル未解決」

翻訳単位:
  1つの .c + 含まれる .h
  宣言は .h、定義は .c

主要オプション:
  -Wall -Wextra -O2 -gが現代の基本
  -fsanitize=address/undefined/threadでバグ検出

次のセクションでは、Cの型システムとメモリレイアウトを扱います。


3. 型とメモリレイアウト

Cの型システムは 「機械の型をそのまま見せる」設計です。整数のサイズが処理系依存だったり、暗黙の型変換が複雑だったり、アライメントが効いたりと、低レベルの世界が透けて見えます。


3-1. 整数型のサイズと符号

Cの整数型は 「最低サイズだけ規格で保証される」。実際のサイズは処理系依存。

最低 一般的(64bit Linux/Mac) 範囲(signed)
char 1 byte 1 -128 〜 127
short 2 2 -32768 〜 32767
int 2 4 -2^31 〜 2^31-1
long 4 8 (LP64) / 4 (Windows LLP64) プラットフォーム依存
long long 8 8 -2^63 〜 2^63-1

移植性のためにstdint.hを使う

C99固定幅整数型が標準化されました。

#include <stdint.h>

int8_t    a;    // 必ず8bit
int16_t   b;    // 必ず16bit
int32_t   c;    // 必ず32bit
int64_t   d;    // 必ず64bit
uint8_t   e;    // 符号なし版
size_t    f;    // 配列・サイズ用(unsigned)
ptrdiff_t g;    // ポインタ差分用(signed)

移植性のあるコードでは int32_t などを使うのが現代の作法。int は性能の目安であって、サイズが必要なら固定幅型を。

charの符号は処理系依存

char c = -1;    // 動くかも、動かないかも!

charsignedかunsignedかは処理系依存。明確にしたいなら signed char または unsigned char を使う。


3-2. 浮動小数点

float       a;     // 単精度(約7桁)
double      b;     // 倍精度(約15桁)
long double c;     // 拡張精度(処理系依存、80bitや128bit)

すべて IEEE 754 に準拠。

#include <math.h>
#include <float.h>

FLT_MAX        // floatの最大値
DBL_EPSILON    // doubleの機械イプシロン

// NaN / Inf
double inf = INFINITY;
double nan = NAN;
isnan(x);      // NaNかチェック
isinf(x);      // 無限かチェック

3-3. charと文字エンコーディング

Cの char「1バイトの整数」。文字コードと混同しないことが大事。

char c = 'A';     // 0x41 = 65
char *s = "hello";

マルチバイト・ワイドキャラクタ

#include <wchar.h>

wchar_t wc = L'あ';        // ワイドキャラクタ
wchar_t *ws = L"こんにちは";

ただし wchar_t処理系依存(Linuxで4byte、Windowsで2byte)。現代は UTF-8char * を使うのが主流。

char *utf8 = u8"こんにちは";   // UTF-8文字列
char16_t *utf16 = u"hello";    // C11 UTF-16
char32_t *utf32 = U"hello";    // C11 UTF-32

3-4. 暗黙の型変換と整数昇格

Cには 整数昇格(integer promotion)」通常の算術変換」というルールがあり、知らずに書くとバグります。

整数昇格

charshort は演算前に 自動的に int に昇格します。

char a = 100, b = 100;
char c = a + b;          // 200ではなく -56!(intで計算後にcharに切り捨て)
int  d = a + b;          // 200(OK)

符号の混在

signedunsigned を混ぜると unsigned に統一される。

unsigned int u = 1;
int s = -1;

if (s < u) printf("yes");
else       printf("no");
// "no" が出力される!
// sがunsignedに変換されて巨大な値になるため

これが 「Cの最も嵌る罠の一つ」。コンパイラの -Wsign-compare 警告で検出可能。

型変換の優先順位

1. 同じ型同士はそのまま
2. char/short → intに昇格
3. 一方がlong, long long, double, floatなら高い方に揃える
4. 符号が違うならunsignedに揃える(注意!)

3-5. アライメントとパディング

CPU「型のサイズの倍数のアドレス」 で値を読むのが効率的。これをアライメントと言う。

struct A {
    char  a;     // 1 byte
    int   b;     // 4 byte
    char  c;     // 1 byte
};
sizeof(struct A);    // 12(1 + 3 padding + 4 + 1 + 3 padding)
オフセット
  0: a (1)
  1: padding (3)    ← intを4の倍数アドレスに
  4: b (4)
  8: c (1)
  9: padding (3)    ← structのサイズを4の倍数に

サイズ最適化

struct B {
    int  b;
    char a;
    char c;
};
sizeof(struct B);    // 8(4 + 1 + 1 + 2 padding)

サイズの大きいフィールドから順に並べる」とパディングが減る。

_Alignof / _Alignas(C11)

_Alignof(int)      // 通常4
_Alignas(16) int x;    // 16 byte境界に揃える

SIMD命令などでアライメント要求がある場合に使う。


3-6. このセクションのまとめ

整数型:
  サイズは処理系依存(intは32bitとは限らない)
  移植性のためにstdint.h(int32_t / size_tなど)

浮動小数点:
  float (32bit) / double (64bit) / long double(処理系依存)
  IEEE 754準拠、NaN / Infあり

charと文字:
  charは1バイト整数(符号は処理系依存)
  現代はUTF-8 + char* が主流

暗黙の型変換:
  char/shortは演算前にint昇格
  signedとunsignedを混ぜるとunsigned優先(罠)
  -Wsign-compareで検出

アライメント:
  CPUは型サイズの倍数アドレスで読む
  structはパディングが入る → サイズ大きい順に並べる
  _Alignasで明示的に揃える(C11)

次のセクションでは、Cの演算子と式――特にシーケンスポイントの罠を扱います。


4. 演算子と式

Cの演算子は機械語に近いシンプルさですが、「副作用の評価順序」「優先順位」「整数昇格との組み合わせ」 で混乱を招きます。


4-1. 算術・比較・論理

5 + 2     // 7
5 - 2     // 3
5 * 2     // 10
5 / 2     // 2(整数除算)
5.0 / 2   // 2.5
5 % 2     // 1(剰余)

// 比較
1 < 2     // 1(Cの真偽はintで0 / 非0)
1 == 1    // 1

// 論理
1 && 0    // 0
1 || 0    // 1
!1        // 0

真偽の扱い

Cには伝統的に bool型がなくint0 / 非0 で真偽を表現していました。C99_Bool 型と <stdbool.h> が導入されました。

#include <stdbool.h>

bool flag = true;     // C99+ で書ける

C23では true / false がキーワードになり、stdbool.h が不要に。


4-2. ビット演算

Cはビット操作の表現力が豊かで、低レベル操作で多用されます。

0b1100 & 0b1010    // 0b1000(AND)
0b1100 | 0b1010    // 0b1110(OR)
0b1100 ^ 0b1010    // 0b0110(XOR)
~0b1100            // ...11110011(NOT、補数)
0b0001 << 3        // 0b1000(左シフト)
0b1000 >> 2        // 0b0010(右シフト)

C99までは 0b... リテラルがなく 0xc のような16進数を使うのが慣例でした。C23で2進数リテラルが標準化。

ビットフィールド操作のイディオム

// ビットを立てる
flags |= (1 << 3);

// ビットを下ろす
flags &= ~(1 << 3);

// ビットをトグル
flags ^= (1 << 3);

// ビットを確認
if (flags & (1 << 3)) { ... }

4-3. 演算子の優先順位とよくある罠

Cの演算子の優先順位は複雑で、括弧で明示するのが安全。

// よくある罠
if (a & 1 == 0)    // (a & (1 == 0)) と解釈される!
                    // == の方が & より優先順位が高い
if ((a & 1) == 0) // 正しい

// ポインタとインクリメント
*p++       // *(p++) と解釈される
(*p)++     // 値をインクリメントしたいなら明示

推奨

- 同じ式内で複数の演算子を使うときは括弧
- 自分でも他人でも迷うなら括弧
- 「優先順位を覚えるよりは括弧」

4-4. 副作用の評価順序とシーケンスポイント

Cでは 「同じ式内で同じ変数を複数回変更すると未定義動作(UB)」というルールがあります。

int i = 0;
i = i++ + ++i;       // UB!

a[i] = i++;           // UB!

f(g(), h());          // gとhの評価順は未規定

シーケンスポイント

この点までに副作用がすべて完了することが保証される」境界。

- ; の後
- 関数呼び出し前
- &&, ||, ?:, , 演算子の左側評価後
- 関数の引数評価後

C11でこの概念は 「sequenced before/after」 に置き換えられましたが、本質は同じ。

安全な書き方

// Bad
i = i++ + 1;

// Good
i++;
i = i + 1;

1つの式で同じ変数を複数回変更しない」が黄金律。


4-5. このセクションのまとめ

算術・比較・論理:
  / は整数除算
  Cの真偽はintの0 / 非0
  C99+ でbool(stdbool.h)、C23でキーワード化

ビット演算:
  & | ^ ~ << >>
  フラグ操作のイディオム: |= / &= ~ / ^= / & で確認

優先順位:
  迷ったら括弧(特に & と ==、&& と ||)
  *p++ は *(p++)、(*p)++ で明示

副作用の評価順序:
  同じ式内で同じ変数を複数回変更するとUB
  i = i++ やa[i] = i++ は書かない
  シーケンスポイント(;, &&, ?:, ,)が境界

次のセクションでは、Cの制御フローを扱います。


5. 制御フロー

Cの制御フローは古典的: if/switch/for/while/do-while、そして gotosetjmp/longjmp という非局所脱出機構もあります。


5-1. if / else / switch

if (x > 0) {
    printf("positive");
} else if (x < 0) {
    printf("negative");
} else {
    printf("zero");
}

switch

switch (day) {
    case 1: case 2: case 3: case 4: case 5:
        printf("weekday");
        break;
    case 6:
    case 7:
        printf("weekend");
        break;
    default:
        printf("invalid");
}

break を忘れると fall-through(次のcaseに流れる)。これが古典的な罠。switch で扱える型は 整数型・列挙型のみ。文字列は使えません。

C23では [[fallthrough]] 属性で意図的なfall-throughを明示できます。

case 1:
    do_thing();
    [[fallthrough]];
case 2:
    do_more();
    break;

5-2. for / while / do-while

for (int i = 0; i < 10; i++) {     // C99で初期化での宣言が可能に
    printf("%d\n", i);
}

int i = 0;
while (i < 10) {
    i++;
}

do {
    i--;
} while (i > 0);

無限ループ

for (;;) { ... }      // 無限ループの慣用句
while (1) { ... }     // こちらも一般的

5-3. break / continue / goto

for (int i = 0; i < 10; i++) {
    if (i == 5) break;
    if (i % 2) continue;
    printf("%d\n", i);
}

goto

Cには goto があります。多重ループからの脱出共通エラー処理 で使われます。

int process() {
    void *resource1 = NULL, *resource2 = NULL;

    resource1 = malloc(...);
    if (!resource1) goto err;

    resource2 = malloc(...);
    if (!resource2) goto err;

    // 処理...

    free(resource2);
    free(resource1);
    return 0;

err:
    free(resource2);
    free(resource1);
    return -1;
}

これは 「クリーンアップを1箇所に集約」するLinuxカーネル流のイディオム。RAIIのないCで広く使われます。

gotoの制約

goto同じ関数内のラベルにしかジャンプできない。関数を超えたジャンプは setjmp/longjmp


5-4. setjmp / longjmp

例外処理の代わり」として使える非局所脱出。スタックを超えてジャンプできる。

#include <setjmp.h>

jmp_buf env;

void risky() {
    longjmp(env, 1);    // envでsetjmpした場所に戻る
}

int main() {
    if (setjmp(env) == 0) {
        risky();
    } else {
        printf("caught\n");
    }
}

ただし setjmp/longjmpリソースリークを起こしやすく、現代では使用が制限されます(割り込みハンドラの脱出など特殊用途のみ)。


5-5. このセクションのまとめ

分岐:
  if / else if / else
  switch(整数・列挙型のみ、break必須、C23で [[fallthrough]])

ループ:
  for / while / do-while
  for(;;) / while(1) で無限ループ
  C99でfor初期化部での宣言可能

ジャンプ:
  break / continue
  gotoは同関数内のみ、エラークリーンアップに使う
  setjmp/longjmpは非局所脱出(特殊用途)

次のセクションでは、Cの関数を扱います。


6. 関数

Cの関数は 「単一戻り値・値渡し・前方宣言が必要」 という基本に、関数ポインタ・可変長引数・static/extern が加わったシンプルな世界。


6-1. 関数定義とプロトタイプ宣言

// プロトタイプ宣言(ヘッダなどに)
int add(int a, int b);

// 定義
int add(int a, int b) {
    return a + b;
}

Cでは 関数を呼ぶ前に宣言が必要。ヘッダに宣言、.c に定義、というのが定石。

voidと 引数なし

void greet(void);    // 引数なし
void greet() { ... } // C89 / 古いK&Rでは「引数の数を指定しない」意味だった

C23では ()(void) と同じ意味になりました(K&R互換性が捨てられた)。


6-2. 引数の値渡し

Cの引数渡しは すべて値渡し。配列とポインタを除き、関数内の変更は呼び出し側に影響しません。

void add_one(int x) {
    x++;    // ローカルが変わるだけ
}

int n = 5;
add_one(n);
// nは5のまま

「参照渡し」を実現するにはポインタ

void add_one(int *x) {
    (*x)++;
}

int n = 5;
add_one(&n);
// nは6

Cには 参照型がないので、ポインタで擬似的に参照渡しを実現します。

配列はポインタとして渡される

void process(int arr[10]) { ... }    // 実はint *arrと同じ
void process(int *arr) { ... }       // 同等

int data[10];
process(data);    // 配列の先頭ポインタが渡る

これは初心者を混乱させる罠。「配列は関数引数では型情報を失ってポインタになる」と覚える。サイズも別途渡す必要があります。


6-3. staticとextern

static(ファイルスコープ・関数)

static を関数や変数に付けると そのファイル内でのみ見えるようになります。「翻訳単位ローカル」とも。

static int counter = 0;             // このファイルでだけ見える

static void helper() { ... }        // 同上、リンクされない

これにより内部実装を隠蔽し、他ファイルとの名前衝突を避けられます。

static(関数内ローカル)

関数内 static 変数は 値が呼び出し間で保持されます。

int counter() {
    static int n = 0;
    return ++n;
}

counter();   // 1
counter();   // 2
counter();   // 3

extern

別の翻訳単位の変数を「ここから見える」と宣言。

// foo.c
int global_x = 42;

// bar.c
extern int global_x;    // 宣言だけ、他の .cで定義されている

void f() { printf("%d\n", global_x); }

6-4. inlineと再帰

inline

インライン展開してほしい」というヒント(強制ではない)。

inline int square(int x) { return x * x; }

C99inline は仕様が複雑で、ヘッダに書くなら static inlineextern inline の使い分けに注意が必要。

再帰

int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

Cは末尾呼び出し最適化を保証しないので、深い再帰はスタックオーバーフロー注意。


6-5. 可変長引数(varargs)

#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, 1, 2, 3);    // 6

printf などが内部で使う仕組み。型安全ではないため、現代では推奨されない。


6-6. 関数ポインタ

Cの柔軟性の核。関数を変数に代入し、引数として渡せます

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

int (*op)(int, int) = add;    // 関数ポインタ
op(1, 2);                       // 3

op = sub;
op(5, 2);                       // 3

// 配列にも入れられる
int (*ops[2])(int, int) = { add, sub };
ops[0](1, 2);    // 3

typedefで読みやすく

typedef int (*BinOp)(int, int);

BinOp op = add;
op(1, 2);

qsortの使い方(古典例)

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

int arr[] = {3, 1, 4, 1, 5, 9, 2, 6};
qsort(arr, 8, sizeof(int), compare);

qsort比較関数を関数ポインタで受け取るジェネリックなソート。これがCの代表的な高階関数の例です。


6-7. このセクションのまとめ

関数:
  プロトタイプ宣言が必要、ヘッダに置くのが定石
  値渡しのみ、参照渡し相当はポインタで

static / extern:
  static: 翻訳単位内に隠蔽、または値の保持
  extern: 別翻訳単位の変数の宣言

inline / 可変長引数:
  inlineはヒント、複雑なので慎重に
  varargsは型安全でない、printf系のみ

関数ポインタ:
  関数を変数化、引数で渡せる
  int (*fn)(int, int) の構文
  typedefで読みやすく
  qsortのような汎用APIの核

次のセクションでは、Cの核――ポインタを徹底解説します。


7. ポインタ

Cを理解する上で最も重要な概念が ポインタ。配列、文字列、関数、メモリ管理、データ構造、すべてがポインタで動いています。「ポインタが分かればCが分かる」と言われる所以です。


7-1. ポインタとは何か

ポインタは 「メモリアドレスを保持する変数」。型を持つ点が「ただのアドレス」と違います。

int x = 42;
int *p = &x;    // pはxのアドレスを持つ

printf("%d\n", x);     // 42
printf("%p\n", &x);    // 0x7fff...(アドレス)
printf("%p\n", p);     // 同じアドレス
printf("%d\n", *p);    // 42(参照外し)

「型付きアドレス」

ポインタには 「指している先の型」 がついている。これがアドレス計算と参照外しの基礎。

int *pi;       // intを指すポインタ
char *pc;      // charを指す
double *pd;    // doubleを指す

sizeof(pi);    // ポインタ自体のサイズ(64bitシステムで8)
sizeof(*pi);   // intのサイズ(4)

7-2. & と * の意味

int x = 42;

int *p = &x;    // & = 「アドレスを取る」
int  y = *p;    // * = 「アドレスの先の値を取る(参照外し)」
*p = 100;       // * で書き込みもできる(xが100になる)

宣言と使用で * の意味が違うので混乱しがち:

int *p;     // ここの * は「ポインタ型」を意味する
*p = 5;     // ここの * は「参照外し」

7-3. ポインタ算術

ポインタには整数を足し引きでき、型のサイズ単位でアドレスが進む

int arr[5] = {10, 20, 30, 40, 50};
int *p = arr;

p + 1;        // arr[1] のアドレス(4 byte進む)
*(p + 1);     // 20
*(p + 2);     // 30

p++;          // pがarr[1] を指すように
*p;           // 20

配列インデックスとポインタの等価性

arr[i]   ←→   *(arr + i)
&arr[i]  ←→   arr + i

これが 「配列とポインタは似ているが同じではない」 の核。詳細は次章。


7-4. ヌルポインタ

何も指さないポインタ」を表す特別な値。

int *p = NULL;
if (p != NULL) { *p = 5; }    // nullチェック

NULLと0とnullptr

// 伝統的
NULL                          // (void*) 0や0にマクロ展開される

// C23
nullptr                        // 型安全なNULL

C23nullptr キーワードが追加されました。NULL だと int 0とも互換だったため、可変長引数などで型曖昧性が問題になっていた解決策です。


7-5. constポインタ

const の位置で意味が変わる。

int       *p;            // 普通のポインタ
const int *p;            // 指す先を変更不可(ポインタ自体は変更可)
int *const p;            // ポインタ自体は変更不可(指す先は変更可)
const int *const p;      // 両方変更不可

読み方のコツ

* の左側が指す先、右側がポインタ自身」と覚える。

const int *p;      // 「const int」を指すポインタ
int *const p;      // 「constポインタ」が「int」を指す

関数引数で重要

size_t strlen(const char *s);    // sの中身を変更しないと宣言
void *memcpy(void *dest, const void *src, size_t n);

読み取り専用ならconstを付ける」のが現代Cの作法。


7-6. 多重ポインタ

ポインタを指すポインタ、さらにそれを指すポインタ……と多重化できます。

int x = 42;
int *p = &x;
int **pp = &p;

*pp;        // p(int *)
**pp;       // 42(int)

関数で「ポインタを変更したい」

void allocate(int **pp) {
    *pp = malloc(sizeof(int));
    **pp = 100;
}

int *p;
allocate(&p);     // 関数内でmallocされたアドレスがpに入る

これがCで「関数からポインタを返すには参照渡しが必要」の根拠。

char ** とargv

int main(int argc, char *argv[]) {
    // argvはchar* の配列
    // 等価: int main(int argc, char **argv)
}

7-7. voidポインタ

void *「型を持たないポインタ」。あらゆる型のポインタと相互に変換可能。

void *p = malloc(100);    // mallocはvoid * を返す
int *ip = p;               // 暗黙キャストOK(Cでは)

memcpy(dest, src, n);      // void *dest, const void *src

メモリ操作・汎用コンテナで活躍。ただし 型情報を失うので注意。

qsortの比較関数

int compare(const void *a, const void *b) {
    int x = *(const int*)a;
    int y = *(const int*)b;
    return x - y;
}

C++ のtemplatesやRustのgenericsに相当する仕組みがCには無いので、void * でジェネリックに書きます。


7-8. このセクションのまとめ

ポインタ:
  メモリアドレス + 型情報を持つ変数
  & で「アドレスを取る」、* で「参照外し」

ポインタ算術:
  p + nで型サイズ × nだけ進む
  arr[i] = *(arr + i)

NULL:
  「何も指さない」を表す特別な値
  C23でnullptrが型安全な代替

const:
  const int *p指す先const
  int *const pポインタ自身const
  関数引数で読み取り専用を表明

多重ポインタ:
  int **ppで関数からポインタを返す
  argvはchar ** またはchar *[]

void *:
  型を持たないポインタ
  malloc / qsortなどで活躍
  型情報を失うので注意

次のセクションでは、配列と文字列を扱います。


8. 配列と文字列

Cの配列と文字列の関係はシンプルでありつつ、ポインタとの関係が混乱を招きます。「配列はポインタではない、しかし関数引数では実質的にポインタになる」という区別が要点です。


8-1. 配列の本質

int arr[5] = {1, 2, 3, 4, 5};
int arr2[] = {1, 2, 3};         // サイズは推論される
int arr3[100] = {0};            // 全要素を0で初期化
int arr4[5] = {[2] = 10};       // 指定初期化(C99+)。arr4 = {0, 0, 10, 0, 0}

配列のサイズ

int arr[5];
sizeof(arr);            // 20(5 * 4)
sizeof(arr) / sizeof(arr[0]);    // 5(要素数)

sizeof(arr) / sizeof(arr[0])」が要素数を求める伝統的なイディオム。

#define COUNT_OF(arr) (sizeof(arr) / sizeof((arr)[0]))

ヘッダで定義しておくのが定石。


8-2. 配列とポインタの関係

配列は「アドレス」になりがち

int arr[5] = {1, 2, 3, 4, 5};
int *p = arr;        // arrは &arr[0] と同じ意味で「減衰」する

p[0];     // arr[0] と同じ
p[3];     // arr[3] と同じ
*(p + 2); // arr[2] と同じ

Cでは多くの場面で 配列名が「先頭要素のポインタ」に自動変換されます。これを 「配列の減衰(array decay)」 と呼びます。

関数引数では型情報が消える

void f(int arr[10]) { sizeof(arr); }    // 8か4(ポインタのサイズ)!
void g(int *arr)    { sizeof(arr); }    // 同上

関数の int arr[]int * と完全に同じ意味になり、サイズ情報は消えます。これが嵌りどころで、関数にはサイズも一緒に渡すのが鉄則。

int sum(const int *arr, size_t n) {
    int total = 0;
    for (size_t i = 0; i < n; i++) total += arr[i];
    return total;
}

int data[5] = {1, 2, 3, 4, 5};
sum(data, 5);

配列とポインタの違い

int arr[5];
int *p = arr;

sizeof(arr);    // 20(配列のサイズ)
sizeof(p);      // 8(ポインタのサイズ)

&arr;           // int (*)[5] 型(配列へのポインタ)
&p;             // int ** 型

「配列はポインタの似て非なるもの」と覚える。sizeof& で違いが見える。


8-3. 多次元配列

int matrix[3][4];           // 3行4列
matrix[1][2] = 42;

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

メモリは 行優先(row-major) で連続配置。

matrix[0][0] matrix[0][1] matrix[0][2] matrix[0][3]
matrix[1][0] matrix[1][1] ...

関数引数

void process(int m[3][4]) { ... }    // [3] は省略不可じゃない、[4] は必要
void process(int m[][4])  { ... }    // 等価
void process(int (*m)[4]) { ... }    // 「int 4要素配列へのポインタ」

これは初心者には謎の構文。「最初の次元以外は型情報として必要」と覚える。

可変長配列(VLA、C99+)

void process(size_t rows, size_t cols, int m[rows][cols]) { ... }

C99で導入されたが、C11でoptional化(実装が必須でなくなった)され、Microsoft Visual C++ は対応していません。移植性のために避けるのが現代の作法。


8-4. C文字列(null終端)

Cには 「文字列型」がありません。文字列は char の配列に末尾 \0 を置いたもの

char s[] = "hello";    // 6 byte: 'h','e','l','l','o','\0'
char *t = "hello";     // 文字列リテラルへのポインタ(変更不可)

s[0] = 'H';            // OK
t[0] = 'H';            // UB!リテラルは変更してはいけない

文字列リテラルはconst

const char *s = "hello";    // 推奨(書き込み禁止が型で表現される)
char *s = "hello";          // 動くが、書き込み禁止に注意
char  s[] = "hello";        // 配列なので書き込み可能

sizeofとstrlen

char s[] = "hello";
sizeof(s);     // 6('\0' を含む配列のサイズ)
strlen(s);     // 5('\0' を除く文字数)

これも嵌りどころ。sizeof は配列のバイト数、strlen は実際の文字数。


8-5. string.hの主要関数

#include <string.h>

strlen(s);                  // 長さ
strcpy(dst, src);           // コピー(dstが十分大きいことを呼び出し側で保証)
strncpy(dst, src, n);       // n文字までコピー('\0' 自動付加なし)
strcat(dst, src);           // 連結
strncat(dst, src, n);
strcmp(s1, s2);             // 比較(== ではない!)
strncmp(s1, s2, n);
strchr(s, c);               // 文字検索
strstr(s, sub);             // 部分文字列検索
strdup(s);                  // 動的にコピー(POSIX、C23で標準化)

memcpy(dst, src, n);        // 任意のメモリコピー
memset(dst, c, n);          // nバイトをcで埋める
memcmp(p1, p2, n);          // バイナリ比較

バッファオーバーフローの罠

char buf[10];
strcpy(buf, "this is too long");    // バッファ越え!UB(攻撃の温床)

// より安全
strncpy(buf, src, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';

// snprintfが現代的
snprintf(buf, sizeof(buf), "%s", src);

strcpy strcat getsバッファオーバーフローを起こしやすいため、現代では snprintf strncpy を使うのが推奨。getsC11で完全に削除されました。


8-6. このセクションのまとめ

配列:
  int arr[N];
  COUNT_OF(arr) で要素数(sizeof / sizeof[0])
  関数引数では型情報が消える(ポインタになる)→ サイズも渡す

配列とポインタ:
  「配列の減衰」で先頭ポインタに自動変換
  しかしsizeofと & の挙動は別物
  「配列とポインタは似ているが同じではない」

文字列:
  char配列 + '\0' 終端
  リテラルはconst、書き込み不可(UB)
  sizeofは配列のバイト数、strlenは文字数

string.h:
  strlen / strcpy / strcat / strcmp / strstr
  memcpy / memset / memcmp
  バッファオーバーフロー回避にはsnprintf

次のセクションでは、構造体・共用体・列挙型を扱います。


9. 構造体・共用体・列挙型

Cのデータ構造は struct(構造体)・union(共用体)・enum(列挙型) の3つ。クラスはありませんが、これらを組み合わせて多彩なデータ表現ができます。


9-1. struct

struct Point {
    int x;
    int y;
};

struct Point p1 = {3, 4};
struct Point p2 = {.x = 3, .y = 4};   // 指定初期化(C99+)

p1.x = 10;
printf("%d\n", p1.y);

ポインタ経由のアクセス

struct Point *p = &p1;
(*p).x;        // 値経由
p->x;          // ポインタ経由(同等)

->「ポインタからメンバアクセス」 の糖衣構文。(*p).x よりずっと使われます。

structの代入とコピー

struct Point a = {1, 2};
struct Point b = a;       // 全フィールドがコピーされる(値型)
b.x = 100;
a.x;                       // 1(影響なし)

Cのstructは 値型。代入で全体がコピーされます。

structの比較

a == b    // コンパイルエラー!structには比較演算子がない
memcmp(&a, &b, sizeof(a));    // 一応比較できるが、パディングがあるので注意

メンバごとに比較するか、専用関数を書くのが安全。


9-2. typedef

typedef で型に別名を付けられる。

typedef struct Point Point;     // struct PointをPointと書ける

Point p = {1, 2};

// 1度に書く
typedef struct {
    int x, y;
} Point;

typedef抽象化の道具。実装を隠してAPIを整える。

typedef struct File File;       // 不透明型(中身は隠す)
File *open(const char *path);
void close(File *f);

ヘッダには typedef struct File File; だけ書き、.cstruct File { ... } を定義する。これがCで「カプセル化」を実現する標準パターン(PIMPLイディオム)。


9-3. ビットフィールド

メンバの ビット幅を指定できる。フラグ集合などに使用。

struct Flags {
    unsigned int active : 1;
    unsigned int role   : 3;
    unsigned int level  : 4;
    unsigned int        : 0;    // 残りを揃える
};

struct Flags f = {0};
f.active = 1;
f.role = 5;    // 0〜7

サイズが圧縮できる反面、移植性(メモリレイアウトが処理系依存)に注意。


9-4. union

union複数のメンバが同じメモリを共有する型。

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

union Value v;
v.i = 0x41424344;
printf("%c\n", v.bytes[0]);   // 'A' か 'D'(エンディアン依存)

tagged union(判別共用体)

実用ではstructと組み合わせて 判別共用体を作るのが定番。

enum Tag { TAG_INT, TAG_FLOAT, TAG_STR };

struct Value {
    enum Tag tag;
    union {
        int    i;
        float  f;
        char  *s;
    } as;
};

struct Value v = { .tag = TAG_INT, .as.i = 42 };

switch (v.tag) {
    case TAG_INT:   printf("%d\n", v.as.i); break;
    case TAG_FLOAT: printf("%f\n", v.as.f); break;
    case TAG_STR:   printf("%s\n", v.as.s); break;
}

これがCで 「複数の型のうちのどれか」を表現する標準パターン。OCaml/Rustの代数的データ型に相当します。


9-5. enum

enum Color { RED, GREEN, BLUE };
// 暗黙的にRED=0, GREEN=1, BLUE=2

enum Status {
    OK    = 0,
    ERROR = -1,
    BUSY  = 100
};

enumと整数の関係

Cの enum は実質的に int の別名。型安全性は弱い。

enum Color c = 999;    // 警告すら出ないことが多い

C++ の enum class、Javaの enum のような厳格な型ではない点に注意。

C23enum の基底型を指定できるようになりました。

enum Color : unsigned char { RED, GREEN, BLUE };    // C23

9-6. このセクションのまとめ

struct:
  値型、代入で全体コピー
  -> はポインタ経由アクセスの糖衣
  比較はmemcmpではなく専用関数
  指定初期化(.field = value)がC99+

typedef:
  型に別名
  不透明型でカプセル化(PIMPL)

ビットフィールド:
  : Nでビット幅指定
  サイズ削減、移植性は注意

union:
  メモリ共有
  tagged union(enum + struct + union)がADT風

enum:
  整数定数の集合
  実質intの別名で型安全性は弱い
  C23で基底型指定可能

次のセクションでは、Cで最も注意すべき領域――動的メモリ管理を扱います。


10. 動的メモリと所有権

CにはGCがありません。メモリは自分で確保し、自分で解放する。これがCプログラミングの最大の責任であり、最大のバグの源でもあります。


10-1. malloc / calloc / realloc / free

#include <stdlib.h>

int *p = malloc(100 * sizeof(int));    // 100個分のintを確保
if (p == NULL) { /* エラー処理 */ }
p[0] = 42;
free(p);    // 解放

int *q = calloc(100, sizeof(int));     // 0で初期化された確保
free(q);

int *r = malloc(10 * sizeof(int));
r = realloc(r, 20 * sizeof(int));      // サイズ変更
free(r);

malloc / calloc / reallocの違い

関数 動作
malloc(n) nバイト確保(中身は不定)
calloc(n, sz) n × szバイト確保(0初期化)
realloc(p, n) pのサイズをnに変更(拡縮)
free(p) pを解放(NULLを渡しても安全)

reallocの罠

int *p = malloc(10 * sizeof(int));
p = realloc(p, 20 * sizeof(int));    // reallocがNULLを返したら?
                                      // 元のpが失われる!

正しくは:

int *new_p = realloc(p, 20 * sizeof(int));
if (new_p == NULL) {
    free(p);   // 元のは生きているので忘れず解放
    return -1;
}
p = new_p;

10-2. メモリリーク・ダブルフリー・ダングリングポインタ

Cのメモリ管理の3大バグ。

メモリリーク

void leaky() {
    int *p = malloc(100);
    return;    // free忘れ → リーク
}

ダブルフリー

free(p);
free(p);    // UB!クラッシュかセキュリティ脆弱性

防止策:

free(p);
p = NULL;    // 二重解放を防ぐ

ダングリングポインタ(dangling pointer)

int *p = malloc(...);
free(p);
*p = 100;    // UB!解放済みメモリへの書き込み
int *get_local() {
    int x = 42;
    return &x;    // UB!関数を抜けるとxはスタック上から消える
}

解放したメモリを参照しない」「関数のローカル変数のアドレスを返さない」が鉄則。


10-3. スタックvsヒープ

              ┌──────────────┐
高アドレス    │   Stack       │ ← 関数呼び出し、ローカル変数
              │ ↓ 伸びる方向  │   サイズに上限あり(数MB程度)
              │               │   自動的に解放される
              ├──────────────┤
              │   Heap        │ ← malloc / new
              │ ↑ 伸びる方向  │   サイズの上限大きい
              │               │   手動でfree必須
              ├──────────────┤
              │   BSS         │ ← 未初期化グローバル
              │   Data        │ ← 初期化済みグローバル
              │   Text(Code)  │ ← 機械語
低アドレス    └──────────────┘
int global_x = 0;     // BSS or Data
const char *literal = "hello";   // Textセクション

void f() {
    int local = 0;            // Stack
    int *heap = malloc(100);  // Heap
    static int s = 0;         // BSS or Data(関数ローカルだがアドレスは固定)
}

短命な変数はスタック、長く生きるならヒープ」。スタックは速く自動解放されるが、巨大配列はオーバーフローする。


10-4. 所有権の慣習と命名

Cには言語レベルの所有権の概念がないので、命名と慣習で表現します。

よくある慣習

// 戻り値がmalloc済み → 呼び出し側でfree
char *strdup(const char *s);
char *get_name(void);

// 引数で受け取って解放
void destroy(struct Foo *foo);
void free_name(char *name);

// 引数を借りる(解放してはいけない)
size_t strlen(const char *s);

// out引数(呼び出し側に新しいオブジェクトを返す)
int create(struct Foo **out);

create / destroyペア

struct Foo *foo_create(void);
void        foo_destroy(struct Foo *foo);

struct Foo *foo = foo_create();
// ... 使う ...
foo_destroy(foo);

Cライブラリで広く使われるパターン。


10-5. Sanitizerによる検査

GCC / Clangの AddressSanitizer(ASan)と MemorySanitizerValGrind などで動的にメモリバグを検出できます。

# AddressSanitizer
gcc -fsanitize=address -g main.c -o main
./main    # メモリエラーがあれば詳細レポート

# Valgrind
valgrind --leak-check=full ./main

ASanは本番でも使えるくらい高速で、「すべてのC開発者は常時有効化すべき」と言われるレベルの強力ツール。


10-6. このセクションのまとめ

malloc / calloc / realloc / free:
  mallocは不定値、callocは0初期化
  reallocは失敗時に元をfreeしない(自分で対処)
  free(NULL) は安全

3大バグ:
  メモリリーク(free忘れ)
  ダブルフリー(freeを2回 → free後にNULL代入で防ぐ)
  ダングリングポインタ(解放後アクセス、ローカル変数のアドレス返却)

スタックvsヒープ:
  スタック: 高速、自動、サイズ制限
  ヒープ: 大量、手動、free必須

所有権:
  言語レベルの仕組みなし、命名と慣習で表現
  create/destroyペア、out引数、const引数

ツール:
  -fsanitize=address(ASan)
  valgrind --leak-check=full
  全開発者の必携ツール

次のセクションでは、プリプロセッサを扱います。


11. プリプロセッサ

#include#define#ifdef など # で始まる命令はすべて プリプロセッサが処理します。コンパイル前のテキスト変換で、強力ですが落とし穴も多い世界。


11-1. #include

#include <stdio.h>      // 標準ヘッダ(システムパス)
#include "myheader.h"   // ローカルヘッダ(カレント・指定パス)

< > はシステム、" " はローカル」が慣習。実装的には <>-I で指定したパスのみ、"" はソースと同じディレクトリも探します。

内部で何が起こるか

#include "foo.h"「foo.hの中身をその場に丸ごと貼り付け」るだけ。だからインクルードが多重に起こると同じ宣言が2回見えてしまいます。


11-2. #defineとマクロ

定数マクロ

#define PI 3.14159
#define MAX_SIZE 100

double area = PI * r * r;

関数マクロ

#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int n = SQUARE(5);     // → ((5) * (5)) → 25

C++ では inline 関数や constexpr で代替されますが、Cではマクロが標準的な定数・小関数の手段。

#、## 演算子

#define STR(x) #x          // 文字列化
#define CONCAT(a, b) a##b  // 連結

STR(hello)         // "hello"
CONCAT(foo, bar)   // foobar

11-3. 条件付きコンパイル

#if PROD
#define LOG(...) ((void)0)
#else
#define LOG(...) printf(__VA_ARGS__)
#endif

#ifdef _WIN32
#include <windows.h>
#else
#include <unistd.h>
#endif

#if __STDC_VERSION__ >= 201112L
// C11機能を使うコード
#endif

OS依存・コンパイラ依存・規格依存のコードを切り替えるのに必須。


11-4. インクルードガード

ヘッダの 二重include を防ぐ仕組み。

// foo.h
#ifndef FOO_H
#define FOO_H

// 宣言
int add(int, int);

#endif

#pragma once

非標準だが多くのコンパイラで対応。

// foo.h
#pragma once

int add(int, int);

シンプルだが標準ではないので、標準的なプロジェクトはインクルードガードを使うことが多い。


11-5. マクロの落とし穴

1. 引数を必ず括弧で囲む

#define SQUARE(x) x * x        // Bad
SQUARE(1 + 2);                  // → 1 + 2 * 1 + 2 = 5(!)

#define SQUARE(x) ((x) * (x))   // Good
SQUARE(1 + 2);                  // → ((1+2)*(1+2)) = 9

2. 副作用がある引数

#define MAX(a, b) ((a) > (b) ? (a) : (b))
int x = 5;
int y = MAX(x++, 10);    // x++ が2回評価されるかも

3. デバッグしにくい

マクロ展開後のコードがエラーメッセージに出るので、原因が見えにくい。

現代の指針

- 定数: constかenumを優先(可能なら)
- 小関数: inline関数を優先
- やむを得ずマクロなら全引数を括弧
- 副作用のある式は渡さない

11-6. このセクションのまとめ

#include:
  <> はシステム、" " はローカル
  内容を丸ごと貼り付ける単純な操作

#define:
  定数 / 関数マクロ
  # 文字列化、## 連結

条件付きコンパイル:
  #if / #ifdef / #ifndef / #endif
  OS / コンパイラ / 規格依存の切り替え

インクルードガード:
  #ifndef HEADER_H ... #define HEADER_H ... #endif
  または #pragma once(非標準だが普及)

マクロの落とし穴:
  全引数を括弧で囲む
  副作用のある引数を渡さない
  可能ならinline関数 / constに置き換え

次のセクションでは、Cの標準ライブラリを扱います。


12. 標準ライブラリ

Cの標準ライブラリは小さく、stdio / stdlib / string / math が4本柱。それ以外はPOSIXOS固有のAPIを使います。


12-1. stdio.h(入出力)

#include <stdio.h>

printf("%d\n", 42);
fprintf(stderr, "error: %s\n", msg);
sprintf(buf, "x=%d", n);    // バッファサイズ未指定 → 危険
snprintf(buf, sizeof(buf), "x=%d", n);    // 安全な代替

scanf("%d", &x);
fscanf(file, "%d %d", &a, &b);
sscanf(str, "%d", &x);

FILE *f = fopen("data.txt", "r");
if (f == NULL) { /* エラー */ }
char line[256];
while (fgets(line, sizeof(line), f)) {
    // 処理
}
fclose(f);

// バイナリ
fwrite(buf, sizeof(int), 100, f);
fread(buf, sizeof(int), 100, f);

printfのフォーマット指定子

%d / %i      int
%u           unsigned int
%ld / %lld   long / long long
%zu          size_t
%f / %g / %e double / float
%c           char
%s           char *
%p           void *
%x / %X      16進数
%%           %

%zu はsize_t用。%dsize_t を渡すと実装依存の挙動になります。


12-2. stdlib.h(汎用)

#include <stdlib.h>

malloc / calloc / realloc / free

abs(x)              // 絶対値
atoi("42")          // 文字列 → int(エラー処理がない)
strtol("42", NULL, 10)  // より安全(エラー検出可)
strtod("3.14", NULL)
rand()              // 疑似乱数(質は低い)
srand(seed)
exit(status)        // プログラム終了
abort()             // 異常終了
qsort(arr, n, sz, cmp)
bsearch(key, arr, n, sz, cmp)
getenv("HOME")      // 環境変数
system("ls")        // シェル実行(セキュリティ注意)

12-3. string.h

第8章8-5で詳述。

strlen / strcpy / strncpy / strcat / strcmp / strchr / strstr
memcpy / memset / memcmp / memmove

memmove重なるメモリ領域でも安全にコピー(memcpy は重なるとUB)。


12-4. math.h

#include <math.h>

sqrt(2.0)         // 1.41...
pow(2, 10)        // 1024
exp(1)            // e ≒ 2.718...
log(e)            // 1.0(自然対数)
log10(100)        // 2.0
sin / cos / tan / asin / acos / atan / atan2
floor / ceil / round / trunc
fabs / fmod

リンク時に -lm が必要(GCC):

gcc main.c -lm

12-5. time.h

#include <time.h>

time_t now = time(NULL);
struct tm *t = localtime(&now);
printf("%d-%02d-%02d\n", t->tm_year + 1900, t->tm_mon + 1, t->tm_mday);

clock_t start = clock();
// ... 処理 ...
clock_t end = clock();
double elapsed = (double)(end - start) / CLOCKS_PER_SEC;

tm_year「1900年からのオフセット」tm_mon「0から始まる」など、UNIXの歴史を引きずるAPI。


12-6. errnoとperror

Cの標準的なエラー処理機構。

#include <errno.h>
#include <string.h>
#include <stdio.h>

FILE *f = fopen("nonexistent", "r");
if (f == NULL) {
    perror("fopen");                          // "fopen: No such file or directory"
    fprintf(stderr, "errno=%d: %s\n", errno, strerror(errno));
}

errnoスレッドローカル(C11)なグローバル変数。失敗したAPIが「何が失敗したか」を教えるのに使う。

エラーチェック・イディオム

if (some_call() == -1) {
    perror("some_call");
    return -1;
}

Cには例外がないので、戻り値で成功/失敗を返すのが基本。-1 / NULL / 専用エラーコードを返すパターンが定石。


12-7. このセクションのまとめ

stdio.h:
  printf / scanf / fopen / fgets / fread / fwrite
  snprintfを使う(sprintfは危険)
  size_tは %zu

stdlib.h:
  malloc / free / abs / strtol / qsort / bsearch
  exit / abort / getenv

string.h:
  strlen / strcpy / strcmp / memcpy
  memmoveは重なる領域でも安全

math.h:
  sqrt / pow / sin / cos / log
  リンク時 -lm

time.h:
  time / localtime / clock
  tm_yearは1900オフセット

errno:
  失敗したAPIのエラーコード
  perror / strerrorで表示
  C標準のエラー伝播は戻り値が基本

次のセクションでは、Cのビルドシステムを扱います。


13. ビルドシステム(Make / CMake)

Cの世界では Make が伝統的、CMake が現代的なクロスプラットフォーム選択肢。


13-1. Makefileの基本

CC = gcc
CFLAGS = -Wall -Wextra -O2 -g
LDFLAGS = -lm

SRCS = main.c util.c
OBJS = $(SRCS:.c=.o)
TARGET = myapp

$(TARGET): $(OBJS)
	$(CC) $(LDFLAGS) -o $@ $^

%.o: %.c
	$(CC) $(CFLAGS) -c {{CONTENT}}lt; -o $@

clean:
	rm -f $(OBJS) $(TARGET)

.PHONY: clean

Makeの動き

  • ターゲット: 作りたいファイル($(TARGET))
  • 依存: そのターゲットが依存するファイル($(OBJS))
  • コマンド: 依存が新しければ実行($(CC) ...)
  • タイムスタンプ比較で必要な部分だけ再ビルド

自動変数

$@   ターゲット名
{{CONTENT}}lt;   最初の依存
$^   すべての依存

Makeは強力ですが、Makefileの構文が独特で、タブとスペースの違い・暗黙ルールなど嵌り所が多い。


13-2. CMakeの基本

CMake「Makefile / Visual Studioプロジェクト / Ninjaなどを生成」 するメタビルドツール。クロスプラットフォーム対応で、現代のC/C++ プロジェクトの主流。

# CMakeLists.txt
cmake_minimum_required(VERSION 3.10)
project(myapp C)

set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED ON)

add_executable(myapp main.c util.c)
target_compile_options(myapp PRIVATE -Wall -Wextra -O2 -g)
target_link_libraries(myapp PRIVATE m)
mkdir build && cd build
cmake ..
make            # またはcmake --build .

CMakeの利点

  • クロスプラットフォーム(Linux / Mac / Windows / 組み込み)
  • IDE統合(CLion・VS Code・Visual Studio)
  • 大規模プロジェクト管理

13-3. ライブラリ(静的・動的)

静的ライブラリ(.a、Windows: .lib)

gcc -c util.c -o util.o
ar rcs libutil.a util.o          # アーカイブ作成

gcc main.c -L. -lutil -o app    # リンク

実行ファイルにライブラリのコードが埋め込まれるので、配布が楽。

動的ライブラリ(.so、Windows: .dll、macOS: .dylib)

gcc -fPIC -c util.c -o util.o
gcc -shared util.o -o libutil.so

gcc main.c -L. -lutil -o app
LD_LIBRARY_PATH=. ./app

実行時にロードされる。メモリ効率が良い(複数プロセスで共有)が、バージョン管理が複雑Linuxの “DLL Hell” に相当する問題が起こりえます。


13-4. このセクションのまとめ

Make:
  伝統的、Unix/Linuxで標準
  ターゲット/依存/コマンド + タイムスタンプ
  Makefileの構文は独特(タブが必須)

CMake:
  メタビルド(Make/VS/Ninjaを生成)
  クロスプラットフォーム
  現代のC/C++ プロジェクトの主流

ライブラリ:
  .a静的(埋め込み、配布が楽)
  .so / .dll動的(共有、バージョン管理が複雑)

次のセクションでは、Cの並行処理を扱います。


14. 並行処理(pthreads / C11 threads)

Cには長らく標準スレッドAPIがなく、pthread(POSIX) がデファクトでした。C11<threads.h> が標準化されたが、Microsoftが対応していないなど普及度はまだpthreadに劣ります。


14-1. POSIX threads(pthread)

#include <pthread.h>

void *worker(void *arg) {
    int n = *(int*)arg;
    printf("worker %d\n", n);
    return NULL;
}

int main() {
    pthread_t threads[4];
    int args[4];

    for (int i = 0; i < 4; i++) {
        args[i] = i;
        pthread_create(&threads[i], NULL, worker, &args[i]);
    }

    for (int i = 0; i < 4; i++) {
        pthread_join(threads[i], NULL);
    }
}

リンク時に -lpthread が必要:

gcc main.c -pthread

14-2. Mutexとcondition variable

Mutex

pthread_mutex_t lock = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;

void *increment(void *arg) {
    for (int i = 0; i < 1000000; i++) {
        pthread_mutex_lock(&lock);
        counter++;
        pthread_mutex_unlock(&lock);
    }
    return NULL;
}

Condition variable

pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t  cond = PTHREAD_COND_INITIALIZER;
int ready = 0;

void *consumer(void *arg) {
    pthread_mutex_lock(&mtx);
    while (!ready) pthread_cond_wait(&cond, &mtx);
    // readyが立つまで待つ
    pthread_mutex_unlock(&mtx);
    return NULL;
}

void *producer(void *arg) {
    pthread_mutex_lock(&mtx);
    ready = 1;
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mtx);
    return NULL;
}

while で待つ」のが重要(spurious wakeup対策)。if だと偽の起床で誤動作する。


14-3. C11標準スレッド

#include <threads.h>

int worker(void *arg) {
    printf("hi\n");
    return 0;
}

thrd_t t;
thrd_create(&t, worker, NULL);
thrd_join(t, NULL);

mtx_t mtx;
mtx_init(&mtx, mtx_plain);
mtx_lock(&mtx);
mtx_unlock(&mtx);

APIはpthreadに近いが、Windows MSVCが対応していないので普及していません。クロスプラットフォーム重視ならpthreadかC++ の std::thread(C++ 11+)。


14-4. アトミック操作(C11 _Atomic)

ロックフリーな並行制御の基盤。

#include <stdatomic.h>

atomic_int counter = 0;

void increment(void) {
    atomic_fetch_add(&counter, 1);
}

int read(void) {
    return atomic_load(&counter);
}

atomic_fetch_add などはメモリオーダ指定もできる:

atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed);

ロックフリーアルゴリズムの基礎ですが、メモリオーダの理解は難しい領域です。


14-5. このセクションのまとめ

pthread:
  POSIX標準、Linux/Mac/Unixで広く使われる
  pthread_create / join / mutex / cond
  -pthreadでリンク

C11 threads.h:
  標準化されたがMicrosoft未対応
  pthread互換的なAPI

Mutexとcondition:
  共有データ保護
  cond_waitはwhileで(spurious wakeup)

stdatomic.h:
  ロックフリー操作の基盤
  atomic_int / atomic_fetch_add
  メモリオーダで詳細制御

次のセクションでは、Cで最も重要な概念――未定義動作(UB)を扱います。


15. 未定義動作(Undefined Behavior)

未定義動作(UB) はCの最も恐ろしい概念。「プログラムの挙動が規格上完全に未定義」になる状況で、コンパイラが予想外の最適化を行う元凶になります。


15-1. UBとは何か

C規格では3種類の動作不確定が定義されています。

implementation-defined behavior実装が決める(処理系依存)。例: intのサイズ
unspecified behavior実装次第だが何かは起こる(具体的選択は不問)
undefined behavior (UB)           動作が完全に未定義 ─ 何が起きてもおかしくない

UBが起こると、コンパイラは 「UBは起こらないと仮定して最適化」 します。これが本番コードでは想定外の挙動を生む原因。

UBを含むプログラムは、悪魔を鼻から飛び出させても規格に違反しない」(C/C++ 業界の有名な格言)


15-2. よくあるUB

1. NULLポインタの参照外し

int *p = NULL;
*p = 5;    // UB

2. 配列の境界外アクセス

int arr[5];
arr[10] = 0;    // UB

3. 解放後のアクセス(use-after-free)

int *p = malloc(sizeof(int));
free(p);
*p = 5;    // UB

4. 符号付き整数オーバーフロー

int x = INT_MAX;
x++;    // UB!(符号なしはUBではなくwrap around)

5. 未初期化変数の使用

int x;
printf("%d\n", x);    // UB

6. 同じ式内で同じ変数を複数回変更

int i = 0;
i = i++ + ++i;    // UB

7. strict aliasing違反

float f = 1.0;
int *p = (int*)&f;
int x = *p;    // UB(型を経由した別解釈)

「型をまたいだメモリ解釈」は安全に行うには memcpy を使う:

int x;
memcpy(&x, &f, sizeof(x));    // OK

8. 整数除算で0で割る

int x = 1 / 0;    // UB

15-3. UBが招く最適化の罠

コンパイラはUBを「起こらない」と仮定するので、奇妙な最適化が起こります。

void example(int *p) {
    int x = *p;        // pはNULLではないと仮定された
    if (p == NULL) {   // 上で参照外したからp != NULL
        // このif分岐は最適化で削除される!
        return;
    }
    use(x);
}

このため、**「ローカルで動いた」が「最適化版でクラッシュ」**という現象が起こります。

「動くからOK」ではない

UBを含むコードが「動いてしまう」ことは多いですが、

  • コンパイラ更新で動かなくなる
  • 最適化レベルで挙動が変わる
  • 未来の自分や他人を破滅させる

ので、UBは 必ず修正するのが鉄則。


15-4. 検出ツール

UndefinedBehaviorSanitizer(UBSan)

gcc -fsanitize=undefined main.c
./a.out
# UBがあるとランタイムにレポート

AddressSanitizer(ASan)

メモリ系のUB(境界外、UAF、リーク)を検出:

gcc -fsanitize=address main.c

Static analyzer

clang --analyze main.c     # Clang Static Analyzer
cppcheck main.c            # オープンソースの静的解析

ASan + UBSanを常時オン」が現代C開発の標準。


15-5. このセクションのまとめ

UBとは:
  動作が完全に未定義 ─ 何でも起こりうる
  コンパイラは「UBは起こらない」と仮定して最適化

よくあるUB:
  NULL参照外し / 境界外アクセス / UAF
  符号付きオーバーフロー / 未初期化変数
  同式内での同変数の複数変更
  strict aliasing違反
  0除算

最適化の罠:
  「動いたコード」がUBを含むなら危険
  コンパイラ・最適化レベルで挙動変化

検出:
  -fsanitize=address / undefined / thread
  cppcheck / clang static analyzer
  全C開発者の必携

次のセクションでは、モダンCのテクニックを扱います。


16. モダンCのテクニック

古いK&R C」と「現代のC99/C11/C23」では、コードの書き方が大きく違います。モダンCのテクニックを整理します。


16-1. C99の革新(VLA、designated initializers)

指定初期化子

struct Point p = {.x = 1, .y = 2};

int arr[10] = {[3] = 100, [5] = 200};   // 指定インデックス初期化

複合リテラル

struct Point *p = &(struct Point){.x = 1, .y = 2};

print_point((struct Point){10, 20});    // 一時的な値を渡す

これにより 「中間変数を作らずにstructを渡せる」というC++ 的な書き方が可能に。

可変長配列(VLA)

void process(size_t n) {
    int arr[n];   // 実行時にサイズが決まる配列
}

C99の革新でしたが、C11optional化(実装が必須でなくなった)。Microsoft Visual C++ は対応しないため、移植性のために避ける流れ。


16-2. C11の機能(_Generic、anonymous struct)

_Generic(型ジェネリック)

引数の型に応じて違う関数を呼び分ける。**マクロで「型多重化」**ができる。

#define abs_g(x) _Generic((x), \
    int:    abs,                \
    long:   labs,                \
    double: fabs                 \
)(x)

abs_g(-5);          // abs(-5)
abs_g(-5L);         // labs(-5L)
abs_g(-5.0);        // fabs(-5.0)

C++ のテンプレートに比べると貧弱ですが、<tgmath.h>(型ジェネリック数学)で標準ライブラリも活用しています。

anonymous struct/union

struct Outer {
    int common;
    union {
        int   as_int;
        float as_float;
    };    // 名前なし、外から直接アクセス可能
};

struct Outer o;
o.as_int = 42;    // 直接アクセス

_Static_assert(C11)

_Static_assert(sizeof(int) == 4, "int must be 4 bytes");

コンパイル時のアサーション。サイズや定数を検証。C23では static_assert キーワードに。


16-3. C23の新機能

C23(2024)33年ぶりの大型改訂。主な追加:

// true / falseがキーワード化(stdbool.h不要)
bool flag = true;

// nullptr(NULLの型安全な代替)
void *p = nullptr;

// auto型推論(C++ 風)
auto x = 42;          // int

// constexpr
constexpr int MAX = 100;

// 2進リテラル
int x = 0b1010;

// typeof / typeof_unqual
typeof(x) y = 5;

// 属性(C++ 風)
[[nodiscard]] int parse(const char *s);
[[deprecated("use new_func")]] void old_func(void);
[[fallthrough]];
[[maybe_unused]];

// 任意桁の整数(_BitInt)
_BitInt(128) huge = 0;

C++ への歩み寄りが進んだ印象。GCC 13+ / Clang 16+ で部分的にサポート。


16-4. 安全なコーディング指針

1. -Wall -Wextra -Wpedanticで警告を最大限に
2. -Werrorで警告をエラー扱い
3. ASan / UBSanをCIで常時実行
4. snprintfを使う、strcpy/sprintfは使わない
5. static / constを積極的に
6. 小さな関数に分割(再利用可能性 + テスト容易性)
7. 入力チェックを早期に(assertやバリデーション)
8. 戻り値で成功/失敗を必ず返す(void戻り値は副作用専用)
9. mallocの戻り値を必ずチェック
10. 関数のドキュメント(所有権、エラー条件)を書く

例: 安全な書き方

typedef struct {
    char *data;
    size_t len;
    size_t cap;
} Buffer;

// out引数で結果を返し、戻り値でエラーを示す
int buffer_create(Buffer *out, size_t initial_cap) {
    if (out == NULL) return -1;
    out->data = malloc(initial_cap);
    if (out->data == NULL) return -1;
    out->len = 0;
    out->cap = initial_cap;
    return 0;
}

void buffer_destroy(Buffer *buf) {
    if (buf == NULL) return;
    free(buf->data);
    buf->data = NULL;
    buf->len = buf->cap = 0;
}

16-5. このセクションのまとめ

C99:
  指定初期化子 .field = value
  複合リテラル (struct T){...}
  VLA(C11でoptional化)

C11:
  _Genericで型ジェネリック
  匿名struct/union
  _Static_assert(C23でstatic_assert)
  threads.h, stdatomic.h

C23:
  true / false / nullptr / auto / constexpr
  2進リテラル、typeof
  [[nodiscard]] [[deprecated]] 属性
  _BitInt(N)

指針:
  -Wall -Wextra -Werror
  ASan / UBSanをCIに
  snprintf / strncpy
  関数の所有権をドキュメント化

次のセクションでは、テストとデバッグを扱います。


17. テストとデバッグ

CにはJavaやRubyのような豊かなテストエコシステムは無いものの、UnityCheck などの軽量フレームワーク、gdb・valgrind といった伝統的ツールがあります。


17-1. assert.h

最もシンプルなテスト・契約検証。

#include <assert.h>

void process(int *arr, size_t n) {
    assert(arr != NULL);
    assert(n > 0);
    // 処理
}

NDEBUG マクロが定義されているとアサーションが消える(gcc -DNDEBUG)。本番ビルドでは無効化するのが慣習。

static_assert(C11+)

_Static_assert(sizeof(int) >= 4, "int too small");

コンパイル時アサーション。


17-2. 単体テストフレームワーク(Unity, Check)

Unity(軽量)

#include "unity.h"

void test_add(void) {
    TEST_ASSERT_EQUAL_INT(3, 1 + 2);
    TEST_ASSERT_NOT_NULL("hello");
}

int main(void) {
    UNITY_BEGIN();
    RUN_TEST(test_add);
    return UNITY_END();
}

ヘッダのみで使える軽量さ、組み込み開発にも人気。

Check(多機能)

シグナルハンドリングや並列実行などをサポート。Linuxサーバ向けプロジェクトでよく使われます。


17-3. gdb / lldb

C開発の伝統的デバッガ。Linuxならgdb、macOSならlldbが標準。

gcc -g main.c -o main
gdb ./main

(gdb) break main
(gdb) run
(gdb) next         # 次の行へ
(gdb) step         # 関数内に入る
(gdb) print x      # 変数を表示
(gdb) backtrace    # スタックトレース
(gdb) continue
(gdb) quit

-g でデバッグ情報を埋め込むのを忘れずに。


17-4. valgrind / ASan / UBSan

Valgrind(伝統的・高機能)

valgrind --leak-check=full --show-leak-kinds=all ./main

メモリリーク・UAF・境界外アクセスを検出。遅い(実行時間が10倍以上になる)が、検出能力は最強クラス。

AddressSanitizer(高速・モダン)

gcc -fsanitize=address -g main.c
./a.out

実行時間オーバーヘッドが2倍程度と高速で、CIでの常時利用に向く。

UBSan

gcc -fsanitize=undefined -g main.c

UB(符号付きオーバーフロー、NULL参照外し)を検出。


17-5. このセクションのまとめ

assert:
  実行時アサーション、NDEBUGで無効化
  静的なら _Static_assert / static_assert(C23)

単体テスト:
  Unity(軽量)、Check(多機能)
  CMocka(モック対応)も人気

デバッガ:
  gdb(Linux)、lldb(macOS)
  -gでデバッグ情報を埋め込む

メモリチェック:
  valgrind(最強・遅い)
  ASan / UBSan(高速・CI常用)
  CIで必ず動かす

次のセクションでは、Cのパフォーマンスチューニングを扱います。


18. パフォーマンス

Cは本来高速ですが、キャッシュとメモリ局所性」「SIMD」「インライン化」などの最適化を意識すると、さらに桁違いの差が出ます。


18-1. プロファイリング(perf, gprof)

perf(Linux)

perf record ./main
perf report

CPU使用率・キャッシュミス・分岐予測ミスなど詳細に取れる。Linuxカーネルが提供する強力なツール。

gprof

gcc -pg main.c -o main
./main             # gmon.out生成
gprof main gmon.out

伝統的な関数レベルプロファイラ。

Linux Flamegraph

perf record -g ./main
perf script | ./stackcollapse-perf.pl | ./flamegraph.pl > flame.svg

ホットパスを視覚的に分析。


18-2. キャッシュとメモリ局所性

現代CPUは L1/L2/L3キャッシュを持ちます。キャッシュヒットは1ns、メモリアクセスは100ns以上。キャッシュフレンドリなコード10倍以上速いこともあります。

// Bad: 列優先アクセス(キャッシュミス多発)
for (int j = 0; j < N; j++)
    for (int i = 0; i < N; i++)
        sum += matrix[i][j];

// Good: 行優先アクセス(キャッシュヒット)
for (int i = 0; i < N; i++)
    for (int j = 0; j < N; j++)
        sum += matrix[i][j];

Cの多次元配列は 行優先で配置されているので、内側のループで列を進めるとキャッシュ局所性が良くなります。

Struct of Arrays(SoA)

// Array of Structs (AoS)
struct Particle {
    float x, y, z;
    float vx, vy, vz;
} particles[N];

// Struct of Arrays (SoA)
struct Particles {
    float x[N], y[N], z[N];
    float vx[N], vy[N], vz[N];
};

全粒子のxだけを処理」する場合、SoAのほうがキャッシュ効率が良い。ゲームエンジンや科学計算で重要なテクニック。


18-3. インライン化・restrict・SIMD

inlineと attribute((always_inline))

static inline int square(int x) { return x * x; }
__attribute__((always_inline)) inline int cube(int x) { return x * x * x; }

ホットな小関数をインライン化すると関数呼び出しオーバーヘッドが消える。

restrict

この2つのポインタは指す先が重ならない」とコンパイラに伝えるキーワード(C99+)。

void copy(int *restrict dst, const int *restrict src, size_t n) {
    for (size_t i = 0; i < n; i++) dst[i] = src[i];
}

これにより、コンパイラはより積極的な最適化(ベクトル化など)ができる。

SIMD

#include <immintrin.h>    // x86 AVX

void add_vec(float *a, float *b, float *c, size_t n) {
    for (size_t i = 0; i < n; i += 8) {
        __m256 va = _mm256_loadu_ps(a + i);
        __m256 vb = _mm256_loadu_ps(b + i);
        __m256 vc = _mm256_add_ps(va, vb);
        _mm256_storeu_ps(c + i, vc);
    }
}

CPUの SIMD命令(AVX、NEON)を直接使うと、ループが数倍速くなることがあります。コンパイラの自動ベクトル化(-O3 -march=native)でも効くことが多い。


18-4. このセクションのまとめ

プロファイリング:
  perf record / report(Linux)
  gprof(古典的)
  flamegraphで視覚化

キャッシュ:
  L1: 1ns、メモリ: 100ns以上
  行優先アクセスで局所性を高める
  SoA vs AoSの使い分け

最適化テクニック:
  inlineで関数呼び出し排除
  restrictで重なり保証 → 自動ベクトル化
  SIMD(intrinsics)で並列化
  -O3 -march=nativeでコンパイラ最適化フル

「測ってから最適化」が鉄則

次はFAQ、図解、ロードマップ、用語集をまとめて。


19. よくある落とし穴FAQ

実務で頻発するCの落とし穴を一問一答で。


Q1. int のサイズはいつでも4バイト?

違う。最低2バイトとしか規格にない。stdint.hint32_t 等を使うのが移植性のある書き方。

Q2. char はsigned? unsigned?

処理系依存。明確にしたいなら signed charunsigned char を使う。

Q3. printf("%d", ptr) がクラッシュする

%d はint用。ポインタは %p。32bit / 64bitでサイズが違うので必須。

Q4. if (a = 5) が意外な動きをする

代入と等価が混同された典型ミス。== を使うか、5 == a のように定数を左に置く(Yoda記法)。

Q5. strcpy(dst, src) でクラッシュ

dstのサイズチェックなし。snprintf(dst, sizeof(dst), "%s", src) を使う。

Q6. 配列のサイズを関数内で取れない

関数引数では配列がポインタに減衰する。サイズも別途渡すのがCのお作法。

Q7. malloc(0) の戻り値は?

NULLかもしれないし、解放可能な何かが返るかもしれない(実装依存)。0 でmallocしないのが安全。

Q8. free(NULL) は?

C規格上 安全。何もせず返る。

Q9. 整数オーバーフローを安全に検出したい

-fsanitize=undefined で実行時検出。または __builtin_add_overflow などの組み込み関数(GCC/Clang)。

Q10. static をいくつもの意味で使う?

3つ:

  1. ファイルスコープに限定(外部リンケージなし)
  2. 関数内ローカル変数の値を保持
  3. C99+ 配列引数で「最低N要素ある」を表明(int arr[static 10])

Q11. ヘッダに関数の実装を書いていい?

static inline ならOK。それ以外は 多重定義でリンクエラーになる。

Q12. void* から戻すときキャストは必要?

Cでは 暗黙キャストOK。C++ では明示キャスト必須。

Q13. i++++i、ループでどちらが速い?

最適化が効く現代では同じ。読みやすさで好きな方を。

Q14. なぜgotoが許されるのか

Cには try/finally がないので、エラークリーンアップを集約するイディオムとして必要。Linuxカーネルでも多用。

Q15. #defineenum どちらで定数を定義?

enum 推奨(型情報・スコープ・デバッガで見える)。例外: 浮動小数点定数は #defineconst double

Q16. ヘッダでマクロを再定義してしまった

#ifndef HEADER_H ... #define HEADER_H ... #endif のインクルードガードを忘れた可能性。

Q17. printf のバッファがフラッシュされない

stdoutはラインバッファ(端末)または完全バッファ(パイプ)。fflush(stdout) または setvbuf で挙動を制御。

Q18. リンク時 undefined reference to ... が出る

呼んでいる関数の実装が含まれていない。.c を追加するか、ライブラリを -lname で指定。

Q19. 構造体を == で比較できない

Cにはオーバーロードがないので不可。メンバごとに比較する関数を書くか、memcmp(パディングに注意)。

Q20. K&R CとANSI Cの違いは?

主に関数プロトタイプ宣言の必須化、void の追加、型変換の整理。古いコードを現代化するなら (void) 引数や宣言のチェックが必要。


20. 図解: メモリレイアウト・ポインタ・スタックフレーム


20-1. プロセスメモリレイアウト

高アドレス
┌──────────────┐
│   Stack      │  ← ローカル変数、関数呼び出し履歴
│   ↓          │
│              │
│              │
│   ↑          │
│   Heap       │  ← malloc / freeで管理
├──────────────┤
│   BSS        │  ← 未初期化グローバル(0で初期化される)
│   Data       │  ← 初期化済みグローバル
│   Text(Code) │  ← 機械語、文字列リテラル
└──────────────┘
低アドレス

  StackとHeapが伸びて衝突するとオーバーフロー

20-2. ポインタの図解

int x = 42;
int *p = &x;

メモリ:
┌─────────┐  アドレス0x1000
│   42    │  ← x
└─────────┘

┌─────────┐  アドレス0x2000
│  0x1000 │  ← p(xのアドレスを保持)
└─────────┘

*p = xの値(42)にアクセス
&p = pのアドレス(0x2000)

20-3. スタックフレーム

関数呼び出し:
  main → caller → callee

スタック(高アドレス → 低アドレス):

┌──────────────┐ 高アドレス
│  mainの引数  │
├──────────────┤
│  main戻り先  │
├──────────────┤
│  mainの変数  │
├──────────────┤
│ caller引数   │
├──────────────┤
│ caller戻り先 │
├──────────────┤
│ caller変数   │
├──────────────┤
│ callee引数   │
├──────────────┤
│ callee戻り先 │
├──────────────┤
│ callee変数   │← 現在のスタックポインタ
└──────────────┘ 低アドレス

calleeがリターンするとフレームがポップ
スタックが深すぎるとオーバーフロー

20-4. 配列とポインタ

int arr[5] = {10, 20, 30, 40, 50};

メモリ:
┌────┬────┬────┬────┬────┐
│ 10 │ 20 │ 30 │ 40 │ 50 │
└────┴────┴────┴────┴────┘
 0    4    8   12   16    (バイトオフセット)

int *p = arr;     // pはarr[0] のアドレス
p + 1             // arr[1] のアドレス(4 byte進む)
p[2]              // *(p + 2) と同じ → 30

20-5. ヒープとmalloc

malloc(100) を呼ぶと:

┌────────────────────┐
│ ヘッダ(サイズ等)  │  ← mallocが管理する
├────────────────────┤
│ ユーザに返される   │  ← この先頭アドレスがポインタとして返る
│ 100 byte           │
│                     │
└────────────────────┘

free時、ヘッダ情報を見てサイズを決めて解放
ヘッダを破壊するとfreeが壊れる(バッファオーバーフロー攻撃)

21. 学習ロードマップ(30日)


Week 1(基礎)

  • Day 1: gcc / clang環境、Hello World、Makefileの基本
  • Day 2: 整数型、stdint.h、暗黙の型変換
  • Day 3: 演算子、副作用の評価順序
  • Day 4: 制御フロー(if、for、switch)
  • Day 5: 関数、プロトタイプ、static / extern
  • Day 6: ポインタの基礎(&、*、ポインタ算術)
  • Day 7: 復習、簡単なプログラム

Week 2(Cのコア)

  • Day 8: 配列とポインタの関係、配列の減衰
  • Day 9: C文字列、string.h、snprintf
  • Day 10: struct、typedef、PIMPL
  • Day 11: union、enum、tagged union
  • Day 12: malloc / free、リーク回避
  • Day 13: ASan / valgrindを試す
  • Day 14: 動的データ構造(連結リスト)を実装

Week 3(実装)

  • Day 15: プリプロセッサ、マクロの落とし穴
  • Day 16: ファイルI/O(fopen / fread / fwrite)
  • Day 17: errno、エラーハンドリング戦略
  • Day 18: 関数ポインタ、qsort
  • Day 19: ビット演算、フラグ集合
  • Day 20: 自分のMakefile / CMake
  • Day 21: 復習、中規模プログラム

Week 4(モダンと並行)

  • Day 22: pthreadMutex、条件変数
  • Day 23: アトミック操作(_Atomic)
  • Day 24: UBを意識的に書いてUBSanで検出
  • Day 25: gdbの使い方
  • Day 26: perfでプロファイリング
  • Day 27: SIMDと最適化
  • Day 28: C99 / C11 / C23の機能
  • Day 29: 既存OSSのソースを読む(curl、SQLite、redis)
  • Day 30: 振り返り、自分のライブラリを作る

22. 用語集


あ行

  • アライメント: 型サイズの倍数アドレスへの配置要件
  • 暗黙の型変換: 演算時の自動型昇格

か行

  • 可変長配列(VLA): 実行時にサイズが決まる配列(C99C11でoptional)
  • 関数ポインタ: 関数のアドレスを保持する変数
  • キャスト: 型変換 (type)expr
  • 共用体(union): 複数メンバが同一メモリを共有
  • クリーンアップ: 関数末尾でリソース解放するパターン
  • 構造体(struct): 複数メンバを持つ複合型

さ行

た行

  • ダングリングポインタ: 解放済みメモリを指すポインタ
  • 定義: 実体(メモリや関数本体)を作る
  • 動的メモリ: malloc / freeで管理されるヒープ領域
  • 動的リンク: 実行時にライブラリ(.so / .dll)をロード

な行

  • nullポインタ: 何も指さないポインタ(NULL / nullptr)

は行

  • バッファオーバーフロー: 配列境界を越える書き込み(攻撃の温床)
  • ビットフィールド: ビット単位のメンバ
  • 複合リテラル: (struct T){...} で一時struct
  • プリプロセッサ: #include#define などを処理する段
  • ヘッダ: 宣言を集めたファイル(.h)
  • ポインタ: アドレス + 型情報を持つ変数

ま行

  • マクロ: テキスト置換による疑似関数・定数
  • 未定義動作(UB): 規格上挙動が定義されない状態
  • メモリリーク: 解放されないままのメモリ

ら行

  • リンカ: 複数の .oを結合して実行ファイルを作る
  • リンクエラー: シンボル未解決エラー

A〜Z


発展: メモリと実行モデル

ここからはCの各機能をさらに深く掘り下げます。実プロジェクトで遭遇するパターン、UBの罠、性能・安全性のテクニックを集約。


23. メモリモデルの詳細

23-1. プロセスのメモリ空間

高アドレス
┌───────────────────────────┐
│ 環境変数・引数            │
├───────────────────────────┤
│ Stack ↓                   │  ローカル変数、関数呼び出し
│                           │  数MBが上限
├───────────────────────────┤
│ ↑ 共有ライブラリ          │  mmap領域
├───────────────────────────┤
│                           │
│ Heap ↑                    │  malloc / brk
│                           │
├───────────────────────────┤
│ BSS                       │  未初期化グローバル(0で初期化)
├───────────────────────────┤
│ Data                      │  初期化済みグローバル
├───────────────────────────┤
│ Text (Code)               │  実行可能、読み取り専用
├───────────────────────────┤
│ ヘッダ等                  │
└───────────────────────────┘
低アドレス

/proc/<pid>/maps で実際のレイアウトが見える(Linux)。


23-2. mallocの実装

void *malloc(size_t size) {
    // 概念的な実装
    // 1. Freeリストを探す
    // 2. 適合するブロックがあれば返す
    // 3. なければbrk() / sbrk() / mmap() でOSから取得
    // 4. ヘッダ(サイズ等)を付けて返す
}

実装は glibcのカスタムアロケータ(ptmalloc2)、jemalloc、tcmallocなど。各々得意分野が違う:

ptmalloc2:  glibc標準
jemalloc:   FreeBSD、Redis、Firefox等。フラグメンテーション抑制
tcmalloc:   Google製。スレッドキャッシュで高速
mimalloc:   Microsoft製。シンプル + 高速

LD_PRELOADで差し替え可能:

LD_PRELOAD=/usr/lib/libtcmalloc.so ./myapp

23-3. アライメントのコスト

struct Bad {
    char a;     // 1
    int b;      // 4
    char c;     // 1
};
sizeof(struct Bad);    // 12

struct Good {
    int b;      // 4
    char a;     // 1
    char c;     // 1
};
sizeof(struct Good);   // 8

サイズ大きい順にフィールドを並べる。組み込みでは劇的にメモリ削減。

packed構造体

struct __attribute__((packed)) Network {
    uint8_t type;
    uint32_t value;    // パディングなしで詰める
};

ファイル形式・ネットワークプロトコルで重要。性能はやや劣化(アライメント違反のロード)。


23-4. mmapの活用

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

int fd = open("huge.dat", O_RDONLY);
struct stat st;
fstat(fd, &st);
void *map = mmap(NULL, st.st_size, PROT_READ, MAP_PRIVATE, fd, 0);

// ファイル内容に直接アクセス
const char *data = (const char *)map;
process(data, st.st_size);

munmap(map, st.st_size);
close(fd);

巨大ファイルを 仮想メモリ越しに読める。fread より速いことが多い。


23-5. スタックオーバーフロー

void recurse(int n) {
    char buf[1024 * 1024];   // 1MBのスタック確保
    if (n > 0) recurse(n - 1);
}
recurse(10);   // スタック10MB → クラッシュ

スタックは数MBが限界。深い再帰や巨大ローカル配列でクラッシュ。

検出: ulimit -s でスタックサイズ確認。-fstack-protector で保護。


23-6. このセクションのまとめ

- プロセスメモリ: Text / Data / BSS / Heap / Stack
- malloc実装はjemalloc / tcmallocも選択肢
- structはサイズ大きい順
- packedでパディング除去
- mmapで巨大ファイル
- スタック制限に注意

24. ポインタの徹底活用

24-1. 関数ポインタの応用

typedef int (*compare_fn)(const void *, const void *);

void sort(void *arr, size_t n, size_t size, compare_fn cmp);

// 使用
int int_cmp(const void *a, const void *b) {
    return *(const int *)a - *(const int *)b;
}

int arr[] = {3, 1, 4, 1, 5};
qsort(arr, 5, sizeof(int), int_cmp);

Cの 唯一のジェネリクス代替が関数ポインタ + void*。qsortbsearch のAPI設計の原型。


24-2. コールバック登録

typedef void (*event_handler)(int event, void *userdata);

struct event_loop {
    event_handler handler;
    void *userdata;
};

void register_handler(struct event_loop *el, event_handler h, void *data) {
    el->handler = h;
    el->userdata = data;
}

userdataクロージャ的な状態を渡せる。GLib・GTKの典型パターン。


24-3. ポインタの型変換とエンディアン

union {
    uint32_t i;
    uint8_t  bytes[4];
} u;
u.i = 0x12345678;

// little-endianならbytes[0] = 0x78
// big-endianならbytes[0] = 0x12

ネットワークプロトコル・バイナリファイル処理で頻出。htonl/ntohlエンディアン変換。


24-4. 関数ポインタテーブル(vtable)

struct shape_vtable {
    double (*area)(const void *);
    void (*draw)(const void *);
};

struct shape {
    const struct shape_vtable *vtable;
};

// 円
struct circle {
    struct shape base;
    double radius;
};

double circle_area(const void *self) {
    return 3.14 * ((const struct circle *)self)->radius * ((const struct circle *)self)->radius;
}

static const struct shape_vtable circle_vtable = {
    .area = circle_area,
    .draw = circle_draw,
};

// 多態的呼び出し
struct shape *s = ...;
double a = s->vtable->area(s);

Cで OOPのvirtual function を実装する古典的パターン。Linuxカーネル・GTKで多用。


24-5. このセクションのまとめ

- 関数ポインタ + void* で「ジェネリクス」
- userdataでクロージャ的状態
- エンディアン変換: htonl/ntohl
- vtableで多態性(OOPの自前実装)

25. Cの文字列処理深掘り

25-1. C文字列の弱さ

char buf[10];
strcpy(buf, "this is too long");   // バッファ越え!UB

C文字列は 「ヌル終端 + バイト列」で、長さを持たない。これが多くの脆弱性の温床。

安全な代替

strncpy(buf, src, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';     // 終端を保証

// より安全(長さも返る)
size_t n = snprintf(buf, sizeof(buf), "%s", src);
if (n >= sizeof(buf)) {
    // 切り詰められた
}

gets() は廃止(C11で削除)。strncpy も罠(終端を付けない)。snprintf が現代の推奨


25-2. 文字列の動的生成

char *create_message(const char *name, int age) {
    int len = snprintf(NULL, 0, "%s is %d", name, age);   // サイズだけ計算
    char *buf = malloc(len + 1);
    if (!buf) return NULL;
    snprintf(buf, len + 1, "%s is %d", name, age);
    return buf;
}

char *msg = create_message("Alice", 30);
free(msg);

snprintf(NULL, 0, ...)必要なサイズを取得するイディオム。


25-3. 文字列ライブラリ

標準(限られる):
  strlen / strcpy / strncpy / strcat / strncat
  strcmp / strncmp / strchr / strrchr / strstr
  strdup(POSIX、C23で標準化)
  snprintf / vsnprintf

外部ライブラリ:
  GLib (gchar*, GString)
  bstrlib(Better String Library)
  SDS(Simple Dynamic Strings、Redis製)
  sdsで文字列操作が安全に

25-4. UTF-8処理

Cの標準ライブラリは 基本的にバイト列しか扱わない。UTF-8を意識した処理が必要:

// 文字数(コードポイント数)
size_t utf8_count(const char *s) {
    size_t count = 0;
    while (*s) {
        if ((*s & 0xC0) != 0x80) count++;   // 継続バイトでない
        s++;
    }
    return count;
}

ICU(International Components for Unicode)ライブラリが本格的なUnicode対応。


25-5. このセクションのまとめ

- C文字列は脆い、snprintf推奨
- getsは廃止(C11)
- 必要サイズはsnprintf(NULL, 0, ...) で取得
- 外部ライブラリ(SDS、GLib)も選択肢
- UTF-8は自前で扱うかICU

26. UB(未定義動作)の実例

26-1. 符号付き整数オーバーフロー

int x = INT_MAX;
x = x + 1;    // UB!

これが コンパイラの最適化を惑わせる:

int safe(int x) {
    if (x + 100 < x) return -1;    // オーバーフロー検出?
    return x + 100;
}

GCC/Clangは「符号付きオーバーフローはUBだから起きない」と仮定し、if (x + 100 < x)削除することがある!

対策

// __builtin_add_overflow(GCC/Clang)
int result;
if (__builtin_add_overflow(x, 100, &result)) {
    return -1;    // オーバーフロー
}

// signed → unsignedで計算(unsignedはwrap aroundでUBではない)
unsigned int sum = (unsigned int)x + 100;

26-2. strict aliasing違反

float f = 1.0f;
int *p = (int *)&f;
int x = *p;    // UB! 異なる型で同じメモリを解釈

正しくは:

int x;
memcpy(&x, &f, sizeof(x));    // 型を超えるメモリ操作はmemcpyで

または共用体:

union {
    float f;
    int i;
} u = { .f = 1.0f };
int x = u.i;    // OK(共用体経由は許される)

26-3. スタックポインタを返す

int *bad(void) {
    int x = 42;
    return &x;    // UB! 関数を抜けるとxは無効
}

int *p = bad();
*p;    // UB

頻出のバグ。ローカル変数のアドレスを返さないこと。


26-4. ヌルポインタ参照外し

int *p = NULL;
*p = 5;    // UB(多くの場合SIGSEGV)

26-5. 0除算

int x = 10;
int y = 0;
int z = x / y;    // UB(整数)

float の場合はIEEE 754で inf / nan


26-6. UB検出ツール

# UBSan
gcc -fsanitize=undefined main.c
./a.out
# 実行時にレポート

# 静的解析
clang --analyze main.c
cppcheck main.c
clang-tidy main.c

# Valgrind(実行時メモリ系)
valgrind ./a.out

CIで常時実行することで多くのバグを早期発見。


26-7. このセクションのまとめ

- 符号付き整数オーバーフロー → __builtin_add_overflow
- strict aliasing → memcpy / union
- ローカル変数のアドレスを返さない
- 0除算 / NULL参照外し
- ASan / UBSan / valgrind / cppcheckで検出

27. 並行処理の正しい書き方

27-1. pthread詳細

#include <pthread.h>

void *worker(void *arg) {
    int id = *(int *)arg;
    printf("thread %d\n", id);
    return NULL;
}

pthread_t threads[4];
int ids[4] = {0, 1, 2, 3};

for (int i = 0; i < 4; i++) {
    pthread_create(&threads[i], NULL, worker, &ids[i]);
}
for (int i = 0; i < 4; i++) {
    pthread_join(threads[i], NULL);
}

リンク: gcc main.c -pthread


27-2. Mutex

pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
int counter = 0;

void *increment(void *arg) {
    for (int i = 0; i < 10000; i++) {
        pthread_mutex_lock(&mtx);
        counter++;
        pthread_mutex_unlock(&mtx);
    }
    return NULL;
}

pthread_mutexattr_t で詳細設定(再帰、PI、エラーチェック)。


27-3. Condition Variable

pthread_mutex_t mtx = PTHREAD_MUTEX_INITIALIZER;
pthread_cond_t cond = PTHREAD_COND_INITIALIZER;
int ready = 0;

void *consumer(void *arg) {
    pthread_mutex_lock(&mtx);
    while (!ready) {
        pthread_cond_wait(&cond, &mtx);
    }
    // 処理
    pthread_mutex_unlock(&mtx);
    return NULL;
}

void *producer(void *arg) {
    pthread_mutex_lock(&mtx);
    ready = 1;
    pthread_cond_signal(&cond);
    pthread_mutex_unlock(&mtx);
    return NULL;
}

while で待つ」のはspurious wakeup(偽の起床)対策。


27-4. C11 Atomic

#include <stdatomic.h>

atomic_int counter = ATOMIC_VAR_INIT(0);

void *worker(void *arg) {
    for (int i = 0; i < 10000; i++) {
        atomic_fetch_add(&counter, 1);
    }
    return NULL;
}

int v = atomic_load(&counter);

メモリオーダ:

atomic_fetch_add_explicit(&counter, 1, memory_order_relaxed);
atomic_fetch_add_explicit(&counter, 1, memory_order_acq_rel);
atomic_fetch_add_explicit(&counter, 1, memory_order_seq_cst);  // デフォルト

まずseq_cst、ボトルネックで緩める」が原則。


27-5. Read-Write Lock

pthread_rwlock_t rwl = PTHREAD_RWLOCK_INITIALIZER;

void read_op(void) {
    pthread_rwlock_rdlock(&rwl);    // 複数並行可
    // 読み取り
    pthread_rwlock_unlock(&rwl);
}

void write_op(void) {
    pthread_rwlock_wrlock(&rwl);    // 単独
    // 書き込み
    pthread_rwlock_unlock(&rwl);
}

読み多い場面で pthread_mutex より速い。


27-6. このセクションのまとめ

- pthread_create / join / mutex / cond
- spurious wakeup → whileでwait
- C11 _Atomicでロックフリー
- memory_orderで詳細制御
- rwlockで読み並行

28. システムコールとUNIX

28-1. ファイルI/O

#include <fcntl.h>
#include <unistd.h>

int fd = open("file.txt", O_RDONLY);
char buf[4096];
ssize_t n;
while ((n = read(fd, buf, sizeof(buf))) > 0) {
    write(STDOUT_FILENO, buf, n);
}
close(fd);

stdiofopen/fread よりも低レベル。バッファリングなし。


28-2. プロセス操作

#include <sys/wait.h>

pid_t pid = fork();
if (pid == 0) {
    // 子プロセス
    execlp("ls", "ls", "-la", NULL);
    exit(1);   // exec失敗
} else if (pid > 0) {
    // 親プロセス
    int status;
    waitpid(pid, &status, 0);
} else {
    // fork失敗
}

UNIXの fork-execモデル


28-3. パイプとシグナル

#include <signal.h>

void handler(int sig) {
    printf("got signal %d\n", sig);
}

signal(SIGINT, handler);    // 古いAPI
struct sigaction sa = { .sa_handler = handler };
sigaction(SIGINT, &sa, NULL);    // 推奨

// パイプ
int pipefd[2];
pipe(pipefd);
// pipefd[0] = 読み取り、pipefd[1] = 書き込み

28-4. ソケット(TCPサーバ)

#include <sys/socket.h>
#include <netinet/in.h>

int sock = socket(AF_INET, SOCK_STREAM, 0);
struct sockaddr_in addr = {
    .sin_family = AF_INET,
    .sin_port = htons(8080),
    .sin_addr.s_addr = INADDR_ANY,
};
bind(sock, (struct sockaddr *)&addr, sizeof(addr));
listen(sock, 10);

while (1) {
    int client = accept(sock, NULL, NULL);
    char buf[1024];
    int n = read(client, buf, sizeof(buf));
    write(client, "OK\n", 3);
    close(client);
}

POSIX socket API。Webサーバの基礎


28-5. epoll(Linux)

#include <sys/epoll.h>

int epfd = epoll_create1(0);
struct epoll_event ev = { .events = EPOLLIN, .data.fd = sock };
epoll_ctl(epfd, EPOLL_CTL_ADD, sock, &ev);

struct epoll_event events[64];
while (1) {
    int n = epoll_wait(epfd, events, 64, -1);
    for (int i = 0; i < n; i++) {
        // 処理
    }
}

数千〜数万の同時接続を 1スレッドで効率処理。nginx・Redisの核心。


28-6. このセクションのまとめ

- open/read/write/close: 低レベルI/O
- fork/exec/wait: プロセス制御
- pipe/sigaction: IPC・シグナル
- socket: ネットワーク
- epoll(Linux)/ kqueue(BSD): 高並行

29. Cのための雑多なテクニック

29-1. errnoの使い方

errno = 0;
long n = strtol(s, NULL, 10);
if (errno == ERANGE) {
    // オーバーフロー
}

POSIX関数の失敗はほぼ errno で詳細を返す。perrorstrerror で文字列化。

29-2. constとvolatileの組み合わせ

const volatile uint32_t *register;    // ハードウェアレジスタ
                                       // const = ソフトウェアは書けない
                                       // volatile = ハードウェアが書き換える

組み込みでハードウェアレジスタを表現。

29-3. 配列要素数のマクロ

#define ARRAY_SIZE(a) (sizeof(a) / sizeof((a)[0]))

int arr[10];
for (int i = 0; i < ARRAY_SIZE(arr); i++) { ... }

ただし関数引数では効かない(配列がポインタに減衰)。

29-4. 静的アサーション

_Static_assert(sizeof(int) >= 4, "int must be >= 4 bytes");
static_assert(sizeof(int) >= 4, "int must be >= 4 bytes");   // C23

コンパイル時にチェック。型サイズや定数の検証。

29-5. インライン関数

static inline int max(int a, int b) {
    return a > b ? a : b;
}

static inline でヘッダに書ける。マクロより安全(型・引数評価)。

29-6. ビットフィールドの実用

struct ipv4_header {
    uint8_t  version_ihl;
    uint8_t  tos;
    uint16_t total_length;
    uint16_t id;
    uint16_t flags_fragment;
    uint8_t  ttl;
    uint8_t  protocol;
    uint16_t checksum;
    uint32_t src_addr;
    uint32_t dst_addr;
} __attribute__((packed));

ネットワークプロトコル・ファイル形式の表現。


30. C学習の次のステップ

Cは長く使われ続けてきた言語であり、学ぶほどハードウェア、メモリ、実行時の挙動を具体的に理解しやすくなります。

Cを学ぶ価値:
  - コンピュータの仕組みが見える
  - メモリ管理の理解が深まる
  - ポインタを通じてデータ構造が腑に落ちる
  - 他言語の設計判断が分かる
  - 組み込み・OS・言語処理系で重要

現代におけるC:
  - Linuxカーネル、Windowsカーネル
  - Python / Ruby / PHPの処理系
  - Redis、SQLite、PostgreSQL
  - nginx、curl、OpenSSL
  - 組み込み・IoT
  - GPUカーネル(CUDA C)

「Cは危険な言語」と言われますが、現代のツール(ASanUBSanValgrind、静的解析)とModern Cの規律を組み合わせれば、十分に安全に書けます。

#include <stdio.h>

int main(void) {
    printf("Welcome to C, the foundation of modern computing.\n");
    return 0;
}

Cを学ぶことで、コンピュータの動きをコードから具体的に追いやすくなります。


応用: 実務で使うC


31. Cプロジェクトの構成

myproject/
├── Makefile or CMakeLists.txt
├── README.md
├── LICENSE
├── include/
│   └── myproject/
│       ├── core.h
│       └── util.h
├── src/
│   ├── core.c
│   └── util.c
├── tests/
│   ├── core_test.c
│   └── util_test.c
├── examples/
│   └── hello.c
├── third_party/
│   └── ...
└── docs/

include/<project>/ がヘッダ、#include "myproject/core.h" で参照する慣習。

Cプロジェクトでは、ヘッダを「公開API」と「内部実装」に分けると保守しやすい。include/ には外部から使わせたい型と関数だけを置き、内部用のヘッダは src/ 側に閉じる。すべてを公開ヘッダへ入れると、変更のたびに利用側の再コンパイルや互換性問題が起きやすい。

また、src/ のファイル分割は機能単位にする。util.c が巨大化すると依存関係が見えなくなるため、メモリ管理、パース、I/O、プロトコル処理のように責務で分ける。Cは名前空間が弱いので、関数名や型名にはプロジェクト接頭辞を付けると衝突を避けやすい。


32. CMake詳細

cmake_minimum_required(VERSION 3.20)
project(myproject VERSION 1.0 LANGUAGES C)

set(CMAKE_C_STANDARD 17)
set(CMAKE_C_STANDARD_REQUIRED ON)

# ライブラリ
add_library(mylib STATIC src/core.c src/util.c)
target_include_directories(mylib PUBLIC include)
target_compile_options(mylib PRIVATE -Wall -Wextra -Wpedantic)
target_compile_definitions(mylib PRIVATE _GNU_SOURCE)

# 実行ファイル
add_executable(myapp src/main.c)
target_link_libraries(myapp PRIVATE mylib)

# Sanitizerオプション
option(ENABLE_ASAN "Enable AddressSanitizer" OFF)
if(ENABLE_ASAN)
    target_compile_options(myapp PRIVATE -fsanitize=address -g)
    target_link_options(myapp PRIVATE -fsanitize=address)
endif()

# テスト
enable_testing()
add_subdirectory(tests)

# パッケージング
include(GNUInstallDirs)
install(TARGETS myapp DESTINATION ${CMAKE_INSTALL_BINDIR})
install(TARGETS mylib DESTINATION ${CMAKE_INSTALL_LIBDIR})
install(DIRECTORY include/ DESTINATION ${CMAKE_INSTALL_INCLUDEDIR})
cmake -B build -DCMAKE_BUILD_TYPE=Release
cmake --build build -j
ctest --test-dir build
cmake --install build --prefix /usr/local

33. テストフレームワーク詳細

33-1. Unity(軽量)

#include "unity.h"
#include "calculator.h"

void setUp(void) { /* テスト前 */ }
void tearDown(void) { /* テスト後 */ }

void test_add(void) {
    TEST_ASSERT_EQUAL_INT(3, add(1, 2));
    TEST_ASSERT_EQUAL_INT(0, add(-1, 1));
}

void test_div_by_zero(void) {
    TEST_ASSERT_EQUAL_INT(-1, divide(10, 0));
}

int main(void) {
    UNITY_BEGIN();
    RUN_TEST(test_add);
    RUN_TEST(test_div_by_zero);
    return UNITY_END();
}

ヘッダのみ(unity.c を直接コンパイル)。組み込み開発に最適。

33-2. CMocka(モック対応)

#include <stdarg.h>
#include <stddef.h>
#include <setjmp.h>
#include <cmocka.h>

static void test_add(void **state) {
    assert_int_equal(3, add(1, 2));
}

int main(void) {
    const struct CMUnitTest tests[] = {
        cmocka_unit_test(test_add),
    };
    return cmocka_run_group_tests(tests, NULL, NULL);
}

34. 実用パターン集

34-1. 不透明型(PIMPL)

// my_lib.h
typedef struct my_lib my_lib_t;

my_lib_t *my_lib_create(void);
void my_lib_destroy(my_lib_t *lib);
int my_lib_process(my_lib_t *lib, const char *input);
// my_lib.c
struct my_lib {
    int internal_state;
    char *buffer;
    size_t buffer_size;
};

my_lib_t *my_lib_create(void) {
    my_lib_t *lib = malloc(sizeof(*lib));
    if (!lib) return NULL;
    lib->internal_state = 0;
    lib->buffer = malloc(1024);
    if (!lib->buffer) { free(lib); return NULL; }
    lib->buffer_size = 1024;
    return lib;
}

void my_lib_destroy(my_lib_t *lib) {
    if (!lib) return;
    free(lib->buffer);
    free(lib);
}

ヘッダから実装を完全に隠す。API安定性を保ちつつ実装を変更できる。

34-2. リソースクリーンアップの集約

int process(void) {
    void *r1 = NULL;
    void *r2 = NULL;
    void *r3 = NULL;
    int ret = -1;

    r1 = malloc(...);
    if (!r1) goto cleanup;

    r2 = malloc(...);
    if (!r2) goto cleanup;

    r3 = malloc(...);
    if (!r3) goto cleanup;

    // 処理
    ret = 0;

cleanup:
    free(r3);
    free(r2);
    free(r1);
    return ret;
}

free(NULL) は安全なので、初期化時 NULL でもクリーンアップが安全。Linuxカーネルで多用。

34-3. エラーコード返却

typedef enum {
    OK = 0,
    ERR_INVALID_INPUT = -1,
    ERR_OUT_OF_MEMORY = -2,
    ERR_IO = -3,
} status_t;

status_t do_work(const char *input, char **output);

status_t s = do_work("data", &result);
if (s != OK) {
    fprintf(stderr, "error: %d\n", s);
    return s;
}

enum で意味のあるコードを返す。

34-4. 状態機械

typedef enum { IDLE, RUNNING, FAILED, COMPLETE } state_t;

typedef struct {
    state_t state;
    // 状態固有データ
} fsm_t;

void fsm_start(fsm_t *f) {
    if (f->state != IDLE) return;
    f->state = RUNNING;
    // ...
}

void fsm_complete(fsm_t *f) {
    if (f->state != RUNNING) return;
    f->state = COMPLETE;
}

enum ベースの単純な状態機械。


35. Cのセキュリティ

35-1. バッファオーバーフロー

// Bad
char buf[10];
strcpy(buf, user_input);   // 危険!

// Good
strncpy(buf, user_input, sizeof(buf) - 1);
buf[sizeof(buf) - 1] = '\0';

// Better
snprintf(buf, sizeof(buf), "%s", user_input);

Cの最大の脆弱性源。必ず境界チェック。

35-2. format string攻撃

// Bad
printf(user_input);    // 危険!%sや %nを含むかも

// Good
printf("%s", user_input);

printf の第一引数にユーザ入力を 絶対に直接渡さない

35-3. integer overflow

// Bad: サイズが負になりうる
size_t total = count * size;
char *buf = malloc(total);

// Good: オーバーフローチェック
if (count > SIZE_MAX / size) return NULL;
char *buf = malloc(count * size);

// Better: __builtin_mul_overflow
size_t total;
if (__builtin_mul_overflow(count, size, &total)) return NULL;

35-4. strict aliasingとendianness

ネットワークプロトコルやバイナリ処理では、memcpy でバイト単位コピー、htonl/ntohl でエンディアン変換。

35-5. パスワードハッシュ

Cで書くならlibsodium:

#include <sodium.h>

if (sodium_init() < 0) return -1;

char hashed_password[crypto_pwhash_STRBYTES];
if (crypto_pwhash_str(hashed_password, password, strlen(password),
        crypto_pwhash_OPSLIMIT_INTERACTIVE,
        crypto_pwhash_MEMLIMIT_INTERACTIVE) != 0) {
    return -1;
}

// 検証
if (crypto_pwhash_str_verify(hashed_password, password, strlen(password)) != 0) {
    // 失敗
}

md5 / sha1 は使わない。bcrypt / argon2 を使う。


36. C実務の補足知識

36-1. _Generic(型ジェネリック、C11)

#define abs_g(x) _Generic((x),       \
    int:    abs,                       \
    long:   labs,                      \
    double: fabs                       \
)(x)

abs_g(-5);      // abs(-5)
abs_g(-5L);     // labs(-5L)
abs_g(-5.0);    // fabs(-5.0)

Cのテンプレート相当。<tgmath.h> の中核。

36-2. atomic(C11)

#include <stdatomic.h>

atomic_int counter = 0;
atomic_fetch_add(&counter, 1);

// CAS
int expected = 0;
atomic_compare_exchange_strong(&counter, &expected, 1);

36-3. complex(C99)

#include <complex.h>

double complex z = 1.0 + 2.0 * I;
double complex w = z * z;
double r = creal(w);
double i = cimag(w);

科学計算で。

36-4. designated initializers

struct point p = {.x = 1, .y = 2};
int arr[10] = {[3] = 100, [7] = 200};   // 部分初期化

C99+。読みやすい初期化。

36-5. compound literals

struct point *p = &(struct point){.x = 1, .y = 2};
print_point((struct point){10, 20});

一時オブジェクトを作る。


37. Cエコシステム

37-1. 主要ライブラリ

GLib:        GObject、GHashTable、GPtrArrayなど
SDL:         マルチメディア・ゲーム
libuv:       非同期I/O(Node.jsの核)
libcurl:     HTTPクライアント
SQLite:      組み込みDB
zlib:        圧縮
OpenSSL:     暗号
libsodium:   モダン暗号
ImageMagick: 画像処理
ffmpeg:      動画

37-2. 言語処理系のホスト

CPython:     Cで実装(最も巨大なCプロジェクトの一つ)
Ruby (CRuby): Cで実装
PHP:          Cで実装
Perl:         Cで実装
Lua:          Cで実装、組み込み向け

スクリプト言語の処理系の大半がC。

37-3. OS / カーネル

Linuxカーネル
Windowsカーネル
macOS(XNU)
*BSD
RTOS(FreeRTOS、Zephyr)

38. Cの現代的な書き方ガイド

// ✅ Good
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

typedef struct user {
    char *name;
    int age;
} user_t;

user_t *user_create(const char *name, int age) {
    if (!name) return NULL;
    user_t *u = malloc(sizeof(*u));
    if (!u) return NULL;
    u->name = strdup(name);
    if (!u->name) {
        free(u);
        return NULL;
    }
    u->age = age;
    return u;
}

void user_destroy(user_t *u) {
    if (!u) return;
    free(u->name);
    free(u);
}

int main(void) {
    user_t *u = user_create("Alice", 30);
    if (!u) return 1;
    printf("%s, %d\n", u->name, u->age);
    user_destroy(u);
    return 0;
}

規律:

  1. const を最大限
  2. 戻り値で成功/失敗
  3. malloc後のNULLチェック
  4. リソースのcreate/destroyペア
  5. static でファイル内に隠蔽
  6. ヘッダで宣言、.c で定義
  7. -Wall -Wextra -Wpedantic 有効化
  8. -fsanitize=address,undefined をテスト時

39. 組み込みC

39-1. メモリ制約

// 動的メモリ禁止が多い
char buf[1024];           // 静的バッファ

// ヒープを使うなら定数サイズで
#define BUFFER_POOL_SIZE 16
static char buffers[BUFFER_POOL_SIZE][256];
static int buffer_used[BUFFER_POOL_SIZE];

39-2. レジスタアクセス

volatile uint32_t *gpio_data = (volatile uint32_t *)0x40020014;
*gpio_data |= (1 << 5);    // 5番ピンをON

volatile でハードウェアによる書き換えを許可。

39-3. 割り込みハンドラ

__attribute__((interrupt))
void timer_handler(void) {
    counter++;
}

特定アーキテクチャの拡張。

39-4. このセクションのまとめ

- 動的メモリは禁止 / 制限的に
- volatileでハードウェアアクセス
- __attribute__ で割り込み・パッキング
- linker scriptで配置制御

C言語のメモリレイアウトと最適化

構造体のパディングと Alignment

// パディングなし意識(実装依存)
struct Packed {
    char a;      // offset 0, size 1
    // 3 bytes padding (32bit alignment)
    int b;       // offset 4, size 4
    char c;      // offset 8, size 1
    // 7 bytes padding (64bit platform)
};  // total size: 16 bytes

// offsetof マクロで確認
#include <stddef.h>
size_t offset_b = offsetof(struct Packed, b);  // 4
size_t offset_c = offsetof(struct Packed, c);  // 8

標準ライブラリの実装詳細

malloc/free メモリ管理

// 実装概念:Free list + chunk headers
// [metadata][user data][metadata][user data]...

void* malloc(size_t size) {
    // 1. size + metadata用サイズでメモリ確保
    // 2. metadata に size と free flag を記録
    // 3. user area へのポインタを返す
}

void free(void* ptr) {
    // 1. metadata から size を読む
    // 2. free flag を立てる
    // 3. free list に追加(隣接chunk とマージ可能)
}

// Memory leak 検出
// Valgrind: valgrind --leak-check=full ./app
// AddressSanitizer: gcc -fsanitize=address

String Functions と Buffer Overflow

char buffer[10];

// 危険:bounds checking なし
strcpy(buffer, "very long string");  // overflow!

// 安全:サイズ明示
strncpy(buffer, input, sizeof(buffer) - 1);
buffer[sizeof(buffer) - 1] = '�';

// より安全:snprintf
snprintf(buffer, sizeof(buffer), "%s", input);

Bit Operations と Flags

// Bit flags 効率的管理
#define FLAG_READ   (1 << 0)
#define FLAG_WRITE  (1 << 1)
#define FLAG_EXEC   (1 << 2)

unsigned int perm = 0;

perm |= FLAG_READ;           // Set
if (perm & FLAG_READ) { }    // Check
perm &= ~FLAG_WRITE;         // Clear
perm ^= FLAG_EXEC;           // Toggle

// ビット数カウント(GCC builtin)
int popcount = __builtin_popcount(perm);

Function Pointers と Callbacks

typedef int (*CompareFn)(const void*, const void*);

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

int arr[] = {3, 1, 4, 1, 5};
qsort(arr, 5, sizeof(int), cmp_ints);

// Callback pattern
typedef void (*EventHandler)(int event);
EventHandler listeners[10];
size_t count = 0;

void register_handler(EventHandler h) {
    listeners[count++] = h;
}

void fire_event(int e) {
    for (size_t i = 0; i < count; i++) {
        listeners[i](e);
    }
}

C11 Features

_Generic で多相

#define MAX(x, y)     _Generic((x),         int: max_int,         long: max_long,         double: max_double     )(x, y)

_Noreturn

_Noreturn void abort_program(void) {
    // 戻らないことを宣言
    exit(1);
}

restrict ポインタ

void fast_copy(int * restrict dest,
               int * restrict src,
               size_t n) {
    // dest と src は alias しないことを約束
    // コンパイラは最適化を強化できる
    for (size_t i = 0; i < n; i++) {
        dest[i] = src[i];
    }
}

Preprocessor 活用

Conditional Compilation

#ifdef DEBUG
#define LOG(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__)
#else
#define LOG(fmt, ...) ((void)0)
#endif

#if defined(_WIN32)
#include <windows.h>
#elif defined(__APPLE__)
#include <TargetConditionals.h>
#endif

Stringification と Token Pasting

#define STRINGIFY(x) #x
#define CONCAT(a, b) a##b

const char* name = STRINGIFY(VAR);  // "VAR"
typedef int CONCAT(my_, int);       // typedef int my_int;

Signal Handling

#include <signal.h>
#include <stdlib.h>

static volatile sig_atomic_t stop = 0;

void handler(int sig) {
    stop = 1;  // async-signal-safe のみ
}

int main(void) {
    signal(SIGINT, handler);
    while (!stop) {
        // work
    }
    return 0;
}

セキュリティ実践

Format String Attack

// 危険
char input[100];
scanf("%s", input);
printf(input);  // %x で stack を読める

// 安全
printf("%s", input);

Integer Overflow チェック

#include <limits.h>

int safe_add(int a, int b, int* result) {
    if (a > 0 && b > INT_MAX - a) return -1;
    if (a < 0 && b < INT_MIN - a) return -1;
    *result = a + b;
    return 0;
}

Inline と Performance

// Inlining で関数呼び出しコスト削減
static inline int add(int a, int b) {
    return a + b;
}

// restrict で alias optimization
void process(int * restrict out,
             const int * restrict in) {
    for (int i = 0; i < N; i++) {
        out[i] = in[i] * 2;  // メモリ読取最適化可能
    }
}

Thread Safety (C11)

#include <threads.h>
#include <stdatomic.h>

_Atomic(int) counter = 0;

void increment(void) {
    atomic_fetch_add(&counter, 1);
}

int get_value(void) {
    return atomic_load(&counter);
}

Locale とCharacter Encoding

#include <locale.h>
#include <wchar.h>

setlocale(LC_ALL, "ja_JP.UTF-8");

wchar_t str[] = L"こんにちは";
wprintf(L"Width: %zu\n", wcslen(str));

40. まとめ

Cは 半世紀近く、コンピュータ業界の根幹を支えてきた言語です。

1972 - 現在:
  ・OS、ブラウザ、データベース、ゲームエンジンの基盤
  ・他のすべての言語の設計に影響
  ・組み込み・カーネルでの不動の地位
  ・C99 / C11 / C23と規格は進化中

Cは古い」という見方は半分正しく、半分間違いです。Modern C(C99/C11/C23 + ASan/UBSan + -Wall)で書けば、十分に安全で生産的なコードが書けます。

#include <stdio.h>

int main(void) {
    printf("Welcome to C.\n");
    printf("The foundation of modern computing.\n");
    return 0;
}

Cを学ぶことで、コンピュータの動きをより低いレイヤから理解できます。

最後にBrian Kernighanの言葉:

“Debugging is twice as hard as writing the code in the first place. Therefore, if you write the code as cleverly as possible, you are, by definition, not smart enough to debug it.”

シンプルに書く規律こそが、最も強い武器です。

まとめ

Cは、メモリ、ポインタ、ABI、コンパイル、未定義動作を直接意識する言語です。抽象度は高くありませんが、OS、コンパイラ、組込み、ランタイムの基礎を理解するうえで強力な教材になります。安全に書くには、所有権、境界、初期化、エラー処理、ビルド設定を常に明示する姿勢が重要です。

参考文献

公式・標準

解説・補助