第5章: ソフトウェアアーキテクチャと実装戦略
5.1 ソフトウェアアーキテクチャの原則
Parnasの情報隠蔽(1972年)
David L. Parnasは1972年に情報隠蔽(Information Hiding)の原則を提唱した("On the Criteria To Be Used in Decomposing Systems into Modules", Communications of the ACM, 1972)。
原則:
モジュールは「変更される可能性のある設計決定」を隠蔽すべきである。
インターフェースは安定した抽象を提供し、実装の詳細を隠す。
Parnasの2つの分解基準:
従来の基準(フローチャートベース):
1. 処理のステップに分割
2. 各ステップをモジュールに
→ 変更に弱い
新しい基準(情報隠蔽ベース):
1. 変更される可能性のある決定を特定
2. 各決定を1つのモジュールに隠蔽
→ 変更に強い
Philosophersプロジェクトへの適用:
変更される可能性のある決定:
1. 時間管理の方法 → time.c に隠蔽
2. 出力フォーマット → utils.c に隠蔽
3. 同期メカニズム → mutex操作を関数でラップ
4. デッドロック回避戦略 → routine.c に隠蔽
UNIXの設計哲学
Ken Thompson と Dennis Ritchie によるUNIXの設計哲学(1978年):
Doug McIlroyの要約:
1. 1つのことを行い、うまく行う
2. 協調して動作するプログラムを書く
3. テキストストリームを扱う(普遍的インターフェース)
Philosophersの設計への適用:
ファイル構成(単一責任の原則):
main.c → プログラムのエントリーポイントと全体制御
init.c → 初期化のみ
routine.c → 哲学者の行動ロジック
monitor.c → 監視ロジック
time.c → 時間管理のみ
utils.c → ユーティリティ関数
cleanup.c → リソース解放のみ
結合度と凝集度
Larry Constantine と Edward Yourdon は1979年に結合度(Coupling)と凝集度(Cohesion)を定式化した("Structured Design", 1979):
結合度(低い方が良い):
内容結合(最悪): 一方が他方の内部を直接操作
共通結合: グローバル変数を共有
制御結合: 制御フラグを渡す
スタンプ結合: 構造体を渡す
データ結合(最良): 単純なデータのみ渡す
凝集度(高い方が良い):
偶発的凝集(最悪): 関連のない機能の集まり
論理的凝集: 似た機能(全て「初期化」など)
時間的凝集: 同時に実行される機能
手続き的凝集: 順序に従う機能
通信的凝集: 同じデータを扱う機能
逐次的凝集: 出力が次の入力になる
機能的凝集(最良): 単一の明確な目的
5.2 データ構造設計の理論
キャッシュ局所性(Cache Locality)
Hennessy と Patterson は "Computer Architecture: A Quantitative Approach"(1990年)において、キャッシュ効率の重要性を解説した:
時間的局所性(Temporal Locality):
最近アクセスしたデータは再度アクセスされる可能性が高い
→ 頻繁にアクセスするデータをまとめる
空間的局所性(Spatial Locality):
あるアドレスへのアクセスは、近くのアドレスへのアクセスを伴う
→ 連続したメモリ配置が効率的
キャッシュラインの考慮:
典型的なキャッシュラインサイズ: 64バイト
悪い例: 構造体メンバがキャッシュラインをまたぐ
良い例: 関連データを64バイト境界に収める
構造体のメモリレイアウト
パディングとアラインメント:
/* 悪い例: パディングが多い */
struct bad_layout {
char a; /* 1 byte + 7 padding */
long b; /* 8 bytes */
char c; /* 1 byte + 7 padding */
long d; /* 8 bytes */
}; /* 合計: 32 bytes(実データ: 18 bytes) */
/* 良い例: メンバを大きさ順に並べる */
struct good_layout {
long b; /* 8 bytes */
long d; /* 8 bytes */
char a; /* 1 byte */
char c; /* 1 byte + 6 padding */
}; /* 合計: 24 bytes(実データ: 18 bytes) */
False Sharing の回避
異なるスレッドが同じキャッシュラインのデータを更新すると、パフォーマンスが大幅に低下する:
/* 悪い例: false sharing が発生 */
struct shared_data {
int counter_thread_1; /* 同じキャッシュラインに */
int counter_thread_2; /* 配置される可能性 */
};
/* 良い例: キャッシュライン境界で分離 */
struct shared_data {
int counter_thread_1;
char padding[60]; /* 64バイト境界まで埋める */
int counter_thread_2;
};
5.3 時間管理の理論
POSIXの時間概念
POSIXは複数の時計(clock)を定義している:
CLOCK_REALTIME:
実世界の時刻(wall clock time)
システム管理者やNTPにより変更される可能性
1970年1月1日 00:00:00 UTC からの経過時間
CLOCK_MONOTONIC:
単調増加する時計
システム起動からの経過時間
時刻の巻き戻りなし(経過時間測定に適切)
gettimeofday() の仕様:
#include <sys/time.h>
int gettimeofday(struct timeval *tv, struct timezone *tz);
struct timeval {
time_t tv_sec; /* 秒 */
suseconds_t tv_usec; /* マイクロ秒 (0-999999) */
};
リアルタイムシステムの時間精度
並行システムにおける時間精度は重要である。Philosophersでは、ミリ秒単位の精度が要求される:
usleep() の非精度性:
usleep(n) は「少なくとも n マイクロ秒」スリープする
スケジューリング遅延により、実際の遅延は長くなる可能性
典型的な誤差: 数ミリ秒~数十ミリ秒
精密なスリープの実装戦略:
/* 戦略1: ビジーウェイト(CPU負荷高) */
void busy_wait_ms(long ms)
{
long start = get_time_ms();
while (get_time_ms() - start < ms)
; /* CPU 100% 使用 */
}
/* 戦略2: ハイブリッド(推奨) */
void precise_sleep_ms(long ms)
{
long start = get_time_ms();
long remaining;
while (1)
{
remaining = ms - (get_time_ms() - start);
if (remaining <= 0)
break;
if (remaining > 10)
usleep(5000); /* 大きな遅延: 5ms スリープ */
else if (remaining > 1)
usleep(500); /* 中程度: 0.5ms スリープ */
else
usleep(100); /* 微調整: 0.1ms スリープ */
}
}
5.4 防御的プログラミング
McConnellの防御的プログラミング原則
Steve McConnell は "Code Complete"(1993年, 2004年改訂)において、防御的プログラミングの原則を体系化した:
原則1: 不正な入力から身を守る:
int init_data(t_data *data, int argc, char **argv)
{
/* 引数のNULLチェック */
if (!data || !argv)
return (error_return("NULL pointer"));
/* 引数の範囲チェック */
if (argc < 5 || argc > 6)
return (error_return("Invalid argument count"));
/* 値の妥当性チェック */
data->num_philos = ft_atoi(argv[1]);
if (data->num_philos <= 0 || data->num_philos > 200)
return (error_return("Invalid philosopher count"));
/* ... */
}
原則2: アサーションを使う:
#include <assert.h>
void philosopher_routine(t_philo *philo)
{
/* 開発時のみ有効な検証 */
assert(philo != NULL);
assert(philo->data != NULL);
assert(philo->id >= 1 && philo->id <= philo->data->num_philos);
/* ... */
}
原則3: エラーを早期に発見する:
int init_mutexes(t_data *data)
{
int i;
int ret;
data->forks = malloc(sizeof(pthread_mutex_t) * data->num_philos);
if (!data->forks)
return (-1); /* 早期リターン */
i = 0;
while (i < data->num_philos)
{
ret = pthread_mutex_init(&data->forks[i], NULL);
if (ret != 0)
{
/* エラー発生時、即座にクリーンアップ */
while (--i >= 0)
pthread_mutex_destroy(&data->forks[i]);
free(data->forks);
data->forks = NULL;
return (-1);
}
i++;
}
return (0);
}
エラー処理の設計パターン
パターン1: エラーコードの伝播:
int create_philosophers(t_data *data)
{
int ret;
ret = init_data(data);
if (ret != 0)
return (ret); /* エラーを伝播 */
ret = init_mutexes(data);
if (ret != 0)
{
cleanup_data(data);
return (ret);
}
ret = init_philos(data);
if (ret != 0)
{
cleanup_mutexes(data);
cleanup_data(data);
return (ret);
}
return (0);
}
パターン2: リソース取得初期化(RAII風):
/* 初期化と解放をペアで管理 */
typedef struct s_resources {
int data_initialized;
int mutexes_initialized;
int philos_initialized;
int threads_created;
} t_resources;
void cleanup_by_state(t_data *data, t_resources *res)
{
if (res->threads_created)
join_all_threads(data);
if (res->philos_initialized)
free(data->philos);
if (res->mutexes_initialized)
destroy_mutexes(data);
if (res->data_initialized)
cleanup_data(data);
}
5.5 プロジェクト構造の設計
ヘッダーファイルの設計
/* philo.h */
#ifndef PHILO_H
# define PHILO_H
/*
** 標準ライブラリ
*/
# include <stdio.h>
# include <stdlib.h>
# include <unistd.h>
# include <pthread.h>
# include <sys/time.h>
/*
** 定数定義
*/
# define SUCCESS 0
# define FAILURE -1
# define TRUE 1
# define FALSE 0
/*
** 型定義(前方宣言)
*/
typedef struct s_data t_data;
typedef struct s_philo t_philo;
/*
** 哲学者構造体
** - 各哲学者の状態を管理
** - スレッドから独立してアクセスされる
*/
struct s_philo
{
int id; /* 識別番号 (1-N) */
int left_fork_id; /* 左フォークインデックス */
int right_fork_id; /* 右フォークインデックス */
int meals_eaten; /* 食事回数 */
long last_meal_time; /* 最終食事時刻 (ms) */
pthread_t thread; /* スレッドハンドル */
t_data *data; /* グローバルデータへの参照 */
};
/*
** グローバルデータ構造体
** - 全スレッドで共有される情報
** - ミューテックスで保護が必要なフィールドあり
*/
struct s_data
{
/* 設定値(読み取り専用) */
int num_philos;
long time_to_die;
long time_to_eat;
long time_to_sleep;
int must_eat_count;
/* 実行時状態(ミューテックス保護要) */
long start_time;
int simulation_end;
/* 同期プリミティブ */
pthread_mutex_t *forks;
pthread_mutex_t print_mutex;
pthread_mutex_t state_mutex;
pthread_mutex_t meal_mutex;
/* 哲学者配列 */
t_philo *philos;
};
/*
** 関数プロトタイプ: init.c
*/
int init_all(t_data *data, int argc, char **argv);
int parse_arguments(t_data *data, int argc, char **argv);
int init_mutexes(t_data *data);
int init_philosophers(t_data *data);
/*
** 関数プロトタイプ: routine.c
*/
void *philosopher_routine(void *arg);
int take_forks(t_philo *philo);
void release_forks(t_philo *philo);
/*
** 関数プロトタイプ: actions.c
*/
void philo_eat(t_philo *philo);
void philo_sleep(t_philo *philo);
void philo_think(t_philo *philo);
/*
** 関数プロトタイプ: monitor.c
*/
void *monitor_routine(void *arg);
int check_death(t_data *data);
void set_simulation_end(t_data *data);
/*
** 関数プロトタイプ: time.c
*/
long get_timestamp_ms(void);
void precise_sleep(long duration_ms);
/*
** 関数プロトタイプ: utils.c
*/
int ft_atoi_safe(const char *str, int *result);
void print_status(t_philo *philo, const char *status);
void print_death(t_philo *philo);
int error_exit(const char *msg);
/*
** 関数プロトタイプ: cleanup.c
*/
void cleanup_all(t_data *data);
void destroy_mutexes(t_data *data);
void join_threads(t_data *data);
#endif
ソースファイルの構成
philo/
├── Makefile
├── includes/
│ └── philo.h
└── srcs/
├── main.c # エントリーポイント
├── init.c # 初期化関数
├── routine.c # 哲学者スレッドルーチン
├── actions.c # 個別アクション(eat, sleep, think)
├── monitor.c # 監視スレッド
├── time.c # 時間関連ユーティリティ
├── utils.c # 汎用ユーティリティ
└── cleanup.c # リソース解放
5.6 初期化の実装
メイン関数
/* main.c */
#include "philo.h"
int main(int argc, char **argv)
{
t_data data;
/* 構造体をゼロ初期化 */
memset(&data, 0, sizeof(t_data));
/* 全体初期化 */
if (init_all(&data, argc, argv) != SUCCESS)
return (EXIT_FAILURE);
/* スレッド作成・実行 */
if (start_simulation(&data) != SUCCESS)
{
cleanup_all(&data);
return (EXIT_FAILURE);
}
/* 監視(メインスレッドで実行) */
monitor_simulation(&data);
/* クリーンアップ */
cleanup_all(&data);
return (EXIT_SUCCESS);
}
引数パース
/* init.c */
/*
** 安全な文字列→整数変換
** オーバーフローと不正な文字をチェック
*/
int ft_atoi_safe(const char *str, int *result)
{
long value;
int i;
int sign;
value = 0;
sign = 1;
i = 0;
/* 空白スキップ */
while (str[i] == ' ' || (str[i] >= 9 && str[i] <= 13))
i++;
/* 符号処理 */
if (str[i] == '-' || str[i] == '+')
{
if (str[i] == '-')
sign = -1;
i++;
}
/* 数字以外の文字チェック */
if (str[i] == '\0')
return (FAILURE);
/* 数値変換(オーバーフローチェック付き) */
while (str[i])
{
if (str[i] < '0' || str[i] > '9')
return (FAILURE);
value = value * 10 + (str[i] - '0');
if (value > INT_MAX)
return (FAILURE);
i++;
}
*result = (int)(value * sign);
return (SUCCESS);
}
int parse_arguments(t_data *data, int argc, char **argv)
{
int value;
if (argc < 5 || argc > 6)
return (error_exit("Usage: ./philo num time_die time_eat time_sleep [must_eat]"));
if (ft_atoi_safe(argv[1], &value) != SUCCESS || value <= 0)
return (error_exit("Invalid number of philosophers"));
data->num_philos = value;
if (ft_atoi_safe(argv[2], &value) != SUCCESS || value <= 0)
return (error_exit("Invalid time_to_die"));
data->time_to_die = value;
if (ft_atoi_safe(argv[3], &value) != SUCCESS || value <= 0)
return (error_exit("Invalid time_to_eat"));
data->time_to_eat = value;
if (ft_atoi_safe(argv[4], &value) != SUCCESS || value <= 0)
return (error_exit("Invalid time_to_sleep"));
data->time_to_sleep = value;
if (argc == 6)
{
if (ft_atoi_safe(argv[5], &value) != SUCCESS || value <= 0)
return (error_exit("Invalid must_eat_count"));
data->must_eat_count = value;
}
else
data->must_eat_count = -1;
return (SUCCESS);
}
ミューテックス初期化
int init_mutexes(t_data *data)
{
int i;
/* フォーク配列のメモリ確保 */
data->forks = malloc(sizeof(pthread_mutex_t) * data->num_philos);
if (!data->forks)
return (FAILURE);
/* 各フォークのミューテックス初期化 */
i = 0;
while (i < data->num_philos)
{
if (pthread_mutex_init(&data->forks[i], NULL) != 0)
{
/* エラー: 既に初期化したものを破棄 */
while (--i >= 0)
pthread_mutex_destroy(&data->forks[i]);
free(data->forks);
data->forks = NULL;
return (FAILURE);
}
i++;
}
/* 出力保護用ミューテックス */
if (pthread_mutex_init(&data->print_mutex, NULL) != 0)
{
destroy_fork_mutexes(data);
return (FAILURE);
}
/* 状態保護用ミューテックス */
if (pthread_mutex_init(&data->state_mutex, NULL) != 0)
{
pthread_mutex_destroy(&data->print_mutex);
destroy_fork_mutexes(data);
return (FAILURE);
}
/* 食事時刻保護用ミューテックス */
if (pthread_mutex_init(&data->meal_mutex, NULL) != 0)
{
pthread_mutex_destroy(&data->state_mutex);
pthread_mutex_destroy(&data->print_mutex);
destroy_fork_mutexes(data);
return (FAILURE);
}
return (SUCCESS);
}
哲学者初期化
int init_philosophers(t_data *data)
{
int i;
data->philos = malloc(sizeof(t_philo) * data->num_philos);
if (!data->philos)
return (FAILURE);
i = 0;
while (i < data->num_philos)
{
data->philos[i].id = i + 1;
data->philos[i].left_fork_id = i;
data->philos[i].right_fork_id = (i + 1) % data->num_philos;
data->philos[i].meals_eaten = 0;
data->philos[i].last_meal_time = data->start_time;
data->philos[i].data = data;
i++;
}
return (SUCCESS);
}
5.7 哲学者ルーチンの実装
メインループ
/* routine.c */
void *philosopher_routine(void *arg)
{
t_philo *philo;
philo = (t_philo *)arg;
/* 偶数IDは開始を遅延(デッドロック回避) */
if (philo->id % 2 == 0)
precise_sleep(philo->data->time_to_eat / 2);
while (!check_simulation_end(philo->data))
{
/* 思考 */
philo_think(philo);
/* フォーク取得 */
if (!take_forks(philo))
break;
/* 食事 */
philo_eat(philo);
/* フォーク返却 */
release_forks(philo);
/* 必要回数達成チェック */
if (check_meals_complete(philo))
break;
/* 睡眠 */
philo_sleep(philo);
}
return (NULL);
}
フォーク取得(デッドロック回避)
int take_forks(t_philo *philo)
{
int first, second;
/* リソース順序付けによるデッドロック回避 */
if (philo->left_fork_id < philo->right_fork_id)
{
first = philo->left_fork_id;
second = philo->right_fork_id;
}
else
{
first = philo->right_fork_id;
second = philo->left_fork_id;
}
/* 1本目のフォーク取得 */
pthread_mutex_lock(&philo->data->forks[first]);
if (check_simulation_end(philo->data))
{
pthread_mutex_unlock(&philo->data->forks[first]);
return (FALSE);
}
print_status(philo, "has taken a fork");
/* 1人の場合の特殊処理 */
if (philo->data->num_philos == 1)
{
precise_sleep(philo->data->time_to_die + 10);
pthread_mutex_unlock(&philo->data->forks[first]);
return (FALSE);
}
/* 2本目のフォーク取得 */
pthread_mutex_lock(&philo->data->forks[second]);
if (check_simulation_end(philo->data))
{
pthread_mutex_unlock(&philo->data->forks[second]);
pthread_mutex_unlock(&philo->data->forks[first]);
return (FALSE);
}
print_status(philo, "has taken a fork");
return (TRUE);
}
void release_forks(t_philo *philo)
{
pthread_mutex_unlock(&philo->data->forks[philo->left_fork_id]);
pthread_mutex_unlock(&philo->data->forks[philo->right_fork_id]);
}
行動の実装
/* actions.c */
void philo_think(t_philo *philo)
{
print_status(philo, "is thinking");
}
void philo_eat(t_philo *philo)
{
print_status(philo, "is eating");
/* 食事時刻を更新(ミューテックス保護) */
pthread_mutex_lock(&philo->data->meal_mutex);
philo->last_meal_time = get_timestamp_ms();
philo->meals_eaten++;
pthread_mutex_unlock(&philo->data->meal_mutex);
/* 食事時間待機 */
precise_sleep(philo->data->time_to_eat);
}
void philo_sleep(t_philo *philo)
{
print_status(philo, "is sleeping");
precise_sleep(philo->data->time_to_sleep);
}
5.8 監視スレッドの実装
死亡監視ループ
/* monitor.c */
void *monitor_routine(void *arg)
{
t_data *data;
int i;
data = (t_data *)arg;
while (!check_simulation_end(data))
{
i = 0;
while (i < data->num_philos)
{
if (is_philosopher_dead(&data->philos[i]))
{
print_death(&data->philos[i]);
set_simulation_end(data);
return (NULL);
}
i++;
}
/* 全員が必要回数食べたかチェック */
if (all_philosophers_finished(data))
{
set_simulation_end(data);
return (NULL);
}
usleep(1000); /* 1ms間隔でチェック */
}
return (NULL);
}
死亡判定
int is_philosopher_dead(t_philo *philo)
{
long current_time;
long last_meal;
long elapsed;
current_time = get_timestamp_ms();
pthread_mutex_lock(&philo->data->meal_mutex);
last_meal = philo->last_meal_time;
pthread_mutex_unlock(&philo->data->meal_mutex);
elapsed = current_time - last_meal;
return (elapsed > philo->data->time_to_die);
}
シミュレーション状態管理
int check_simulation_end(t_data *data)
{
int result;
pthread_mutex_lock(&data->state_mutex);
result = data->simulation_end;
pthread_mutex_unlock(&data->state_mutex);
return (result);
}
void set_simulation_end(t_data *data)
{
pthread_mutex_lock(&data->state_mutex);
data->simulation_end = TRUE;
pthread_mutex_unlock(&data->state_mutex);
}
5.9 時間管理の実装
タイムスタンプ取得
/* time.c */
long get_timestamp_ms(void)
{
struct timeval tv;
gettimeofday(&tv, NULL);
return ((tv.tv_sec * 1000L) + (tv.tv_usec / 1000L));
}
精密スリープ
void precise_sleep(long duration_ms)
{
long start;
long elapsed;
long remaining;
start = get_timestamp_ms();
while (TRUE)
{
elapsed = get_timestamp_ms() - start;
remaining = duration_ms - elapsed;
if (remaining <= 0)
break;
/* 適応的スリープ: 残り時間に応じて調整 */
if (remaining > 10)
usleep(5000); /* 残り10ms以上: 5ms sleep */
else if (remaining > 2)
usleep(500); /* 残り2-10ms: 0.5ms sleep */
else
usleep(100); /* 残り2ms未満: 0.1ms sleep */
}
}
5.10 出力とユーティリティ
スレッドセーフな出力
/* utils.c */
void print_status(t_philo *philo, const char *status)
{
long timestamp;
pthread_mutex_lock(&philo->data->print_mutex);
if (!check_simulation_end(philo->data))
{
timestamp = get_timestamp_ms() - philo->data->start_time;
printf("%ld %d %s\n", timestamp, philo->id, status);
}
pthread_mutex_unlock(&philo->data->print_mutex);
}
void print_death(t_philo *philo)
{
long timestamp;
pthread_mutex_lock(&philo->data->print_mutex);
timestamp = get_timestamp_ms() - philo->data->start_time;
printf("%ld %d died\n", timestamp, philo->id);
pthread_mutex_unlock(&philo->data->print_mutex);
}
エラー処理
int error_exit(const char *msg)
{
write(STDERR_FILENO, "Error: ", 7);
write(STDERR_FILENO, msg, strlen(msg));
write(STDERR_FILENO, "\n", 1);
return (FAILURE);
}
5.11 クリーンアップ
リソース解放
/* cleanup.c */
void cleanup_all(t_data *data)
{
/* スレッドの終了を待機 */
join_threads(data);
/* ミューテックスを破棄 */
destroy_mutexes(data);
/* メモリを解放 */
if (data->philos)
{
free(data->philos);
data->philos = NULL;
}
if (data->forks)
{
free(data->forks);
data->forks = NULL;
}
}
void destroy_mutexes(t_data *data)
{
int i;
if (data->forks)
{
i = 0;
while (i < data->num_philos)
{
pthread_mutex_destroy(&data->forks[i]);
i++;
}
}
pthread_mutex_destroy(&data->print_mutex);
pthread_mutex_destroy(&data->state_mutex);
pthread_mutex_destroy(&data->meal_mutex);
}
void join_threads(t_data *data)
{
int i;
if (!data->philos)
return;
i = 0;
while (i < data->num_philos)
{
pthread_join(data->philos[i].thread, NULL);
i++;
}
}
5.12 まとめ
本章では、ソフトウェアアーキテクチャと実装戦略について学んだ:
- アーキテクチャ原則: Parnasの情報隠蔽、UNIXの設計哲学
- 結合度と凝集度: モジュール設計の品質指標
- データ構造設計: キャッシュ局所性、メモリアラインメント
- 時間管理: POSIX時間関数、精密スリープ実装
- 防御的プログラミング: 入力検証、エラー処理
- 実装パターン: 初期化、ルーチン、監視、クリーンアップ
- Parnas, D. L. (1972). "On the Criteria To Be Used in Decomposing Systems into Modules", Communications of the ACM, 15(12)
- Constantine, L. L. & Yourdon, E. (1979). "Structured Design", Prentice-Hall
- McConnell, S. (2004). "Code Complete", 2nd Edition, Microsoft Press
- Hennessy, J. L. & Patterson, D. A. (1990). "Computer Architecture: A Quantitative Approach", Morgan Kaufmann
- McIlroy, M. D. (1978). "UNIX Time-Sharing System: Foreword", The Bell System Technical Journal
- IEEE (1993). "POSIX.1b-1993: Realtime Extension", IEEE Std 1003.1b-1993
- Drepper, U. (2007). "What Every Programmer Should Know About Memory", Red Hat Inc.
次章では、デバッグとテスト手法について学ぶ。
---