C言語を学び始めると、標準ライブラリ(stdio.h, stdlib.h, string.hなど)の便利さに気づきます。しかし、その内部で何が起きているのかを意識することは少ないかもしれません。
そこで私は、C言語の標準関数のゼロから再実装に取り組みました。
(通っている学校の課題です。笑)
このプロセスを通じて得た、テクニックやアルゴリズムをいくつかピックアップして紹介します。
1. メモリブロックへの「メタデータ」埋め込みによる ft_realloc の実装
標準の realloc は、元のサイズを指定せずにメモリを拡張できます。これを自作する場合、「そのポインタが指すメモリが何バイト確保されているか」をどこかに保存しておく必要があります。
これを実現するために、メモリブロックの直前にメタデータを隠すテクニックを使用します。
実装の核心: ft_malloc と ft_realloc
// ft_malloc.c
void *ft_malloc(size_t size)
{
size_t *block;
// 実データの前に size_t 分のヘッダ領域を確保
block = malloc(sizeof(size_t) + size);
if (!block) return (NULL);
*block = size; // ヘッダにサイズを書き込む
return ((void *)(block + 1)); // ヘッダの直後(実データ領域)を返す
}
// ft_realloc.c
void *ft_realloc(void *ptr, size_t new_size)
{
// ヘッダにアクセスして旧サイズを復元
size_t old_size = *(((size_t *)ptr) - 1);
void *new_ptr = ft_malloc(new_size);
if (!new_ptr) return (NULL);
// 小さい方のサイズ分だけコピー
size_t copy_size = (old_size < new_size) ? old_size : new_size;
ft_memcpy(new_ptr, ptr, copy_size);
// ヘッダごと解放(実装内では ft_free_ でラップ)
free(((size_t *)ptr) - 1);
return (new_ptr);
}
ポイント:
-
ポインタ演算の妙:
block + 1はsize_tのバイト数分だけアドレスを進めます。 -
アライメントの考慮: 本来は
max_align_tなどを考慮してヘッダサイズを調整すべきですが、size_t単位でのオフセットは、モダンな64bit環境における基本的なアライメント要求を最低限満たします。
2. 線形リストの動的管理と「ポインタのポインタ」の威力
C言語における線形リスト(連結リスト)の実装は、動的メモリ確保とポインタ操作の格好の練習台です。特に、リストの先頭を書き換える可能性がある操作では、ポインタのポインタの理解が不可欠です。
線形リストとは ↓
構造体定義と基本操作
typedef struct s_list
{
void *content; // 汎用的なデータ
struct s_list *next; // 次のノードへのポインタ
} t_list;
// リストの末尾に要素を追加する
void ft_lstadd_back(t_list **lst, t_list *new)
{
t_list *last;
if (!lst || !new) return;
if (*lst == NULL) // リストが空の場合
{
*lst = new; // 先頭(呼び出し元の変数)を直接書き換える
return;
}
last = ft_lstlast(*lst);
last->next = new;
}
ポイント:
-
なぜ
t_list **なのか?: リストが空の時、先頭ノードをnewに設定する必要があります。t_list *で渡してしまうと、関数内での書き換えが呼び出し元に反映されません。 -
再帰と反復の選択:
ft_lstclear(全削除)などの実装では、スタックオーバーフローのリスクを避けるため、大規模なリストを想定して反復(whileループ)を選択するのがシステムプログラミングの定石です。
3. 関数ポインタによる「高階関数」の抽象化 (ft_lstmap)
C言語でも、関数ポインタを駆使すれば、関数型言語のような map 処理が可能です。
実装例: ft_lstmap
t_list *ft_lstmap(t_list *lst, void *(*f)(void *), void (*del)(void *))
{
t_list *new_list = NULL;
t_list *new_node;
while (lst)
{
new_node = ft_lstnew(f(lst->content));
if (!new_node)
{
ft_lstclear(&new_list, del); // 失敗時の全破棄(アトミックな振る舞い)
return (NULL);
}
ft_lstadd_back(&new_list, new_node);
lst = lst->next;
}
return (new_list);
}
ポイント:
-
不完全な状態を残さない: 処理の途中で1つでもメモリ確保に失敗すれば、それまでの全ノードを
del関数で解放してからNULLを返す。この「副作用の管理」がライブラリの堅牢性を高めます。
4. オーバーフローを完璧に検知する ft_atoll
atoi の long long 版である ft_atoll。文字列を数値に変換する際、LLONG_MAX や LLONG_MIN を超える場合の処理は非常に重要です。
判定のロジック
while (ft_isdigit(str[i]))
{
// 次の計算でオーバーフローするかを、計算前にチェック
if (sign == 1 && (result > LLONG_MAX / 10
|| (result == LLONG_MAX / 10
&& (str[i] - '0') > LLONG_MAX % 10)))
return (LLONG_MAX);
// ... 負の値も同様にチェック ...
result = result * 10 + (str[i++] - '0');
}
計算結果が出てからチェックするのではなく、「次の計算をしたら溢れるか?」を判定するのがプロの技です。
5. 2進演算を意識したオーバーフローチェックの理論
数値変換において (result * 10 + digit) がオーバーフローするかを事前に判定するロジックは、未定義動作(UB)を避けるための鉄則です。
if (result > LLONG_MAX / 10 ||
(result == LLONG_MAX / 10 && digit > LLONG_MAX % 10))
return (OVERFLOW);
専門的な視点:
- 演算前判定の重要性: 符号付き整数のオーバーフローはC言語では未定義動作です。そのため、結果が出てからチェックするのではなく、溢れる前に止めるのが、安全性が最優先されるシステムプログラミングの極意です。
まとめ
この実装を通じて学んだのは、単に「動くものを作る」ことではなく、あらゆる例外状況を想定し、メモリを1バイトも無駄にしないというエンジニアとしての執念です。
低レイヤの理解は、どんな高レベルな言語(Python, Go, TypeScriptなど)を扱う上でも、パフォーマンス最適化やデバッグ能力の土台となります。
もしこの記事が、皆さんのC言語学習や、堅牢なコード設計のヒントになれば幸いです。
この記事は、42Tokyoでの学習成果の一部をまとめたものです。