第3章: ストレージクラスと状態管理の理論 - 静的変数の計算機科学的基盤
はじめに:なぜ変数は「消える」のか
プログラムにおいて、変数がいつ存在し、いつ消えるのかという問題は、計算機科学の根本的な課題です。この章では、記憶クラス(ストレージクラス)の理論的背景から始め、C言語の静的変数がなぜget_next_lineで必要なのかを深く理解します。
3.1 プログラムメモリの歴史
3.1.1 機械語時代:すべてが静的だった
1940年代〜1950年代初頭
最初期のコンピュータでは、すべてのデータは固定されたメモリアドレスに配置されていました:
ENIAC / EDVAC 時代(1940年代後半)
═══════════════════════════════════════════════════════
メモリモデル:
┌─────────────────────────────────────────────────────┐
│ アドレス 0000: 命令 1 │
│ アドレス 0001: 命令 2 │
│ ... │
│ アドレス 1000: データ A │
│ アドレス 1001: データ B │
│ ... │
└─────────────────────────────────────────────────────┘
特徴:
- すべての変数は絶対アドレスで参照
- 「変数」という概念はない(メモリ位置のみ)
- プログラムとデータの境界は不明確
- 再帰は不可能(スタックがない)
アセンブリ言語での表現:
; 1950年代風のアセンブリ
; 変数Aと変数Bを加算してCに格納
LOAD 1000 ; アドレス1000(データA)を読み込み
ADD 1001 ; アドレス1001(データB)を加算
STORE 1002 ; アドレス1002(データC)に格納
; 問題:
; - アドレス1000, 1001, 1002 は固定
; - 別の場所にデータを置くとプログラムを書き換える必要
; - サブルーチンを再帰的に呼べない
3.1.2 サブルーチンと局所変数の発明
FORTRAN(1957)の登場
最初の高級言語FORTRANは、「変数」という抽象化を導入しましたが、すべての変数は静的でした:
C FORTRAN I の例
SUBROUTINE CALC
INTEGER A, B, C
C A, B, C は常に同じメモリに配置
C サブルーチンが終了しても値は保持される
A = A + 1
RETURN
END
C 問題:再帰ができない
SUBROUTINE FACT(N, RESULT)
INTEGER N, RESULT
IF (N .LE. 1) THEN
RESULT = 1
ELSE
C CALL FACT(N-1, TEMP) ← これはFORTRAN Iでは不可能
...
END IF
END
ALGOL 60(1960)の革命
ALGOL 60は、ブロック構造とスタックベースのメモリ管理を導入しました。これが現代の「ローカル変数」の起源です:
ALGOL 60 の革新
═══════════════════════════════════════════════════════
1. ブロック構造(Block Structure)
begin
integer a; ← この 'a' はブロック内でのみ有効
a := 1;
begin
integer a; ← これは別の 'a'(内側のブロック)
a := 2;
end;
← ここでは最初の 'a' が復活(= 1)
end
2. 動的なメモリ割り当て
- 変数はブロック進入時に作成
- ブロック脱出時に破棄
- これにより再帰が可能に!
3. スタックの概念
- Last In, First Out (LIFO)
- 関数呼び出しのネストを自然に表現
- 「活性化レコード(Activation Record)」の導入
3.1.3 活性化レコード(Activation Record)
Dijkstraが1960年代に形式化した概念で、関数呼び出しの状態を管理する仕組みです:
活性化レコードの構造
═══════════════════════════════════════════════════════
関数が呼び出されると、スタックに以下の情報が積まれる:
┌────────────────────────────────────────────────────┐
│ 活性化レコード │
├────────────────────────────────────────────────────┤
│ 戻りアドレス(Return Address) │
│ → 呼び出し元の次の命令 │
├────────────────────────────────────────────────────┤
│ 動的リンク(Dynamic Link) │
│ → 呼び出し元の活性化レコードへのポインタ │
├────────────────────────────────────────────────────┤
│ 静的リンク(Static Link) │
│ → 語彙的に囲む環境へのポインタ(ネスト時) │
├────────────────────────────────────────────────────┤
│ パラメータ │
│ → 関数に渡された引数 │
├────────────────────────────────────────────────────┤
│ ローカル変数 │
│ → 関数内で宣言された変数 │
├────────────────────────────────────────────────────┤
│ 一時領域 │
│ → 式の計算に使用 │
└────────────────────────────────────────────────────┘
例:factorial(3) の呼び出し
─────────────────────────────
Stack (高位アドレス→低位アドレス)
┌────────────────────┐ ← Stack Top
│ factorial(1) │
│ ├─ return addr │
│ ├─ n = 1 │
│ └─ result = 1 │
├────────────────────┤
│ factorial(2) │
│ ├─ return addr │
│ ├─ n = 2 │
│ └─ waiting... │
├────────────────────┤
│ factorial(3) │
│ ├─ return addr │
│ ├─ n = 3 │
│ └─ waiting... │
├────────────────────┤
│ main() │
└────────────────────┘ ← Stack Bottom
3.1.4 C言語の誕生とストレージクラス(1972年)
Dennis RitchieがUnix用に開発したC言語は、ALGOLのアイデアを実用的な形で実装しました:
/* C言語の4つのストレージクラス(1972年〜) */
/* 1. auto(自動変数)- デフォルト */
void function(void)
{
int x; /* auto int x; と同じ */
auto int y; /* 明示的な auto(現代ではほぼ使用されない) */
}
/* 2. static(静的変数) */
void function(void)
{
static int count = 0; /* 関数呼び出し間で保持 */
count++;
}
/* 3. register(レジスタ変数)- 現代ではほぼ無意味 */
void function(void)
{
register int i; /* 高速アクセスのヒント */
for (i = 0; i < 100; i++)
/* ... */;
}
/* 4. extern(外部変数) */
extern int global_counter; /* 他のファイルで定義された変数 */
C言語のストレージモデル:
C言語のメモリセグメント(Unix環境)
═══════════════════════════════════════════════════════
高位アドレス
┌─────────────────────────────────────────────────────┐
│ Stack │
│ ← 自動変数(auto)、関数引数、戻りアドレス │
│ ← 高位アドレスから低位へ成長 │
├─────────────────────────────────────────────────────┤
│ ↓ │
│ (空き領域) │
│ ↑ │
├─────────────────────────────────────────────────────┤
│ Heap │
│ ← malloc/free で管理 │
│ ← 低位アドレスから高位へ成長 │
├─────────────────────────────────────────────────────┤
│ BSS │
│ ← 未初期化の static/global 変数 │
│ ← プログラム起動時に0で初期化 │
├─────────────────────────────────────────────────────┤
│ Data │
│ ← 初期化済みの static/global 変数 │
│ ← コンパイル時に値が決定 │
├─────────────────────────────────────────────────────┤
│ Text │
│ ← プログラムコード(機械語命令) │
│ ← 読み取り専用 │
└─────────────────────────────────────────────────────┘
低位アドレス
3.2 記憶域期間(Storage Duration)の理論
3.2.1 記憶域期間の分類
C言語標準(C11/C17)では、オブジェクトの記憶域期間を4種類に分類しています:
記憶域期間の理論的分類
═══════════════════════════════════════════════════════
1. Static Storage Duration(静的記憶域期間)
─────────────────────────────────────────
存在期間:プログラム開始 → プログラム終了
初期化:プログラム起動前に一度だけ
該当:
- ファイルスコープの変数(グローバル変数)
- static修飾された変数
例:
int global; /* 静的記憶域期間 */
static int file_var; /* 静的記憶域期間 */
void f(void) {
static int local; /* 静的記憶域期間 */
}
2. Automatic Storage Duration(自動記憶域期間)
───────────────────────────────────────────────
存在期間:ブロック進入 → ブロック脱出
初期化:ブロックに入るたびに
該当:
- ブロックスコープで宣言された非static変数
例:
void f(void) {
int local; /* 自動記憶域期間 */
for (int i = 0; ...) {
int inner; /* 自動記憶域期間 */
}
}
3. Thread Storage Duration(スレッド記憶域期間)
───────────────────────────────────────────────
存在期間:スレッド開始 → スレッド終了
初期化:スレッド開始時
該当:
- _Thread_local修飾された変数(C11以降)
例:
_Thread_local int thread_counter;
4. Allocated Storage Duration(割当記憶域期間)
─────────────────────────────────────────────
存在期間:malloc → free
初期化:プログラマの責任
該当:
- 動的メモリ割り当て
例:
int *p = malloc(sizeof(int));
/* ... 使用 ... */
free(p);
3.2.2 束縛時期(Binding Time)
変数とメモリアドレスの関連付け(束縛)がいつ行われるかという理論的概念:
束縛時期の分類
═══════════════════════════════════════════════════════
1. コンパイル時束縛(Compile-time Binding)
─────────────────────────────────────────
時期:コンパイラがプログラムを処理するとき
対象:静的変数のアドレス、定数
例:
static int x = 42;
/* x のアドレスは実行ファイル内で決定 */
/* ただし、位置独立コード(PIC)では再配置される */
2. リンク時束縛(Link-time Binding)
───────────────────────────────────
時期:リンカがオブジェクトファイルを結合するとき
対象:グローバル変数、extern変数
例:
/* file1.c */
int shared_var = 100;
/* file2.c */
extern int shared_var;
/* リンク時に shared_var のアドレスが解決 */
3. ロード時束縛(Load-time Binding)
───────────────────────────────────
時期:プログラムがメモリにロードされるとき
対象:動的リンクライブラリ、再配置
例:
/* 共有ライブラリの関数 */
printf("Hello");
/* printf のアドレスはロード時に決定 */
4. 実行時束縛(Run-time Binding)
────────────────────────────────
時期:プログラム実行中
対象:ローカル変数、動的メモリ
例:
void f(void) {
int local; /* f() が呼ばれるたびにアドレスが決定 */
int *p = malloc(100); /* malloc時にアドレスが決定 */
}
3.2.3 静的変数の初期化の理論
静的変数の初期化は、厳密なルールに従います:
/* 初期化のルール */
/* 1. 定数式のみ許可 */
static int a = 42; /* OK: 整数リテラル */
static int b = 10 + 20; /* OK: 定数式 */
static int c = sizeof(int); /* OK: sizeof は定数式 */
static int *p = NULL; /* OK: NULL は定数 */
/* 2. 関数呼び出しは不可 */
int get_value(void) { return 42; }
static int d = get_value(); /* エラー! */
/* 3. 非定数変数は不可 */
int runtime_value = 42;
static int e = runtime_value; /* エラー! */
/* 4. 複合リテラル(C99以降) */
static int arr[] = {1, 2, 3}; /* OK */
static struct { int x, y; } pt = {1, 2}; /* OK */
なぜこの制限があるのか?
静的変数の初期化制限の理論的根拠
═══════════════════════════════════════════════════════
理由1: 初期化タイミングの問題
─────────────────────────────
静的変数は main() 実行前に初期化される
→ この時点で関数は呼べない
→ ランタイムの値も存在しない
実行順序:
1. プログラムがメモリにロード
2. BSS セクションが0で初期化
3. Data セクションが初期値で設定 ← ここで静的変数初期化
4. 動的リンカが処理
5. main() が呼ばれる
理由2: 決定性(Determinism)
─────────────────────────────
プログラムは毎回同じ初期状態で開始すべき
→ 関数の戻り値は実行時に変わる可能性がある
→ 非決定的な初期化は避けるべき
理由3: 実行ファイルへの埋め込み
───────────────────────────────
初期化された静的変数の値は、実行ファイルに含まれる
→ コンパイル時に値が確定している必要がある
$ size a.out
text data bss dec hex filename
1234 56 16 1306 51a a.out
^^
初期化された静的変数のサイズ
3.3 スコープと可視性の理論
3.3.1 レキシカルスコープ(Lexical Scoping)
C言語はレキシカルスコープ(静的スコープ)を採用しています:
レキシカルスコープ vs ダイナミックスコープ
═══════════════════════════════════════════════════════
レキシカルスコープ(C, Java, Python 等)
───────────────────────────────────────
変数の参照は「プログラムテキスト」で決まる
コンパイル時に変数の参照先が確定
int x = 10;
void f(void) {
printf("%d", x); /* 常にグローバルの x */
}
void g(void) {
int x = 20;
f(); /* 10 を出力(グローバルの x) */
}
ダイナミックスコープ(Bash, 古いLisp 等)
─────────────────────────────────────────
変数の参照は「呼び出し履歴」で決まる
実行時に変数の参照先が決まる
x=10
f() {
echo $x /* 呼び出し元の x */
}
g() {
local x=20
f # 20 を出力(g の x)
}
3.3.2 名前隠蔽(Name Hiding)
内側のスコープの変数が外側の変数を「隠す」現象:
int x = 10; /* グローバルスコープ */
void outer(void)
{
int x = 20; /* outer のローカルスコープ - グローバルを隠す */
{
int x = 30; /* ブロックスコープ - outer の x を隠す */
printf("%d\n", x); /* 30 */
}
printf("%d\n", x); /* 20 */
}
/* グローバルの x は影響を受けない */
シンボルテーブルによる名前解決:
コンパイラの名前解決プロセス
═══════════════════════════════════════════════════════
シンボルテーブルの構造(スタックベース):
識別子 'x' の参照を解決するとき:
1. 現在のスコープを検索
2. 見つからなければ外側のスコープを検索
3. 見つからなければさらに外側...
4. 最終的にグローバルスコープを検索
┌────────────────────────────────────────┐
│ Block Scope (innermost) │
│ ┌──────────────────────────────────┐ │
│ │ x → int (address: stack+8) │ │ ← 最初に検索
│ └──────────────────────────────────┘ │
├────────────────────────────────────────┤
│ Function Scope │
│ ┌──────────────────────────────────┐ │
│ │ x → int (address: stack+0) │ │ ← 次に検索
│ └──────────────────────────────────┘ │
├────────────────────────────────────────┤
│ File Scope (Global) │
│ ┌──────────────────────────────────┐ │
│ │ x → int (address: 0x601020) │ │ ← 最後に検索
│ └──────────────────────────────────┘ │
└────────────────────────────────────────┘
3.3.3 static とスコープ制限
static キーワードには、スコープを制限する効果もあります:
/* file1.c */
static int file_private = 42; /* このファイル内でのみ可視 */
void use_private(void)
{
printf("%d\n", file_private); /* OK */
}
/* file2.c */
extern int file_private; /* リンクエラー! */
/* 'file_private' への未定義参照 */
リンケージ(Linkage)の概念:
リンケージの分類
═══════════════════════════════════════════════════════
1. External Linkage(外部リンケージ)
───────────────────────────────────
他のファイルから参照可能
デフォルトでグローバル変数/関数
/* file1.c */
int global_var = 10; /* 外部リンケージ */
void global_func(void); /* 外部リンケージ */
/* file2.c */
extern int global_var; /* OK */
extern void global_func(void); /* OK */
2. Internal Linkage(内部リンケージ)
────────────────────────────────────
同じファイル内でのみ参照可能
static 修飾された変数/関数
/* file1.c */
static int private_var = 10; /* 内部リンケージ */
static void private_func(void); /* 内部リンケージ */
/* file2.c */
extern int private_var; /* リンクエラー! */
3. No Linkage(リンケージなし)
───────────────────────────────
関数内のローカル変数
void f(void) {
int local; /* リンケージなし */
static int slocal; /* 内部リンケージ(厳密には) */
}
3.4 状態と純粋関数の理論
3.4.1 純粋関数(Pure Function)
関数型プログラミングの重要な概念:
純粋関数の定義
═══════════════════════════════════════════════════════
純粋関数とは:
1. 同じ引数に対して常に同じ結果を返す(参照透過性)
2. 副作用がない(外部状態を変更しない)
純粋関数の例:
────────────────
int add(int a, int b)
{
return a + b; /* 常に同じ入力 → 同じ出力 */
}
int square(int x)
{
return x * x;
}
size_t ft_strlen(const char *s)
{
size_t len = 0;
while (s[len])
len++;
return len;
}
非純粋関数の例:
────────────────
int global_counter = 0;
int increment(void)
{
return ++global_counter; /* グローバル状態を変更 */
}
int get_random(void)
{
return rand(); /* 呼び出しごとに異なる値 */
}
char *get_next_line(int fd)
{
static char *buffer; /* 内部状態を保持 */
/* ... */
}
3.4.2 副作用(Side Effects)
副作用の分類
═══════════════════════════════════════════════════════
1. 状態変更(State Mutation)
─────────────────────────────
グローバル変数の変更
静的変数の変更
引数として渡されたオブジェクトの変更
void mutate(int *p) { *p = 42; }
2. I/O操作
──────────
ファイル読み書き
標準入出力
ネットワーク通信
printf("Hello");
read(fd, buf, 100);
3. 例外/非局所ジャンプ
───────────────────
setjmp/longjmp
signal
get_next_line の副作用:
───────────────────────
char *get_next_line(int fd)
{
static char *remainder; /* 副作用1: 静的変数の変更 */
char buffer[BUFFER_SIZE];
read(fd, buffer, ...); /* 副作用2: ファイル読み取り */
/* ... */
}
→ get_next_line は意図的に非純粋関数
→ 状態を保持することが仕様の一部
3.4.3 なぜ get_next_line は静的変数を必要とするのか
get_next_line の仕様と状態
═══════════════════════════════════════════════════════
問題設定:
ファイルから1行ずつ読み取る
ただし、バッファサイズは固定
課題:
read() は「行」を認識しない
→ 改行の途中でバッファが切れることがある
→ 読み過ぎた部分を保存する必要がある
例:
ファイル: "Hello\nWorld\n"
BUFFER_SIZE = 10
1回目の read(): "Hello\nWorl" を読み込み
^^^^^^^^ これを返したい
^^^^ これを保存する必要
2回目の get_next_line() 呼び出し:
→ 前回保存した "Worl" から処理を開始
解決策:状態の保持
─────────────────
Option 1: 静的変数(C言語的解決)
static char *remainder;
Option 2: コンテキスト引数(関数型的解決)
char *get_next_line(int fd, char **context);
Option 3: オブジェクト/構造体(OOP的解決)
typedef struct s_reader { ... } t_reader;
char *reader_get_line(t_reader *reader);
42の課題では Option 1 が要求される
→ 関数シグネチャが固定されているため
3.5 メモリレイアウトの理論
3.5.1 プロセスメモリモデル
プロセスの仮想アドレス空間(64bit Linux)
═══════════════════════════════════════════════════════
0xFFFFFFFFFFFFFFFF ┌──────────────────────────────────┐
│ Kernel Space │
│ (ユーザーからアクセス不可) │
0x7FFFFFFFFFFF ├──────────────────────────────────┤
│ │
│ Stack │
│ ↓ 下方向に成長 │
│ │
├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤
│ │
│ (空き領域) │
│ │
├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤
│ │
│ Heap │
│ ↑ 上方向に成長 │
│ │
├──────────────────────────────────┤
│ BSS │
│ (未初期化静的データ) │
├──────────────────────────────────┤
│ Data │
│ (初期化済み静的データ) │
├──────────────────────────────────┤
│ Text │
│ (プログラムコード) │
0x0000000000400000 ├──────────────────────────────────┤
│ (予約領域) │
0x0000000000000000 └──────────────────────────────────┘
3.5.2 各セグメントの詳細
Text セグメント:
/* Text セグメント - プログラムコード */
void function(void) /* この関数のマシンコード */
{
int x = 42; /* この命令も Text に含まれる */
printf("Hello\n"); /* 関数呼び出し命令も Text */
}
/* 特性:
* - 読み取り専用(書き込み禁止)
* - 実行可能
* - 複数プロセスで共有可能(コード共有)
*/
Data セグメント:
/* Data セグメント - 初期化済みの静的データ */
int global_init = 42; /* Data */
static int file_static = 100; /* Data */
char *str = "Hello"; /* ポインタ自体は Data、文字列は rodata */
void function(void)
{
static int local_init = 50; /* Data */
}
/* 特性:
* - 読み書き可能
* - 初期値が実行ファイルに含まれる
* - プログラムロード時に値がコピーされる
*/
BSS セグメント:
/* BSS (Block Started by Symbol) - 未初期化の静的データ */
int global_uninit; /* BSS */
static int file_static_uninit; /* BSS */
static char buffer[1024]; /* BSS */
void function(void)
{
static int local_uninit; /* BSS */
}
/* 特性:
* - 読み書き可能
* - 実行ファイルにはサイズ情報のみ(データなし)
* - プログラム起動時に0で初期化
* - 実行ファイルサイズを小さく保てる
*/
BSS の名前の由来:
BSS = "Block Started by Symbol"
歴史(1950年代):
───────────────
IBMのアセンブラで使用されたディレクティブ
BSS n ; n バイトの未初期化領域を確保
例:
SYMBOL1 BSS 100 ; SYMBOL1 から100バイト確保
SYMBOL2 BSS 50 ; SYMBOL2 から50バイト確保
現代での意味:
未初期化のグローバル/静的変数を格納するセグメント
3.5.3 get_next_line のメモリ配置
/* get_next_line のメモリレイアウト分析 */
char *get_next_line(int fd)
{
/* remainder: BSS セグメント
* - 静的ポインタ変数
* - 初期値 0 (NULL)
* - プログラム全体で1つだけ存在
*/
static char *remainder = NULL;
/* buffer: Stack セグメント
* - 自動変数(配列)
* - 関数呼び出しごとに作成/破棄
* - BUFFER_SIZE + 1 バイト
*/
char buffer[BUFFER_SIZE + 1];
/* line: Stack セグメント
* - 自動変数(ポインタ)
* - 8 バイト(64bit)または 4 バイト(32bit)
*/
char *line;
/* malloc で確保した領域: Heap セグメント
* - 動的メモリ
* - free するまで存在
* - remainder が指す先
* - line が指す先(返却値)
*/
line = malloc(100);
remainder = malloc(BUFFER_SIZE);
return line; /* Heap 上のアドレスを返す */
}
図解:
メモリ配置の視覚化
═══════════════════════════════════════════════════════
[BSS セグメント]
┌──────────────────────────────────────────────────────┐
│ 変数名: remainder │
│ サイズ: 8 bytes (64bit ポインタ) │
│ 値: 0x00007f8a12345000 (Heapを指す) │
│ アドレス: 0x601040 │
└──────────────────────────────────────────────────────┘
[Heap セグメント]
┌──────────────────────────────────────────────────────┐
│ アドレス: 0x00007f8a12345000 │
│ サイズ: 可変(読み込んだデータ量による) │
│ 内容: "Hello World\n" (前回の余り) │
│ 解放責任: プログラマ │
└──────────────────────────────────────────────────────┘
[Stack セグメント] - get_next_line 実行中
┌──────────────────────────────────────────────────────┐
│ 戻りアドレス: 0x00000000004011a0 │
├──────────────────────────────────────────────────────┤
│ 変数: fd = 3 │
├──────────────────────────────────────────────────────┤
│ 変数: buffer[BUFFER_SIZE + 1] │
│ サイズ: BUFFER_SIZE + 1 bytes │
│ 内容: "Next line\n\0..." │
├──────────────────────────────────────────────────────┤
│ 変数: line │
│ サイズ: 8 bytes │
│ 値: 0x00007f8a12346000 (Heapを指す) │
└──────────────────────────────────────────────────────┘
3.6 静的変数の実践的活用
3.6.1 get_next_line の基本パターン
/* 基本的な静的変数の使用パターン */
char *get_next_line(int fd)
{
static char *remainder = NULL; /* 状態を保持 */
char buffer[BUFFER_SIZE + 1];
char *line;
ssize_t bytes_read;
/* パラメータ検証 */
if (fd < 0 || BUFFER_SIZE <= 0)
return (NULL);
/* 読み込みと処理 */
while (!has_newline(remainder))
{
bytes_read = read(fd, buffer, BUFFER_SIZE);
if (bytes_read <= 0)
break;
buffer[bytes_read] = '\0';
remainder = append_to_remainder(remainder, buffer);
}
/* 行の抽出と残りの更新 */
line = extract_line(&remainder);
return (line);
}
3.6.2 複数ファイルディスクリプタ対応(ボーナス)
/* 複数fd対応:配列による状態管理 */
#ifndef OPEN_MAX
# define OPEN_MAX 1024
#endif
char *get_next_line(int fd)
{
/* 各fdに対応する状態を配列で管理 */
static char *remainders[OPEN_MAX];
if (fd < 0 || fd >= OPEN_MAX || BUFFER_SIZE <= 0)
return (NULL);
/* fd に対応する remainder を使用 */
return (process_fd(fd, &remainders[fd]));
}
/*
* メモリレイアウト:
*
* [BSS セグメント]
* remainders[0] = NULL or Heap pointer (stdin)
* remainders[1] = NULL or Heap pointer (stdout)
* remainders[2] = NULL or Heap pointer (stderr)
* remainders[3] = NULL or Heap pointer (最初のファイル)
* remainders[4] = NULL or Heap pointer (2番目のファイル)
* ...
* remainders[1023] = NULL or Heap pointer
*
* 合計サイズ: 8 * 1024 = 8KB(64bit環境)
*/
3.6.3 状態のライフサイクル管理
/* 状態の作成・更新・破棄 */
static char *create_state(const char *initial)
{
if (!initial || !*initial)
return (NULL);
return (ft_strdup(initial));
}
static char *update_state(char *state, const char *addition)
{
char *new_state;
if (!state)
return (create_state(addition));
new_state = ft_strjoin(state, addition);
free(state); /* 古い状態を解放 */
return (new_state);
}
static void destroy_state(char **state)
{
if (state && *state)
{
free(*state);
*state = NULL;
}
}
/* get_next_line での使用 */
char *get_next_line(int fd)
{
static char *remainder = NULL;
/* エラー時は状態を破棄 */
if (fd < 0)
{
destroy_state(&remainder);
return (NULL);
}
/* 読み込みと状態更新 */
while (/* ... */)
{
bytes_read = read(fd, buffer, BUFFER_SIZE);
if (bytes_read < 0)
{
destroy_state(&remainder); /* エラー時のクリーンアップ */
return (NULL);
}
if (bytes_read == 0)
break;
remainder = update_state(remainder, buffer);
}
/* ... */
}
3.7 メモリリークの理論と防止
3.7.1 メモリリークの定義
メモリリーク(Memory Leak)
═══════════════════════════════════════════════════════
定義:
確保したメモリへの参照を失い、
解放できなくなった状態
正式な定義(形式手法的):
Let M = set of allocated memory blocks
Let R = set of reachable memory blocks
Leak = M - R (確保されているが到達不能なブロック)
例:
void leak(void) {
char *p = malloc(100);
p = malloc(200); /* 最初の100バイトへの参照を失う */
} /* → 100バイトがリーク */
結果:
1. メモリ使用量が増加し続ける
2. 長時間実行でシステムが不安定に
3. 最悪の場合、メモリ枯渇でクラッシュ
3.7.2 get_next_line でのリークパターン
/* パターン1: 中間結果の解放忘れ */
/* 悪い例 */
char *bad_join(char *s1, const char *s2)
{
return (ft_strjoin(s1, s2)); /* s1 が解放されない */
}
/* 良い例 */
char *good_join(char *s1, const char *s2)
{
char *result = ft_strjoin(s1, s2);
free(s1); /* 古いメモリを解放 */
return (result);
}
/* パターン2: エラーパス での解放忘れ */
/* 悪い例 */
char *bad_read(int fd, char **remainder)
{
char *buffer = malloc(BUFFER_SIZE + 1);
if (!buffer)
return (NULL); /* remainder が解放されない */
if (read(fd, buffer, BUFFER_SIZE) < 0)
{
free(buffer);
return (NULL); /* remainder が解放されない */
}
/* ... */
}
/* 良い例 */
char *good_read(int fd, char **remainder)
{
char *buffer = malloc(BUFFER_SIZE + 1);
if (!buffer)
{
free(*remainder);
*remainder = NULL;
return (NULL);
}
if (read(fd, buffer, BUFFER_SIZE) < 0)
{
free(buffer);
free(*remainder);
*remainder = NULL;
return (NULL);
}
/* ... */
}
/* パターン3: 静的変数のクリーンアップ忘れ */
/* 問題のあるコード */
int main(void)
{
int fd = open("file.txt", O_RDONLY);
char *line = get_next_line(fd);
/* 1行だけ読んで終了 */
free(line);
close(fd);
return (0);
/* remainder に残ったデータがリーク */
}
/* 解決策1: 最後まで読む */
int main(void)
{
int fd = open("file.txt", O_RDONLY);
char *line;
while ((line = get_next_line(fd)) != NULL)
free(line);
close(fd);
return (0);
}
/* 解決策2: クリーンアップ用の呼び出し */
int main(void)
{
int fd = open("file.txt", O_RDONLY);
char *line = get_next_line(fd);
free(line);
get_next_line(-1); /* fd < 0 でクリーンアップ */
close(fd);
return (0);
}
3.7.3 メモリリーク検出
# Valgrind によるリーク検出
# コンパイル(デバッグ情報付き)
gcc -g -D BUFFER_SIZE=42 get_next_line.c main.c -o gnl
# リーク検出
valgrind --leak-check=full --show-leak-kinds=all ./gnl
# 出力例(リークあり)
==12345== LEAK SUMMARY:
==12345== definitely lost: 1,024 bytes in 1 blocks
==12345== indirectly lost: 0 bytes in 0 blocks
==12345== possibly lost: 0 bytes in 0 blocks
==12345== still reachable: 100 bytes in 1 blocks
==12345== suppressed: 0 bytes in 0 blocks
# "definitely lost": 完全にリーク
# "still reachable": まだ到達可能(静的変数経由)
# 出力例(リークなし)
==12345== LEAK SUMMARY:
==12345== definitely lost: 0 bytes in 0 blocks
==12345== indirectly lost: 0 bytes in 0 blocks
==12345== possibly lost: 0 bytes in 0 blocks
==12345== still reachable: 0 bytes in 0 blocks
==12345== suppressed: 0 bytes in 0 blocks
==12345== All heap blocks were freed -- no leaks are possible
3.8 スレッド安全性の理論
3.8.1 競合状態(Race Condition)
競合状態の理論
═══════════════════════════════════════════════════════
定義:
複数の実行スレッドが共有リソースに同時アクセスし、
実行順序によって結果が変わる状態
例:静的変数への同時アクセス
Thread A Thread B
─────────────────────────────────────────────────
1. remainder = "Hello" |
2. read buffer | 1. remainder = "World"
3. append to remainder | 2. read buffer
4. ↓ | 3. append to remainder
結果: "HelloWorld..." | 結果: "World..."
または予測不能な結果
3.8.2 スレッドローカルストレージ(TLS)
/* C11 以降 */
_Thread_local static char *remainder = NULL;
/* GCC/Clang 拡張 */
__thread static char *remainder = NULL;
/*
* TLSの動作:
*
* Thread A: remainder_A → "Hello..."
* Thread B: remainder_B → "World..."
*
* 各スレッドが独自のコピーを持つ
* → 競合状態が発生しない
*/
3.8.3 42の評価基準
42 get_next_line の評価基準
═══════════════════════════════════════════════════════
要求されていないこと:
- スレッド安全性は要求されていない
- シングルスレッド環境での動作のみ保証すればよい
ボーナス要件:
- 複数fd対応は必要
- しかし、これは「同時」アクセスではない
/* 期待される動作 */
fd1 = open("file1.txt", O_RDONLY);
fd2 = open("file2.txt", O_RDONLY);
/* 順次アクセス(同時ではない) */
line1 = get_next_line(fd1); /* OK */
line2 = get_next_line(fd2); /* OK */
line1 = get_next_line(fd1); /* OK */
/* これで十分 */
まとめ
この章では、静的変数の背後にある計算機科学の理論を学びました:
- プログラムメモリの歴史
- 記憶域期間の理論
- スコープと可視性
- 状態と純粋関数
- メモリレイアウト
- メモリリーク防止
次章では、バッファリングの理論とBUFFER_SIZEの意味について学びます。