第3章:文字列理論とアルゴリズムの世界

3.1 形式言語理論への入門

3.1.1 言語とは何か - 数学的定義

コンピュータサイエンスにおいて、「言語」は日常会話で使う意味とは異なる、厳密な数学的概念です。1950年代、ノーム・チョムスキー(Noam Chomsky)は言語学者として、人間の言語を形式的に記述しようとしました。その成果は、コンピュータサイエンスに革命をもたらしました。

アルファベット(Alphabet)

アルファベット Σ は、有限個の記号の集合です。

Σ = {0, 1}                    // 2進アルファベット
Σ = {a, b, c, ..., z}        // 英小文字アルファベット
Σ = {A, C, G, T}             // DNAアルファベット
Σ = {0, 1, 2, ..., 9, A, ..., F}  // 16進アルファベット

文字列(String)

アルファベット Σ 上の文字列は、Σ の要素を有限個並べたものです。

Σ = {0, 1} のとき:
- ε(空文字列)
- 0, 1
- 00, 01, 10, 11
- 000, 001, 010, 011, 100, 101, 110, 111
- ...(無限に続く)

空文字列 ε は、長さ 0 の特殊な文字列です。どんなアルファベットにも、空文字列が存在します。

言語(Language)

言語 L は、アルファベット Σ 上の文字列の集合です。

L₁ = {ε}                      // 空文字列のみを含む言語
L₂ = {}                       // 空集合(いかなる文字列も含まない)
L₃ = {0, 00, 000, ...}        // 0のみからなる文字列全体
L₄ = {w | |w| ≤ 3}           // 長さ3以下のすべての文字列

3.1.2 チョムスキー階層

チョムスキーは1956年、言語を生成する文法の複雑さに基づいて、4つの階層を定義しました。この分類は、計算可能性理論とオートマトン理論の基礎となっています。

Type 3: 正規言語(Regular Languages)

最も単純な言語クラスです。有限オートマトンで認識でき、正規表現で記述できます。

例:
- 偶数個の0を含む2進文字列
- 特定のパターンで始まる/終わる文字列
- 電子メールアドレスのパターン(近似的に)

正規表現の例:

[a-z]+@[a-z]+\.[a-z]{2,}    // 簡易的なメールアドレス
0*10*                        // ちょうど1つの1を含む
(ab)*                        // abの0回以上の繰り返し

Type 2: 文脈自由言語(Context-Free Languages)

プッシュダウンオートマトンで認識できます。プログラミング言語の構文のほとんどがこのクラスに属します。

例:
- 対応する括弧の列 { ()()(()), ((())) }
- HTML/XMLの入れ子構造
- 算術式 { 1+2*(3-4) }

文脈自由文法の例(算術式):

E → E + T | E - T | T
T → T * F | T / F | F
F → ( E ) | number

Type 1: 文脈依存言語(Context-Sensitive Languages)

線形束縛オートマトンで認識できます。自然言語の多くの特徴がこのクラスに含まれます。

例:
- {aⁿbⁿcⁿ | n ≥ 1}  // 同数のa,b,cが順番に並ぶ

Type 0: 再帰可枚挙言語(Recursively Enumerable Languages)

チューリングマシンで認識できる最も一般的なクラスです。

包含関係:
正規言語 ⊂ 文脈自由言語 ⊂ 文脈依存言語 ⊂ 再帰可枚挙言語

3.1.3 オートマトン理論の基礎

有限オートマトン(Finite Automaton)

有限オートマトンは、有限個の状態を持つ抽象的な計算機械です。入力を1文字ずつ読み、状態を遷移させ、最終的に受理または拒否を判定します。

決定性有限オートマトン(DFA)の形式的定義:

DFA M = (Q, Σ, δ, q₀, F) ここで:
- Q: 状態の有限集合
- Σ: 入力アルファベット
- δ: Q × Σ → Q(遷移関数)
- q₀ ∈ Q: 初期状態
- F ⊆ Q: 受理状態の集合

例:偶数個の1を含む2進文字列を受理するDFA

状態:q₀(偶数個、初期状態、受理状態)、q₁(奇数個)

遷移表:
     |  0  |  1
-----|-----|-----
 q₀  | q₀  | q₁
 q₁  | q₁  | q₀

図式化:
![DFA Even Ones](/Users/hackhike/.gemini/antigravity/brain/bc43455d-719e-4889-8da1-eb68d30dc6a7/dfa_even_ones_1764871134442.png)

C言語でのDFA実装:

typedef enum {
    STATE_EVEN,  // 偶数個の1(受理状態)
    STATE_ODD    // 奇数個の1
} State;

typedef struct {
    State current_state;
    State transition[2][2];  // transition[state][input]
    int accept[2];           // 各状態が受理状態かどうか
} DFA;

void init_even_ones_dfa(DFA *dfa)
{
    dfa->current_state = STATE_EVEN;

    // 遷移関数の定義
    dfa->transition[STATE_EVEN][0] = STATE_EVEN;
    dfa->transition[STATE_EVEN][1] = STATE_ODD;
    dfa->transition[STATE_ODD][0] = STATE_ODD;
    dfa->transition[STATE_ODD][1] = STATE_EVEN;

    // 受理状態
    dfa->accept[STATE_EVEN] = 1;
    dfa->accept[STATE_ODD] = 0;
}

void process_input(DFA *dfa, char input)
{
    int symbol = input - '0';  // '0' → 0, '1' → 1
    dfa->current_state = dfa->transition[dfa->current_state][symbol];
}

int is_accepted(DFA *dfa)
{
    return dfa->accept[dfa->current_state];
}

// 使用例
int main()
{
    DFA dfa;
    init_even_ones_dfa(&dfa);

    const char *input = "110100";  // 1が3個(奇数)

    for (int i = 0; input[i]; i++)
        process_input(&dfa, input[i]);

    if (is_accepted(&dfa))
        printf("Accepted: even number of 1s\n");
    else
        printf("Rejected: odd number of 1s\n");

    return 0;
}

3.1.4 正規表現の理論と実践

1956年、スティーブン・クリーネ(Stephen Kleene)は正規言語を記述する代数的記法を発明しました。これが正規表現の起源です。

正規表現の基本演算

  • 連接(Concatenation): 二つの文字列を連結
- ab は "a" の後に "b" が続く

  • 和(Union/Alternation): いずれかにマッチ
- a|b は "a" または "b"

  • クリーネ閉包(Kleene Star): 0回以上の繰り返し
- a* は "", "a", "aa", "aaa", ...

クリーネの定理

正規言語は以下の3つの記述が等価です:

  • 正規表現で記述できる
  • 決定性有限オートマトン(DFA)で認識できる
  • 非決定性有限オートマトン(NFA)で認識できる
  • 正規表現からNFAへの変換(Thompson構成法)

    基本要素:
    
    ![NFA Regex Construction](/Users/hackhike/.gemini/antigravity/brain/bc43455d-719e-4889-8da1-eb68d30dc6a7/nfa_regex_construction_1764871157357.png)
    

    C言語での簡易正規表現マッチャー:

    // 簡易正規表現マッチャー(ワイルドカードのみ)
    // '.' は任意の1文字、'*' は直前の文字の0回以上の繰り返し
    
    int match_here(const char *regexp, const char *text);
    
    int match_star(int c, const char *regexp, const char *text)
    {
        // '*'の前の文字cの0回以上の繰り返しをマッチ
        do {
            if (match_here(regexp, text))
                return 1;
        } while (*text != '\0' && (*text++ == c || c == '.'));
    
        return 0;
    }
    
    int match_here(const char *regexp, const char *text)
    {
        // 正規表現が終端 → マッチ成功
        if (regexp[0] == '\0')
            return 1;
    
        // 正規表現が'

    3.2 文字列アルゴリズムの歴史と理論

    3.2.1 文字列探索問題の定義

    文字列探索(String Searching/Pattern Matching)問題は、コンピュータサイエンスで最も基本的かつ重要な問題の一つです。

    問題の定式化

    入力:
    - テキスト T[0..n-1](長さ n)
    - パターン P[0..m-1](長さ m、通常 m ≤ n)
    
    出力:
    - T 中で P が出現するすべての位置
    - または、最初に出現する位置
    - または、出現するかどうかの判定
    

    応用分野

  • テキストエディタ: 検索・置換機能
  • Webブラウザ: ページ内検索
  • バイオインフォマティクス: DNA/タンパク質配列解析
  • ネットワークセキュリティ: パケット検査、ウイルススキャン
  • データベース: LIKE句の実装
  • コンパイラ: 字句解析
  • 3.2.2 ナイーブアルゴリズム(Brute Force)

    最も単純なアプローチから始めましょう。

    // ナイーブ法:O(nm)
    int naive_search(const char *text, const char *pattern)
    {
        int n = strlen(text);
        int m = strlen(pattern);
    
        for (int i = 0; i <= n - m; i++)
        {
            int j = 0;
    
            // パターンとの比較
            while (j < m && text[i + j] == pattern[j])
                j++;
    
            if (j == m)
                return i;  // マッチ発見
        }
    
        return -1;  // マッチなし
    }
    

    計算量分析

  • 最悪ケース: O(nm)
- 例: text = "AAAAAAAAAB", pattern = "AAAAB" - 各位置で m 回の比較が必要

  • 平均ケース: O(n)
- ランダムなテキストでは、ミスマッチがすぐに見つかる

3.2.3 Knuth-Morris-Pratt(KMP)アルゴリズム

1977年、ドナルド・クヌース(Donald Knuth)、ジェームズ・モリス(James Morris)、ヴォーン・プラット(Vaughan Pratt)が独立に発見し、共同で発表した画期的なアルゴリズムです。

核心的なアイデア

ミスマッチが発生したとき、すでに比較した情報を活用して、無駄な比較をスキップします。

例: text = "ABABABCA", pattern = "ABABC"

ナイーブ法:
Position 0: ABABABCA
            ABABC    ← 4文字マッチ後、ミスマッチ
            ↓ 1文字進める
Position 1: ABABABCA
             ABABC   ← 最初からやり直し

![KMP Failure Function](/Users/hackhike/.gemini/antigravity/brain/bc43455d-719e-4889-8da1-eb68d30dc6a7/kmp_failure_function_1764871179579.png)

失敗関数(Failure Function)の計算

失敗関数 π[j] は、pattern[0..j] の最長の「真の接頭辞かつ接尾辞」の長さを表します。

// 失敗関数の計算:O(m)
void compute_failure_function(const char *pattern, int *failure)
{
    int m = strlen(pattern);
    failure[0] = 0;

    int k = 0;  // 最長の接頭辞=接尾辞の長さ

    for (int i = 1; i < m; i++)
    {
        // ミスマッチの場合、より短い接頭辞=接尾辞を探す
        while (k > 0 && pattern[k] != pattern[i])
            k = failure[k - 1];

        // マッチした場合
        if (pattern[k] == pattern[i])
            k++;

        failure[i] = k;
    }
}

// 失敗関数の例
// pattern = "ABABC"
// failure = [0, 0, 1, 2, 0]
//
// i=0: "A"       → 真の接頭辞=接尾辞なし → 0
// i=1: "AB"      → 真の接頭辞=接尾辞なし → 0
// i=2: "ABA"     → "A"が接頭辞かつ接尾辞 → 1
// i=3: "ABAB"    → "AB"が接頭辞かつ接尾辞 → 2
// i=4: "ABABC"   → 真の接頭辞=接尾辞なし → 0

KMPアルゴリズムの完全実装

typedef struct {
    int *positions;
    int count;
    int capacity;
} MatchResult;

MatchResult *kmp_search(const char *text, const char *pattern)
{
    int n = strlen(text);
    int m = strlen(pattern);

    // 結果を格納する構造体
    MatchResult *result = malloc(sizeof(MatchResult));
    result->positions = malloc(sizeof(int) * (n / m + 1));
    result->count = 0;
    result->capacity = n / m + 1;

    if (m == 0)
        return result;

    // 失敗関数の計算
    int *failure = malloc(sizeof(int) * m);
    compute_failure_function(pattern, failure);

    int j = 0;  // パターン内の現在位置

    for (int i = 0; i < n; i++)
    {
        // ミスマッチの場合、失敗関数を使用
        while (j > 0 && pattern[j] != text[i])
            j = failure[j - 1];

        // マッチした場合
        if (pattern[j] == text[i])
            j++;

        // パターン全体がマッチ
        if (j == m)
        {
            result->positions[result->count++] = i - m + 1;
            j = failure[j - 1];  // 次のマッチを探す
        }
    }

    free(failure);
    return result;
}

計算量

  • 前処理(失敗関数): O(m)
  • 検索: O(n)
  • 合計: O(n + m)
  • 3.2.4 Boyer-Moore アルゴリズム

    1977年、ロバート・ボイヤー(Robert Boyer)とJ・ストロザー・ムーア(J Strother Moore)が発表した、実用上最も高速な文字列検索アルゴリズムの一つです。

    革新的なアイデア

  • 後ろから比較: パターンを右から左に比較する
  • Bad Character Rule: ミスマッチ文字を利用してスキップ
  • Good Suffix Rule: マッチした部分を利用してスキップ
  • 例: text = "TRUSTHARDWORKNOT", pattern = "HARDWORK"
    
    ![Boyer-Moore Shift](/Users/hackhike/.gemini/antigravity/brain/bc43455d-719e-4889-8da1-eb68d30dc6a7/boyer_moore_shift_1764871202764.png)
    

    Bad Character Rule の実装

    void compute_bad_char(const char *pattern, int bad_char[256])
    {
        int m = strlen(pattern);
    
        // すべての文字をパターン長に初期化
        for (int i = 0; i < 256; i++)
            bad_char[i] = m;
    
        // パターン中の各文字の最右出現位置を記録
        for (int i = 0; i < m - 1; i++)
            bad_char[(unsigned char)pattern[i]] = m - 1 - i;
    }
    

    Good Suffix Rule の実装

    void compute_good_suffix(const char *pattern, int *good_suffix)
    {
        int m = strlen(pattern);
        int *suffix = malloc(sizeof(int) * m);
    
        // suffix[i] = pattern[i..m-1]とpatternの最長共通接尾辞の長さ
        suffix[m - 1] = m;
        int g = m - 1;
        int f = 0;
    
        for (int i = m - 2; i >= 0; i--)
        {
            if (i > g && suffix[i + m - 1 - f] < i - g)
            {
                suffix[i] = suffix[i + m - 1 - f];
            }
            else
            {
                if (i < g)
                    g = i;
                f = i;
                while (g >= 0 && pattern[g] == pattern[g + m - 1 - f])
                    g--;
                suffix[i] = f - g;
            }
        }
    
        // good_suffix テーブルの構築
        for (int i = 0; i < m; i++)
            good_suffix[i] = m;
    
        int j = 0;
        for (int i = m - 1; i >= 0; i--)
        {
            if (suffix[i] == i + 1)
            {
                while (j < m - 1 - i)
                {
                    if (good_suffix[j] == m)
                        good_suffix[j] = m - 1 - i;
                    j++;
                }
            }
        }
    
        for (int i = 0; i < m - 1; i++)
            good_suffix[m - 1 - suffix[i]] = m - 1 - i;
    
        free(suffix);
    }
    

    Boyer-Moore 完全実装

    int boyer_moore_search(const char *text, const char *pattern)
    {
        int n = strlen(text);
        int m = strlen(pattern);
    
        if (m == 0)
            return 0;
        if (m > n)
            return -1;
    
        // 前処理
        int bad_char[256];
        int *good_suffix = malloc(sizeof(int) * m);
    
        compute_bad_char(pattern, bad_char);
        compute_good_suffix(pattern, good_suffix);
    
        int i = 0;  // テキスト内の位置
    
        while (i <= n - m)
        {
            int j = m - 1;  // パターン内の位置(右端から)
    
            // 後ろから比較
            while (j >= 0 && pattern[j] == text[i + j])
                j--;
    
            if (j < 0)
            {
                // マッチ発見
                free(good_suffix);
                return i;
            }
    
            // シフト量を計算
            int bc_shift = bad_char[(unsigned char)text[i + j]] - (m - 1 - j);
            int gs_shift = good_suffix[j];
    
            // より大きいシフトを採用
            i += (bc_shift > gs_shift) ? bc_shift : gs_shift;
        }
    
        free(good_suffix);
        return -1;
    }
    

    計算量

  • 前処理: O(m + |Σ|)
  • 最悪ケース: O(nm)
  • 平均・最良ケース: O(n/m) - サブリニア!
  • 3.2.5 Rabin-Karp アルゴリズム

    1987年、マイケル・ラビン(Michael Rabin)とリチャード・カープ(Richard Karp)が発表した、ハッシュ関数を利用したアルゴリズムです。

    アイデア

    パターンのハッシュ値を計算し、テキストの各位置のハッシュ値と比較します。ローリングハッシュ(Rolling Hash)により、効率的に次の位置のハッシュを計算できます。

    ローリングハッシュ

    base = 256 (または文字集合のサイズ)
    prime = 大きな素数 (例: 101, 1000000007)
    
    hash("abc") = (a × base² + b × base¹ + c × base⁰) mod prime
    
    hash("bcd") = hash("abc")から計算:
                = (hash("abc") - a × base²) × base + d
    

    実装

    #define BASE 256
    #define PRIME 1000000007
    
    long long compute_hash(const char *str, int len)
    {
        long long hash = 0;
        for (int i = 0; i < len; i++)
        {
            hash = (hash * BASE + (unsigned char)str[i]) % PRIME;
        }
        return hash;
    }
    
    // べき乗の計算(モジュラ演算)
    long long power(long long base, int exp, long long mod)
    {
        long long result = 1;
        base %= mod;
        while (exp > 0)
        {
            if (exp & 1)
                result = (result * base) % mod;
            exp >>= 1;
            base = (base * base) % mod;
        }
        return result;
    }
    
    int rabin_karp_search(const char *text, const char *pattern)
    {
        int n = strlen(text);
        int m = strlen(pattern);
    
        if (m > n)
            return -1;
    
        // パターンのハッシュ
        long long pattern_hash = compute_hash(pattern, m);
    
        // 最初のウィンドウのハッシュ
        long long text_hash = compute_hash(text, m);
    
        // base^(m-1) mod PRIME を事前計算
        long long h = power(BASE, m - 1, PRIME);
    
        for (int i = 0; i <= n - m; i++)
        {
            // ハッシュが一致したら実際に比較
            if (text_hash == pattern_hash)
            {
                int j;
                for (j = 0; j < m; j++)
                {
                    if (text[i + j] != pattern[j])
                        break;
                }
                if (j == m)
                    return i;  // 真のマッチ
            }
    
            // ローリングハッシュ
            if (i < n - m)
            {
                text_hash = (BASE * (text_hash - (unsigned char)text[i] * h)
                            + (unsigned char)text[i + m]) % PRIME;
    
                // 負の値を正に補正
                if (text_hash < 0)
                    text_hash += PRIME;
            }
        }
    
        return -1;
    }
    

    複数パターン検索

    Rabin-Karpの真価は、複数のパターンを同時に検索する場合に発揮されます。

    typedef struct {
        const char *pattern;
        long long hash;
    } PatternInfo;
    
    int multi_pattern_search(const char *text, const char **patterns, int num_patterns)
    {
        int n = strlen(text);
    
        // パターンの最短・最長を求める
        int min_len = INT_MAX, max_len = 0;
        for (int i = 0; i < num_patterns; i++)
        {
            int len = strlen(patterns[i]);
            if (len < min_len) min_len = len;
            if (len > max_len) max_len = len;
        }
    
        // 各パターンのハッシュを計算
        PatternInfo *infos = malloc(sizeof(PatternInfo) * num_patterns);
        for (int i = 0; i < num_patterns; i++)
        {
            infos[i].pattern = patterns[i];
            infos[i].hash = compute_hash(patterns[i], strlen(patterns[i]));
        }
    
        // 検索(各長さごとに処理)
        for (int len = min_len; len <= max_len; len++)
        {
            long long text_hash = compute_hash(text, len);
            long long h = power(BASE, len - 1, PRIME);
    
            for (int i = 0; i <= n - len; i++)
            {
                // 同じ長さのパターンと比較
                for (int p = 0; p < num_patterns; p++)
                {
                    if ((int)strlen(patterns[p]) == len &&
                        text_hash == infos[p].hash &&
                        strncmp(text + i, patterns[p], len) == 0)
                    {
                        printf("Pattern '%s' found at position %d\n",
                               patterns[p], i);
                    }
                }
    
                // ローリングハッシュ
                if (i < n - len)
                {
                    text_hash = (BASE * (text_hash - (unsigned char)text[i] * h)
                                + (unsigned char)text[i + len]) % PRIME;
                    if (text_hash < 0)
                        text_hash += PRIME;
                }
            }
        }
    
        free(infos);
        return 0;
    }
    

    3.2.6 Aho-Corasick アルゴリズム

    1975年、アルフレッド・エイホ(Alfred Aho)とマーガレット・コラシック(Margaret Corasick)が発表した、複数パターン同時検索アルゴリズムです。

    構造

  • トライ(Trie): パターンの共通接頭辞を共有する木構造
  • 失敗リンク: KMPの失敗関数を多パターンに一般化
  • 出力リンク: マッチしたパターンを報告
  • #define ALPHABET_SIZE 256
    
    typedef struct AhoCorasickNode {
        struct AhoCorasickNode *children[ALPHABET_SIZE];
        struct AhoCorasickNode *failure;
        struct AhoCorasickNode *output;
        int pattern_index;  // -1 if not a terminal
    } ACNode;
    
    typedef struct {
        ACNode *root;
        const char **patterns;
        int num_patterns;
    } AhoCorasick;
    
    ACNode *create_node()
    {
        ACNode *node = calloc(1, sizeof(ACNode));
        node->pattern_index = -1;
        return node;
    }
    
    void build_trie(AhoCorasick *ac)
    {
        for (int i = 0; i < ac->num_patterns; i++)
        {
            ACNode *current = ac->root;
            const char *pattern = ac->patterns[i];
    
            for (int j = 0; pattern[j]; j++)
            {
                unsigned char c = pattern[j];
    
                if (!current->children[c])
                    current->children[c] = create_node();
    
                current = current->children[c];
            }
    
            current->pattern_index = i;
        }
    }
    
    void build_failure_links(AhoCorasick *ac)
    {
        // BFSのためのキュー
        ACNode **queue = malloc(sizeof(ACNode*) * 10000);
        int front = 0, rear = 0;
    
        // 深さ1のノードの失敗リンクはルート
        for (int c = 0; c < ALPHABET_SIZE; c++)
        {
            if (ac->root->children[c])
            {
                ac->root->children[c]->failure = ac->root;
                queue[rear++] = ac->root->children[c];
            }
        }
    
        // BFSで残りのノードを処理
        while (front < rear)
        {
            ACNode *current = queue[front++];
    
            for (int c = 0; c < ALPHABET_SIZE; c++)
            {
                ACNode *child = current->children[c];
                if (!child)
                    continue;
    
                queue[rear++] = child;
    
                // 失敗リンクを計算
                ACNode *f = current->failure;
                while (f && !f->children[c])
                    f = f->failure;
    
                child->failure = f ? f->children[c] : ac->root;
    
                // 出力リンク
                if (child->failure->pattern_index >= 0)
                    child->output = child->failure;
                else
                    child->output = child->failure->output;
            }
        }
    
        free(queue);
    }
    
    void aho_corasick_search(AhoCorasick *ac, const char *text,
                             void (*callback)(int pos, int pattern_idx))
    {
        ACNode *current = ac->root;
    
        for (int i = 0; text[i]; i++)
        {
            unsigned char c = text[i];
    
            // 失敗リンクをたどる
            while (current != ac->root && !current->children[c])
                current = current->failure;
    
            if (current->children[c])
                current = current->children[c];
    
            // マッチを報告
            if (current->pattern_index >= 0)
            {
                int len = strlen(ac->patterns[current->pattern_index]);
                callback(i - len + 1, current->pattern_index);
            }
    
            // 出力リンクをたどる
            ACNode *temp = current->output;
            while (temp)
            {
                int len = strlen(ac->patterns[temp->pattern_index]);
                callback(i - len + 1, temp->pattern_index);
                temp = temp->output;
            }
        }
    }
    

    計算量

  • 前処理: O(Σ|patterns|)
  • 検索: O(n + k) (kはマッチ数)
  • 3.2.7 Suffix Array と LCP Array

    接尾辞配列は、テキストの全ての接尾辞を辞書順にソートした配列です。1990年代に開発され、バイオインフォマティクスで広く使用されています。

    定義

    text = "banana$"
    
    すべての接尾辞:
    0: banana$
    1: anana$
    2: nana$
    3: ana$
    4: na$
    5: a$
    6: $
    
    辞書順にソート:
    6: $
    5: a$
    3: ana$
    1: anana$
    0: banana$
    4: na$
    2: nana$
    
    Suffix Array: SA = [6, 5, 3, 1, 0, 4, 2]
    

    LCP配列(Longest Common Prefix)

    隣接する接尾辞の最長共通接頭辞の長さです。

    LCP[i] = SA[i]とSA[i-1]の最長共通接頭辞の長さ
    
    SA[0]: $        LCP[0] = 0 (定義により)
    SA[1]: a$       LCP[1] = 0 ($とa$の共通接頭辞なし)
    SA[2]: ana$     LCP[2] = 1 (a$とana$は"a"を共有)
    SA[3]: anana$   LCP[3] = 3 (ana$とanana$は"ana"を共有)
    SA[4]: banana$  LCP[4] = 0
    SA[5]: na$      LCP[5] = 0
    SA[6]: nana$    LCP[6] = 2 (na$とnana$は"na"を共有)
    

    実装(SA-IS法の簡易版)

    typedef struct {
        int *sa;      // Suffix Array
        int *lcp;     // LCP Array
        int n;        // テキスト長
    } SuffixArray;
    
    // 比較関数
    int suffix_compare(const void *a, const void *b, void *text)
    {
        const char *t = (const char *)text;
        int i = *(const int *)a;
        int j = *(const int *)b;
        return strcmp(t + i, t + j);
    }
    
    // 簡易的なSuffix Array構築(O(n² log n))
    SuffixArray *build_suffix_array(const char *text)
    {
        int n = strlen(text);
    
        SuffixArray *sa = malloc(sizeof(SuffixArray));
        sa->n = n;
        sa->sa = malloc(sizeof(int) * n);
        sa->lcp = malloc(sizeof(int) * n);
    
        // 初期化
        for (int i = 0; i < n; i++)
            sa->sa[i] = i;
    
        // ソート(qsort_rを使用)
        qsort_r(sa->sa, n, sizeof(int), suffix_compare, (void *)text);
    
        // LCP計算(Kasai's algorithm)
        int *rank = malloc(sizeof(int) * n);
        for (int i = 0; i < n; i++)
            rank[sa->sa[i]] = i;
    
        int k = 0;
        for (int i = 0; i < n; i++)
        {
            if (rank[i] == 0)
            {
                sa->lcp[0] = 0;
                k = 0;
                continue;
            }
    
            int j = sa->sa[rank[i] - 1];
    
            while (i + k < n && j + k < n && text[i + k] == text[j + k])
                k++;
    
            sa->lcp[rank[i]] = k;
    
            if (k > 0)
                k--;
        }
    
        free(rank);
        return sa;
    }
    
    // パターン検索(二分探索)
    int suffix_array_search(SuffixArray *sa, const char *text, const char *pattern)
    {
        int left = 0, right = sa->n - 1;
        int m = strlen(pattern);
    
        while (left <= right)
        {
            int mid = (left + right) / 2;
            int cmp = strncmp(text + sa->sa[mid], pattern, m);
    
            if (cmp == 0)
                return sa->sa[mid];
            else if (cmp < 0)
                left = mid + 1;
            else
                right = mid - 1;
        }
    
        return -1;
    }
    

    3.3 C言語における文字列の本質

    3.3.1 文字列リテラルとメモリ配置

    C言語の文字列は、他の高級言語とは根本的に異なります。文字列型は存在せず、「ヌル文字で終端される文字の配列」として実装されます。

    文字列リテラルのメモリ配置

    #include <stdio.h>
    
    int main()
    {
        // 異なる宣言方法とその違い
    
        // 1. 配列として宣言(スタック上、書き込み可能)
        char arr[] = "Hello";
    
        // 2. ポインタとして宣言(読み取り専用領域を指す)
        const char *ptr = "Hello";
    
        // 3. 配列のサイズを明示(余分な領域がある)
        char arr2[10] = "Hello";
    
        // メモリレイアウトを表示
        printf("arr address: %p (stack)\n", (void *)arr);
        printf("ptr address: %p (rodata section)\n", (void *)ptr);
    
        // arr は変更可能
        arr[0] = 'h';  // OK
    
        // ptr は変更不可(未定義動作)
        // ptr[0] = 'h';  // 危険!セグメンテーションフォルト
    
        return 0;
    }
    

    プロセスのメモリレイアウト

    高アドレス
    ┌─────────────────────┐
    │     Stack          │ ← ローカル変数、関数の引数
    │         ↓          │
    ├─────────────────────┤
    │                     │
    │    (未使用領域)    │
    │                     │
    ├─────────────────────┤
    │         ↑          │
    │     Heap           │ ← 動的に割り当てられたメモリ
    ├─────────────────────┤
    │     BSS            │ ← 未初期化グローバル/静的変数
    ├─────────────────────┤
    │     Data           │ ← 初期化済みグローバル/静的変数
    ├─────────────────────┤
    │     Text/Code      │ ← プログラムコード
    ├─────────────────────┤
    │     Read-only data │ ← 文字列リテラルなど
    └─────────────────────┘
    低アドレス
    

    3.3.2 ヌル終端の歴史と設計判断

    なぜC言語はヌル終端を採用したのでしょうか?

    1970年代のコンテキスト

  • メモリの希少性: 文字列長を別途保存するのは贅沢だった
  • PDP-11 アーキテクチャ: ヌルチェックが効率的
  • UNIXとの親和性: ファイル名など、可変長文字列が必要

ヌル終端 vs 長さプレフィックス

// Pascal方式(長さプレフィックス)
struct PascalString {
    unsigned char length;  // 最大255文字の制限
    char data[255];
};

// C方式(ヌル終端)
// 任意の長さをサポート、ただし長さ取得にO(n)

// 現代の実装(両方を使う)
struct ModernString {
    size_t length;
    size_t capacity;
    char *data;  // ヌル終端も保持
};

ヌル終端の利点と欠点

利点:

  • メモリ効率(追加のメタデータ不要)
  • 任意の長さの文字列
  • シンプルなAPI

欠点:

  • 長さ取得がO(n)
  • バッファオーバーフローのリスク
  • バイナリデータに0x00を含められない
  • 3.3.3 文字列操作の危険性

    バッファオーバーフローの歴史

    1988年のMorris Wormは、fingerdプログラムのgets()関数を悪用しました。これは最初の大規模なインターネットワームでした。

    // 危険なコード(絶対に使用禁止)
    void vulnerable_function()
    {
        char buffer[64];
        gets(buffer);  // 入力サイズのチェックなし!
    }
    
    // 攻撃者は64文字以上を入力し、
    // スタック上のリターンアドレスを書き換える
    

    2003年のSQL Slammer

    SQL Serverのスタックバッファオーバーフローを悪用し、わずか10分で75,000台以上のサーバーに感染しました。

    現代の防御機構

  • スタックカナリア: バッファとリターンアドレスの間に検知用の値を配置
  • ASLR: アドレス空間のランダム化
  • NX/DEP: スタック上のコード実行禁止
  • SSP: スタック破壊検出
  • // GCCのスタック保護を有効化
    // gcc -fstack-protector-all program.c
    
    void protected_function()
    {
        char buffer[64];
        // コンパイラが自動的にカナリアを挿入
        // 関数終了時にカナリアの整合性をチェック
    }
    

    3.4 Libft文字列関数の実装

    3.4.1 ft_strlen - 文字列長の測定

    これまで学んだ理論を基に、実際の実装に入りましょう。

    基本実装

    size_t ft_strlen(const char *s)
    {
        size_t len = 0;
    
        while (s[len] != '\0')
            len++;
    
        return len;
    }
    

    なぜsize_tを使うのか

    // size_tは符号なし整数型
    // sizeof演算子の結果型と同じ
    // システムのアドレス空間で表現可能な最大サイズを保証
    
    // 32ビットシステム: size_t = 4バイト (最大約4GB)
    // 64ビットシステム: size_t = 8バイト (最大約16EB)
    
    // 負の長さは意味がないため、符号なし型が適切
    

    ポインタ演算版

    size_t ft_strlen(const char *s)
    {
        const char *start = s;
    
        while (*s)
            s++;
    
        return (s - start);
    }
    

    最適化版(ワード単位処理)

    現代のCPUは一度に複数バイトを処理できます。この特性を活用した最適化です。

    // glibc風の最適化実装
    size_t ft_strlen_optimized(const char *s)
    {
        const char *start = s;
    
        // アライメント調整(ワード境界まで1バイトずつ処理)
        while ((uintptr_t)s & (sizeof(size_t) - 1))
        {
            if (*s == '\0')
                return s - start;
            s++;
        }
    
        // ワード単位で処理
        const size_t *word_ptr = (const size_t *)s;
    
        // マジックナンバー(8バイト版)
        // 0x01を各バイトに配置
        const size_t lo_magic = 0x0101010101010101ULL;
        // 0x80を各バイトに配置
        const size_t hi_magic = 0x8080808080808080ULL;
    
        while (1)
        {
            size_t word = *word_ptr;
    
            // ヌル文字検出のトリック:
            // バイトが0x00の場合、word - lo_magic で最上位ビットが1になり、
            // ~word & hi_magic と AND を取ると非ゼロになる
            if (((word - lo_magic) & ~word & hi_magic) != 0)
            {
                // どのバイトがヌルか特定
                const char *cp = (const char *)word_ptr;
                for (int i = 0; i < (int)sizeof(size_t); i++)
                {
                    if (cp[i] == '\0')
                        return cp - start + i;
                }
            }
            word_ptr++;
        }
    }
    

    3.4.2 ft_strcpy / ft_strlcpy - 文字列コピー

    ft_strcpy(危険な関数)

    char *ft_strcpy(char *dst, const char *src)
    {
        char *ret = dst;
    
        while ((*dst++ = *src++))
            ;
    
        return ret;
    }
    

    ft_strlcpy(安全な代替)

    OpenBSDで開発された、より安全な文字列コピー関数です。

    size_t ft_strlcpy(char *dst, const char *src, size_t dstsize)
    {
        size_t srclen = 0;
    
        // ソースの長さを計算しながらコピー
        if (dstsize > 0)
        {
            while (srclen < dstsize - 1 && src[srclen])
            {
                dst[srclen] = src[srclen];
                srclen++;
            }
            dst[srclen] = '\0';
    
            // 残りのソース長を計算
            while (src[srclen])
                srclen++;
        }
        else
        {
            // dstsize == 0 の場合、コピーなし
            while (src[srclen])
                srclen++;
        }
    
        return srclen;
    }
    

    使用例と安全性

    void demonstrate_safe_copy()
    {
        char buffer[10];
        const char *long_string = "This is a very long string";
    
        // 危険な方法
        // strcpy(buffer, long_string);  // バッファオーバーフロー!
    
        // 安全な方法
        size_t result = ft_strlcpy(buffer, long_string, sizeof(buffer));
    
        // 切り詰めが発生したかチェック
        if (result >= sizeof(buffer))
        {
            printf("Warning: string was truncated\n");
            printf("Original length: %zu, buffer size: %zu\n",
                   result, sizeof(buffer));
        }
    
        printf("Buffer: '%s'\n", buffer);  // "This is a"
    }
    

    3.4.3 ft_strcat / ft_strlcat - 文字列連結

    ft_strcat(危険な関数)

    char *ft_strcat(char *s1, const char *s2)
    {
        char *ret = s1;
    
        // s1の終端を探す
        while (*s1)
            s1++;
    
        // s2をコピー
        while ((*s1++ = *s2++))
            ;
    
        return ret;
    }
    

    ft_strlcat(安全な代替)

    size_t ft_strlcat(char *dst, const char *src, size_t dstsize)
    {
        size_t dst_len = 0;
        size_t src_len = 0;
    
        // dst の長さを計算
        while (dst_len < dstsize && dst[dst_len])
            dst_len++;
    
        // src の長さを計算
        while (src[src_len])
            src_len++;
    
        // dstsize が dst の長さ以下なら、連結不可
        if (dst_len >= dstsize)
            return dstsize + src_len;
    
        // 連結
        size_t i = 0;
        while (src[i] && dst_len + i < dstsize - 1)
        {
            dst[dst_len + i] = src[i];
            i++;
        }
        dst[dst_len + i] = '\0';
    
        return dst_len + src_len;
    }
    

    3.4.4 ft_strcmp / ft_strncmp - 文字列比較

    ft_strcmp

    int ft_strcmp(const char *s1, const char *s2)
    {
        while (*s1 && *s1 == *s2)
        {
            s1++;
            s2++;
        }
    
        return (unsigned char)*s1 - (unsigned char)*s2;
    }
    

    なぜunsigned charにキャストするのか

    void demonstrate_signed_char_issue()
    {
        // 多くのシステムでcharはsigned
        // 0x80 (128) 以上の値は負の数として解釈される
    
        char a = 0x80;  // -128 (signed) または 128 (unsigned)
        char b = 0x01;  // 1
    
        // 間違った比較
        int wrong = a - b;  // -128 - 1 = -129 → aがbより小さいと判定
    
        // 正しい比較
        int correct = (unsigned char)a - (unsigned char)b;  // 128 - 1 = 127
    
        printf("Signed comparison: %d\n", wrong);
        printf("Unsigned comparison: %d\n", correct);
    }
    

    ft_strncmp

    int ft_strncmp(const char *s1, const char *s2, size_t n)
    {
        if (n == 0)
            return 0;
    
        while (n > 1 && *s1 && *s1 == *s2)
        {
            s1++;
            s2++;
            n--;
        }
    
        return (unsigned char)*s1 - (unsigned char)*s2;
    }
    

    3.4.5 ft_strchr / ft_strrchr - 文字検索

    ft_strchr

    char *ft_strchr(const char *s, int c)
    {
        unsigned char ch = (unsigned char)c;
    
        while (*s)
        {
            if (*(unsigned char *)s == ch)
                return (char *)s;
            s++;
        }
    
        // c が '\0' の場合、終端のヌル文字を返す
        if (ch == '\0')
            return (char *)s;
    
        return NULL;
    }
    

    ft_strrchr

    char *ft_strrchr(const char *s, int c)
    {
        unsigned char ch = (unsigned char)c;
        char *last = NULL;
    
        while (*s)
        {
            if (*(unsigned char *)s == ch)
                last = (char *)s;
            s++;
        }
    
        if (ch == '\0')
            return (char *)s;
    
        return last;
    }
    

    3.4.6 ft_strnstr - 部分文字列検索

    char *ft_strnstr(const char *haystack, const char *needle, size_t len)
    {
        size_t needle_len;
    
        // 空のneedleは常にマッチ
        if (*needle == '\0')
            return (char *)haystack;
    
        needle_len = ft_strlen(needle);
    
        while (*haystack && len >= needle_len)
        {
            if (*haystack == *needle &&
                ft_strncmp(haystack, needle, needle_len) == 0)
            {
                return (char *)haystack;
            }
            haystack++;
            len--;
        }
    
        return NULL;
    }
    

    3.4.7 ft_strdup - 文字列複製

    char *ft_strdup(const char *s1)
    {
        size_t len = ft_strlen(s1);
        char *dup = malloc(len + 1);
    
        if (!dup)
            return NULL;
    
        // ft_memcpy または ft_strlcpy を使用
        for (size_t i = 0; i <= len; i++)
            dup[i] = s1[i];
    
        return dup;
    }
    

    3.5 追加の文字列関数

    3.5.1 ft_substr - 部分文字列抽出

    char *ft_substr(char const *s, unsigned int start, size_t len)
    {
        char *substr;
        size_t s_len;
    
        if (!s)
            return NULL;
    
        s_len = ft_strlen(s);
    
        // startが文字列長を超える場合、空文字列を返す
        if (start >= s_len)
            return ft_strdup("");
    
        // 実際にコピーする長さを調整
        if (len > s_len - start)
            len = s_len - start;
    
        substr = malloc(len + 1);
        if (!substr)
            return NULL;
    
        for (size_t i = 0; i < len; i++)
            substr[i] = s[start + i];
    
        substr[len] = '\0';
        return substr;
    }
    

    3.5.2 ft_strjoin - 文字列結合

    char *ft_strjoin(char const *s1, char const *s2)
    {
        char *result;
        size_t len1;
        size_t len2;
        size_t i;
    
        if (!s1 || !s2)
            return NULL;
    
        len1 = ft_strlen(s1);
        len2 = ft_strlen(s2);
    
        result = malloc(len1 + len2 + 1);
        if (!result)
            return NULL;
    
        for (i = 0; i < len1; i++)
            result[i] = s1[i];
    
        for (i = 0; i < len2; i++)
            result[len1 + i] = s2[i];
    
        result[len1 + len2] = '\0';
        return result;
    }
    

    3.5.3 ft_strtrim - 文字列トリム

    static int is_in_set(char c, char const *set)
    {
        while (*set)
        {
            if (*set == c)
                return 1;
            set++;
        }
        return 0;
    }
    
    char *ft_strtrim(char const *s1, char const *set)
    {
        size_t start;
        size_t end;
    
        if (!s1)
            return NULL;
        if (!set)
            return ft_strdup(s1);
    
        start = 0;
        while (s1[start] && is_in_set(s1[start], set))
            start++;
    
        if (!s1[start])
            return ft_strdup("");
    
        end = ft_strlen(s1);
        while (end > start && is_in_set(s1[end - 1], set))
            end--;
    
        return ft_substr(s1, start, end - start);
    }
    

    3.5.4 ft_split - 文字列分割

    static size_t count_words(char const *s, char c)
    {
        size_t count = 0;
        int in_word = 0;
    
        while (*s)
        {
            if (*s != c && !in_word)
            {
                in_word = 1;
                count++;
            }
            else if (*s == c)
            {
                in_word = 0;
            }
            s++;
        }
        return count;
    }
    
    static char *get_word(char const *s, char c, size_t *start)
    {
        size_t end;
        char *word;
    
        // 区切り文字をスキップ
        while (s[*start] && s[*start] == c)
            (*start)++;
    
        // 単語の終端を探す
        end = *start;
        while (s[end] && s[end] != c)
            end++;
    
        // 単語をコピー
        word = ft_substr(s, *start, end - *start);
        *start = end;
    
        return word;
    }
    
    char **ft_split(char const *s, char c)
    {
        char **result;
        size_t word_count;
        size_t i;
        size_t pos;
    
        if (!s)
            return NULL;
    
        word_count = count_words(s, c);
        result = malloc(sizeof(char *) * (word_count + 1));
        if (!result)
            return NULL;
    
        pos = 0;
        for (i = 0; i < word_count; i++)
        {
            result[i] = get_word(s, c, &pos);
            if (!result[i])
            {
                // エラー処理:これまでの割り当てを解放
                while (i > 0)
                    free(result[--i]);
                free(result);
                return NULL;
            }
        }
        result[word_count] = NULL;
    
        return result;
    }
    

    3.5.5 ft_itoa - 整数から文字列への変換

    static int get_num_len(int n)
    {
        int len = 0;
    
        if (n <= 0)
            len = 1;  // 負号または0
    
        while (n != 0)
        {
            n /= 10;
            len++;
        }
        return len;
    }
    
    char *ft_itoa(int n)
    {
        char *str;
        int len;
        long num;
    
        num = n;
        len = get_num_len(n);
    
        str = malloc(len + 1);
        if (!str)
            return NULL;
    
        str[len] = '\0';
    
        if (num == 0)
        {
            str[0] = '0';
            return str;
        }
    
        if (num < 0)
        {
            str[0] = '-';
            num = -num;
        }
    
        while (num > 0)
        {
            str[--len] = (num % 10) + '0';
            num /= 10;
        }
    
        return str;
    }
    

    3.5.6 ft_strmapi - 文字列変換

    char *ft_strmapi(char const *s, char (*f)(unsigned int, char))
    {
        char *result;
        size_t len;
        unsigned int i;
    
        if (!s || !f)
            return NULL;
    
        len = ft_strlen(s);
        result = malloc(len + 1);
        if (!result)
            return NULL;
    
        for (i = 0; i < len; i++)
            result[i] = f(i, s[i]);
    
        result[len] = '\0';
        return result;
    }
    

    3.5.7 ft_striteri - 文字列反復処理

    void ft_striteri(char *s, void (*f)(unsigned int, char*))
    {
        unsigned int i;
    
        if (!s || !f)
            return;
    
        for (i = 0; s[i]; i++)
            f(i, &s[i]);
    }
    

    3.6 実践的なアプリケーション

    3.6.1 簡易的なシェルのコマンドパーサー

    typedef struct s_command {
        char *name;
        char **args;
        int argc;
        char *input_file;
        char *output_file;
        int append_mode;
    } t_command;
    
    t_command *parse_command(const char *line)
    {
        t_command *cmd = calloc(1, sizeof(t_command));
        char **tokens;
        int i;
    
        if (!cmd)
            return NULL;
    
        // 空白で分割
        tokens = ft_split(line, ' ');
        if (!tokens || !tokens[0])
        {
            free(cmd);
            return NULL;
        }
    
        cmd->name = ft_strdup(tokens[0]);
    
        // 引数とリダイレクトを解析
        i = 1;
        cmd->args = malloc(sizeof(char *) * 100);
        cmd->argc = 0;
    
        while (tokens[i])
        {
            if (ft_strcmp(tokens[i], "<") == 0 && tokens[i + 1])
            {
                cmd->input_file = ft_strdup(tokens[++i]);
            }
            else if (ft_strcmp(tokens[i], ">") == 0 && tokens[i + 1])
            {
                cmd->output_file = ft_strdup(tokens[++i]);
                cmd->append_mode = 0;
            }
            else if (ft_strcmp(tokens[i], ">>") == 0 && tokens[i + 1])
            {
                cmd->output_file = ft_strdup(tokens[++i]);
                cmd->append_mode = 1;
            }
            else
            {
                cmd->args[cmd->argc++] = ft_strdup(tokens[i]);
            }
            i++;
        }
        cmd->args[cmd->argc] = NULL;
    
        // tokensを解放
        for (i = 0; tokens[i]; i++)
            free(tokens[i]);
        free(tokens);
    
        return cmd;
    }
    

    3.6.2 URLパーサー

    typedef struct s_url {
        char *scheme;    // http, https, ftp
        char *host;
        char *port;
        char *path;
        char *query;
        char *fragment;
    } t_url;
    
    t_url *parse_url(const char *url_str)
    {
        t_url *url = calloc(1, sizeof(t_url));
        char *pos;
        char *temp;
    
        if (!url)
            return NULL;
    
        temp = ft_strdup(url_str);
        pos = temp;
    
        // スキーム(://の前)
        char *scheme_end = ft_strnstr(pos, "://", ft_strlen(pos));
        if (scheme_end)
        {
            *scheme_end = '\0';
            url->scheme = ft_strdup(pos);
            pos = scheme_end + 3;
        }
        else
        {
            url->scheme = ft_strdup("http");
        }
    
        // フラグメント(#以降)
        char *fragment = ft_strchr(pos, '#');
        if (fragment)
        {
            url->fragment = ft_strdup(fragment + 1);
            *fragment = '\0';
        }
    
        // クエリ(?以降)
        char *query = ft_strchr(pos, '?');
        if (query)
        {
            url->query = ft_strdup(query + 1);
            *query = '\0';
        }
    
        // パス(/以降)
        char *path = ft_strchr(pos, '/');
        if (path)
        {
            url->path = ft_strdup(path);
            *path = '\0';
        }
        else
        {
            url->path = ft_strdup("/");
        }
    
        // ポート(:以降)
        char *port = ft_strchr(pos, ':');
        if (port)
        {
            url->port = ft_strdup(port + 1);
            *port = '\0';
        }
    
        // ホスト(残り)
        url->host = ft_strdup(pos);
    
        free(temp);
        return url;
    }
    

    3.6.3 設定ファイルパーサー

    typedef struct s_config_entry {
        char *key;
        char *value;
        struct s_config_entry *next;
    } t_config_entry;
    
    typedef struct s_config {
        t_config_entry *entries;
    } t_config;
    
    t_config *parse_config_file(const char *content)
    {
        t_config *config = calloc(1, sizeof(t_config));
        char **lines;
        int i;
    
        if (!config)
            return NULL;
    
        lines = ft_split(content, '\n');
        if (!lines)
        {
            free(config);
            return NULL;
        }
    
        for (i = 0; lines[i]; i++)
        {
            char *line = ft_strtrim(lines[i], " \t");
    
            // コメントと空行をスキップ
            if (!line || line[0] == '#' || line[0] == '\0')
            {
                free(line);
                continue;
            }
    
            // key=value を解析
            char *equals = ft_strchr(line, '=');
            if (equals)
            {
                *equals = '\0';
    
                t_config_entry *entry = malloc(sizeof(t_config_entry));
                entry->key = ft_strtrim(line, " \t");
                entry->value = ft_strtrim(equals + 1, " \t\"'");
                entry->next = config->entries;
                config->entries = entry;
            }
    
            free(line);
            free(lines[i]);
        }
        free(lines);
    
        return config;
    }
    
    const char *get_config_value(t_config *config, const char *key)
    {
        t_config_entry *entry = config->entries;
    
        while (entry)
        {
            if (ft_strcmp(entry->key, key) == 0)
                return entry->value;
            entry = entry->next;
        }
    
        return NULL;
    }
    

    3.7 パフォーマンス最適化とベンチマーク

    3.7.1 文字列操作のコスト

    #include <time.h>
    
    void benchmark_string_functions()
    {
        const int iterations = 1000000;
        clock_t start, end;
    
        // テスト文字列
        char *test_short = "Hello";
        char *test_medium = "The quick brown fox jumps over the lazy dog";
        char *test_long = malloc(10001);
        memset(test_long, 'A', 10000);
        test_long[10000] = '\0';
    
        printf("Benchmarking ft_strlen:\n");
    
        // 短い文字列
        start = clock();
        for (int i = 0; i < iterations; i++)
            ft_strlen(test_short);
        end = clock();
        printf("  Short (%zu chars): %.3f ms\n",
               ft_strlen(test_short),
               ((double)(end - start) / CLOCKS_PER_SEC) * 1000);
    
        // 中程度の文字列
        start = clock();
        for (int i = 0; i < iterations; i++)
            ft_strlen(test_medium);
        end = clock();
        printf("  Medium (%zu chars): %.3f ms\n",
               ft_strlen(test_medium),
               ((double)(end - start) / CLOCKS_PER_SEC) * 1000);
    
        // 長い文字列
        start = clock();
        for (int i = 0; i < iterations; i++)
            ft_strlen(test_long);
        end = clock();
        printf("  Long (%zu chars): %.3f ms\n",
               ft_strlen(test_long),
               ((double)(end - start) / CLOCKS_PER_SEC) * 1000);
    
        free(test_long);
    }
    

    3.7.2 キャッシュ効率

    void measure_cache_effects()
    {
        // L1キャッシュ: 通常32KB
        // L2キャッシュ: 通常256KB-1MB
        // L3キャッシュ: 通常8MB-32MB
    
        size_t sizes[] = {
            1024,       // 1KB - L1に収まる
            16384,      // 16KB - L1に収まる
            65536,      // 64KB - L1を超える
            262144,     // 256KB - L2に収まる
            1048576,    // 1MB - L2を超える
            4194304,    // 4MB - L3に収まる
            16777216    // 16MB - L3を超える可能性
        };
    
        for (int s = 0; s < sizeof(sizes)/sizeof(sizes[0]); s++)
        {
            size_t size = sizes[s];
            char *buffer = malloc(size);
            memset(buffer, 'A', size - 1);
            buffer[size - 1] = '\0';
    
            // ウォームアップ
            for (int i = 0; i < 10; i++)
                ft_strlen(buffer);
    
            // 計測
            clock_t start = clock();
            for (int i = 0; i < 1000; i++)
                ft_strlen(buffer);
            clock_t end = clock();
    
            double time_per_call = ((double)(end - start) / CLOCKS_PER_SEC) / 1000 * 1e9;
            double ns_per_byte = time_per_call / size;
    
            printf("Buffer %zu bytes: %.2f ns/call, %.3f ns/byte\n",
                   size, time_per_call, ns_per_byte);
    
            free(buffer);
        }
    }
    

    3.8 セキュリティとベストプラクティス

    3.8.1 安全な文字列操作のガイドライン

    // 1. 常にバッファサイズを追跡する
    typedef struct s_safe_string {
        char *data;
        size_t length;
        size_t capacity;
    } t_safe_string;
    
    t_safe_string *safe_string_new(size_t initial_capacity)
    {
        t_safe_string *s = malloc(sizeof(t_safe_string));
        if (!s)
            return NULL;
    
        s->data = malloc(initial_capacity);
        if (!s->data)
        {
            free(s);
            return NULL;
        }
    
        s->data[0] = '\0';
        s->length = 0;
        s->capacity = initial_capacity;
    
        return s;
    }
    
    int safe_string_append(t_safe_string *s, const char *str)
    {
        size_t str_len = ft_strlen(str);
    
        // 容量チェックと拡張
        if (s->length + str_len + 1 > s->capacity)
        {
            size_t new_capacity = (s->length + str_len + 1) * 2;
            char *new_data = realloc(s->data, new_capacity);
            if (!new_data)
                return -1;
    
            s->data = new_data;
            s->capacity = new_capacity;
        }
    
        // コピー
        ft_strlcpy(s->data + s->length, str, s->capacity - s->length);
        s->length += str_len;
    
        return 0;
    }
    
    // 2. 入力のサニタイズ
    char *sanitize_string(const char *input, const char *allowed_chars)
    {
        size_t len = ft_strlen(input);
        char *result = malloc(len + 1);
        size_t j = 0;
    
        if (!result)
            return NULL;
    
        for (size_t i = 0; i < len; i++)
        {
            if (ft_strchr(allowed_chars, input[i]))
                result[j++] = input[i];
        }
    
        result[j] = '\0';
        return result;
    }
    

    3.8.2 よくある脆弱性パターンと対策

    // 脆弱なパターン1: フォーマット文字列攻撃
    void vulnerable_log(char *user_input)
    {
        printf(user_input);  // 危険!
    }
    
    void safe_log(char *user_input)
    {
        printf("%s", user_input);  // 安全
    }
    
    // 脆弱なパターン2: 境界チェックなしのコピー
    void vulnerable_copy(char *dest, char *src)
    {
        while (*src)
            *dest++ = *src++;
        *dest = '\0';
    }
    
    void safe_copy(char *dest, size_t dest_size, const char *src)
    {
        ft_strlcpy(dest, src, dest_size);
    }
    
    // 脆弱なパターン3: オフバイワンエラー
    void off_by_one_error(char *buffer, size_t size)
    {
        for (size_t i = 0; i <= size; i++)  // <= ではなく < を使うべき
            buffer[i] = 'A';
    }
    

    3.9 本章のまとめ

    3.9.1 学習した理論

  • 形式言語理論
- アルファベット、文字列、言語の数学的定義 - チョムスキー階層 - 有限オートマトンと正規表現

  • 文字列アルゴリズム
- ナイーブ法(O(nm)) - KMP法(O(n+m)) - Boyer-Moore法(O(n/m) 平均) - Rabin-Karp法(ハッシュベース) - Aho-Corasick法(複数パターン) - Suffix Array(高度なテキスト処理)

  • C言語の文字列
- ヌル終端文字列の設計思想 - メモリレイアウトと安全性 - 歴史的なセキュリティ問題

3.9.2 実装したLibft関数

| 関数 | 用途 | 計算量 | |------|------|--------| | ft_strlen | 文字列長 | O(n) | | ft_strcpy | コピー(危険) | O(n) | | ft_strlcpy | 安全なコピー | O(n) | | ft_strcat | 連結(危険) | O(n+m) | | ft_strlcat | 安全な連結 | O(n+m) | | ft_strcmp | 比較 | O(n) | | ft_strncmp | n文字比較 | O(n) | | ft_strchr | 文字検索 | O(n) | | ft_strrchr | 逆方向検索 | O(n) | | ft_strnstr | 部分文字列検索 | O(nm) | | ft_strdup | 複製 | O(n) | | ft_substr | 部分文字列 | O(n) | | ft_strjoin | 結合 | O(n+m) | | ft_strtrim | トリム | O(n) | | ft_split | 分割 | O(n) | | ft_itoa | 整数→文字列 | O(log n) | | ft_strmapi | マップ | O(n) | | ft_striteri | イテレート | O(n) |

3.9.3 次のステップ

次章では、メモリ操作関数を学びます:

  • メモリ階層とキャッシュの仕組み
  • 仮想メモリとページング
  • ポインタの深い理解
  • ft_memset, ft_bzero, ft_memcpy, ft_memmove の実装

文字列操作とメモリ操作は密接に関連しており、両方の理解がより効率的で安全なコードを書く基礎となります。

で終わる → テキストも終端ならマッチ if (regexp[0] == '

3.2 文字列アルゴリズムの歴史と理論

3.2.1 文字列探索問題の定義

文字列探索(String Searching/Pattern Matching)問題は、コンピュータサイエンスで最も基本的かつ重要な問題の一つです。

問題の定式化

___CODE_BLOCK_14___

応用分野

  • テキストエディタ: 検索・置換機能
  • Webブラウザ: ページ内検索
  • バイオインフォマティクス: DNA/タンパク質配列解析
  • ネットワークセキュリティ: パケット検査、ウイルススキャン
  • データベース: LIKE句の実装
  • コンパイラ: 字句解析
  • 3.2.2 ナイーブアルゴリズム(Brute Force)

    最も単純なアプローチから始めましょう。

    ___CODE_BLOCK_15___

    計算量分析

  • 最悪ケース: O(nm)
  • - 例: text = "AAAAAAAAAB", pattern = "AAAAB" - 各位置で m 回の比較が必要

    • 平均ケース: O(n)
    - ランダムなテキストでは、ミスマッチがすぐに見つかる

    3.2.3 Knuth-Morris-Pratt(KMP)アルゴリズム

    1977年、ドナルド・クヌース(Donald Knuth)、ジェームズ・モリス(James Morris)、ヴォーン・プラット(Vaughan Pratt)が独立に発見し、共同で発表した画期的なアルゴリズムです。

    核心的なアイデア

    ミスマッチが発生したとき、すでに比較した情報を活用して、無駄な比較をスキップします。

    ___CODE_BLOCK_16___

    失敗関数(Failure Function)の計算

    失敗関数 π[j] は、pattern[0..j] の最長の「真の接頭辞かつ接尾辞」の長さを表します。

    ___CODE_BLOCK_17___

    KMPアルゴリズムの完全実装

    ___CODE_BLOCK_18___

    計算量

    • 前処理(失敗関数): O(m)
    • 検索: O(n)
    • 合計: O(n + m)
    • 3.2.4 Boyer-Moore アルゴリズム

      1977年、ロバート・ボイヤー(Robert Boyer)とJ・ストロザー・ムーア(J Strother Moore)が発表した、実用上最も高速な文字列検索アルゴリズムの一つです。

      革新的なアイデア

    • 後ろから比較: パターンを右から左に比較する
    • Bad Character Rule: ミスマッチ文字を利用してスキップ
    • Good Suffix Rule: マッチした部分を利用してスキップ
    • ___CODE_BLOCK_19___

      Bad Character Rule の実装

      ___CODE_BLOCK_20___

      Good Suffix Rule の実装

      ___CODE_BLOCK_21___

      Boyer-Moore 完全実装

      ___CODE_BLOCK_22___

      計算量

    • 前処理: O(m + |Σ|)
    • 最悪ケース: O(nm)
    • 平均・最良ケース: O(n/m) - サブリニア!
    • 3.2.5 Rabin-Karp アルゴリズム

      1987年、マイケル・ラビン(Michael Rabin)とリチャード・カープ(Richard Karp)が発表した、ハッシュ関数を利用したアルゴリズムです。

      アイデア

      パターンのハッシュ値を計算し、テキストの各位置のハッシュ値と比較します。ローリングハッシュ(Rolling Hash)により、効率的に次の位置のハッシュを計算できます。

      ローリングハッシュ

      ___CODE_BLOCK_23___

      実装

      ___CODE_BLOCK_24___

      複数パターン検索

      Rabin-Karpの真価は、複数のパターンを同時に検索する場合に発揮されます。

      ___CODE_BLOCK_25___

      3.2.6 Aho-Corasick アルゴリズム

      1975年、アルフレッド・エイホ(Alfred Aho)とマーガレット・コラシック(Margaret Corasick)が発表した、複数パターン同時検索アルゴリズムです。

      構造

    • トライ(Trie): パターンの共通接頭辞を共有する木構造
    • 失敗リンク: KMPの失敗関数を多パターンに一般化
    • 出力リンク: マッチしたパターンを報告
    • ___CODE_BLOCK_26___

      計算量

    • 前処理: O(Σ|patterns|)
    • 検索: O(n + k) (kはマッチ数)
    • 3.2.7 Suffix Array と LCP Array

      接尾辞配列は、テキストの全ての接尾辞を辞書順にソートした配列です。1990年代に開発され、バイオインフォマティクスで広く使用されています。

      定義

      ___CODE_BLOCK_27___

      LCP配列(Longest Common Prefix)

      隣接する接尾辞の最長共通接頭辞の長さです。

      ___CODE_BLOCK_28___

      実装(SA-IS法の簡易版)

      ___CODE_BLOCK_29___

      3.3 C言語における文字列の本質

      3.3.1 文字列リテラルとメモリ配置

      C言語の文字列は、他の高級言語とは根本的に異なります。文字列型は存在せず、「ヌル文字で終端される文字の配列」として実装されます。

      文字列リテラルのメモリ配置

      ___CODE_BLOCK_30___

      プロセスのメモリレイアウト

      ___CODE_BLOCK_31___

      3.3.2 ヌル終端の歴史と設計判断

      なぜC言語はヌル終端を採用したのでしょうか?

      1970年代のコンテキスト

    • メモリの希少性: 文字列長を別途保存するのは贅沢だった
    • PDP-11 アーキテクチャ: ヌルチェックが効率的
    • UNIXとの親和性: ファイル名など、可変長文字列が必要

    ヌル終端 vs 長さプレフィックス

    ___CODE_BLOCK_32___

    ヌル終端の利点と欠点

    利点:

    • メモリ効率(追加のメタデータ不要)
    • 任意の長さの文字列
    • シンプルなAPI

    欠点:

    • 長さ取得がO(n)
    • バッファオーバーフローのリスク
    • バイナリデータに0x00を含められない
    • 3.3.3 文字列操作の危険性

      バッファオーバーフローの歴史

      1988年のMorris Wormは、fingerdプログラムのgets()関数を悪用しました。これは最初の大規模なインターネットワームでした。

      ___CODE_BLOCK_33___

      2003年のSQL Slammer

      SQL Serverのスタックバッファオーバーフローを悪用し、わずか10分で75,000台以上のサーバーに感染しました。

      現代の防御機構

    • スタックカナリア: バッファとリターンアドレスの間に検知用の値を配置
    • ASLR: アドレス空間のランダム化
    • NX/DEP: スタック上のコード実行禁止
    • SSP: スタック破壊検出
    • ___CODE_BLOCK_34___

      3.4 Libft文字列関数の実装

      3.4.1 ft_strlen - 文字列長の測定

      これまで学んだ理論を基に、実際の実装に入りましょう。

      基本実装

      ___CODE_BLOCK_35___

      なぜsize_tを使うのか

      ___CODE_BLOCK_36___

      ポインタ演算版

      ___CODE_BLOCK_37___

      最適化版(ワード単位処理)

      現代のCPUは一度に複数バイトを処理できます。この特性を活用した最適化です。

      ___CODE_BLOCK_38___

      3.4.2 ft_strcpy / ft_strlcpy - 文字列コピー

      ft_strcpy(危険な関数)

      ___CODE_BLOCK_39___

      ft_strlcpy(安全な代替)

      OpenBSDで開発された、より安全な文字列コピー関数です。

      ___CODE_BLOCK_40___

      使用例と安全性

      ___CODE_BLOCK_41___

      3.4.3 ft_strcat / ft_strlcat - 文字列連結

      ft_strcat(危険な関数)

      ___CODE_BLOCK_42___

      ft_strlcat(安全な代替)

      ___CODE_BLOCK_43___

      3.4.4 ft_strcmp / ft_strncmp - 文字列比較

      ft_strcmp

      ___CODE_BLOCK_44___

      なぜunsigned charにキャストするのか

      ___CODE_BLOCK_45___

      ft_strncmp

      ___CODE_BLOCK_46___

      3.4.5 ft_strchr / ft_strrchr - 文字検索

      ft_strchr

      ___CODE_BLOCK_47___

      ft_strrchr

      ___CODE_BLOCK_48___

      3.4.6 ft_strnstr - 部分文字列検索

      ___CODE_BLOCK_49___

      3.4.7 ft_strdup - 文字列複製

      ___CODE_BLOCK_50___

      3.5 追加の文字列関数

      3.5.1 ft_substr - 部分文字列抽出

      ___CODE_BLOCK_51___

      3.5.2 ft_strjoin - 文字列結合

      ___CODE_BLOCK_52___

      3.5.3 ft_strtrim - 文字列トリム

      ___CODE_BLOCK_53___

      3.5.4 ft_split - 文字列分割

      ___CODE_BLOCK_54___

      3.5.5 ft_itoa - 整数から文字列への変換

      ___CODE_BLOCK_55___

      3.5.6 ft_strmapi - 文字列変換

      ___CODE_BLOCK_56___

      3.5.7 ft_striteri - 文字列反復処理

      ___CODE_BLOCK_57___

      3.6 実践的なアプリケーション

      3.6.1 簡易的なシェルのコマンドパーサー

      ___CODE_BLOCK_58___

      3.6.2 URLパーサー

      ___CODE_BLOCK_59___

      3.6.3 設定ファイルパーサー

      ___CODE_BLOCK_60___

      3.7 パフォーマンス最適化とベンチマーク

      3.7.1 文字列操作のコスト

      ___CODE_BLOCK_61___

      3.7.2 キャッシュ効率

      ___CODE_BLOCK_62___

      3.8 セキュリティとベストプラクティス

      3.8.1 安全な文字列操作のガイドライン

      ___CODE_BLOCK_63___

      3.8.2 よくある脆弱性パターンと対策

      ___CODE_BLOCK_64___

      3.9 本章のまとめ

      3.9.1 学習した理論

    • 形式言語理論
    - アルファベット、文字列、言語の数学的定義 - チョムスキー階層 - 有限オートマトンと正規表現

    • 文字列アルゴリズム
    - ナイーブ法(O(nm)) - KMP法(O(n+m)) - Boyer-Moore法(O(n/m) 平均) - Rabin-Karp法(ハッシュベース) - Aho-Corasick法(複数パターン) - Suffix Array(高度なテキスト処理)

    • C言語の文字列
    - ヌル終端文字列の設計思想 - メモリレイアウトと安全性 - 歴史的なセキュリティ問題

    3.9.2 実装したLibft関数

    | 関数 | 用途 | 計算量 | |------|------|--------| | ft_strlen | 文字列長 | O(n) | | ft_strcpy | コピー(危険) | O(n) | | ft_strlcpy | 安全なコピー | O(n) | | ft_strcat | 連結(危険) | O(n+m) | | ft_strlcat | 安全な連結 | O(n+m) | | ft_strcmp | 比較 | O(n) | | ft_strncmp | n文字比較 | O(n) | | ft_strchr | 文字検索 | O(n) | | ft_strrchr | 逆方向検索 | O(n) | | ft_strnstr | 部分文字列検索 | O(nm) | | ft_strdup | 複製 | O(n) | | ft_substr | 部分文字列 | O(n) | | ft_strjoin | 結合 | O(n+m) | | ft_strtrim | トリム | O(n) | | ft_split | 分割 | O(n) | | ft_itoa | 整数→文字列 | O(log n) | | ft_strmapi | マップ | O(n) | | ft_striteri | イテレート | O(n) |

    3.9.3 次のステップ

    次章では、メモリ操作関数を学びます:

    • メモリ階層とキャッシュの仕組み
    • 仮想メモリとページング
    • ポインタの深い理解
    • ft_memset, ft_bzero, ft_memcpy, ft_memmove の実装

    文字列操作とメモリ操作は密接に関連しており、両方の理解がより効率的で安全なコードを書く基礎となります。

    && regexp[1] == '\0') return *text == '\0'; // '*'の処理 if (regexp[1] == '*') return match_star(regexp[0], regexp + 2, text); // テキストが残っていて、1文字がマッチ if (*text != '\0' && (regexp[0] == '.' || regexp[0] == *text)) return match_here(regexp + 1, text + 1); return 0; } int match(const char *regexp, const char *text) { // '^'で始まる → 先頭からマッチ if (regexp[0] == '^') return match_here(regexp + 1, text); // テキストのどこかでマッチするか試す do { if (match_here(regexp, text)) return 1; } while (*text++ != '\0'); return 0; }

    3.2 文字列アルゴリズムの歴史と理論

    3.2.1 文字列探索問題の定義

    文字列探索(String Searching/Pattern Matching)問題は、コンピュータサイエンスで最も基本的かつ重要な問題の一つです。

    問題の定式化

    ___CODE_BLOCK_14___

    応用分野

  • テキストエディタ: 検索・置換機能
  • Webブラウザ: ページ内検索
  • バイオインフォマティクス: DNA/タンパク質配列解析
  • ネットワークセキュリティ: パケット検査、ウイルススキャン
  • データベース: LIKE句の実装
  • コンパイラ: 字句解析
  • 3.2.2 ナイーブアルゴリズム(Brute Force)

    最も単純なアプローチから始めましょう。

    ___CODE_BLOCK_15___

    計算量分析

  • 最悪ケース: O(nm)
  • - 例: text = "AAAAAAAAAB", pattern = "AAAAB" - 各位置で m 回の比較が必要

    • 平均ケース: O(n)
    - ランダムなテキストでは、ミスマッチがすぐに見つかる

    3.2.3 Knuth-Morris-Pratt(KMP)アルゴリズム

    1977年、ドナルド・クヌース(Donald Knuth)、ジェームズ・モリス(James Morris)、ヴォーン・プラット(Vaughan Pratt)が独立に発見し、共同で発表した画期的なアルゴリズムです。

    核心的なアイデア

    ミスマッチが発生したとき、すでに比較した情報を活用して、無駄な比較をスキップします。

    ___CODE_BLOCK_16___

    失敗関数(Failure Function)の計算

    失敗関数 π[j] は、pattern[0..j] の最長の「真の接頭辞かつ接尾辞」の長さを表します。

    ___CODE_BLOCK_17___

    KMPアルゴリズムの完全実装

    ___CODE_BLOCK_18___

    計算量

    • 前処理(失敗関数): O(m)
    • 検索: O(n)
    • 合計: O(n + m)
    • 3.2.4 Boyer-Moore アルゴリズム

      1977年、ロバート・ボイヤー(Robert Boyer)とJ・ストロザー・ムーア(J Strother Moore)が発表した、実用上最も高速な文字列検索アルゴリズムの一つです。

      革新的なアイデア

    • 後ろから比較: パターンを右から左に比較する
    • Bad Character Rule: ミスマッチ文字を利用してスキップ
    • Good Suffix Rule: マッチした部分を利用してスキップ
    • ___CODE_BLOCK_19___

      Bad Character Rule の実装

      ___CODE_BLOCK_20___

      Good Suffix Rule の実装

      ___CODE_BLOCK_21___

      Boyer-Moore 完全実装

      ___CODE_BLOCK_22___

      計算量

    • 前処理: O(m + |Σ|)
    • 最悪ケース: O(nm)
    • 平均・最良ケース: O(n/m) - サブリニア!
    • 3.2.5 Rabin-Karp アルゴリズム

      1987年、マイケル・ラビン(Michael Rabin)とリチャード・カープ(Richard Karp)が発表した、ハッシュ関数を利用したアルゴリズムです。

      アイデア

      パターンのハッシュ値を計算し、テキストの各位置のハッシュ値と比較します。ローリングハッシュ(Rolling Hash)により、効率的に次の位置のハッシュを計算できます。

      ローリングハッシュ

      ___CODE_BLOCK_23___

      実装

      ___CODE_BLOCK_24___

      複数パターン検索

      Rabin-Karpの真価は、複数のパターンを同時に検索する場合に発揮されます。

      ___CODE_BLOCK_25___

      3.2.6 Aho-Corasick アルゴリズム

      1975年、アルフレッド・エイホ(Alfred Aho)とマーガレット・コラシック(Margaret Corasick)が発表した、複数パターン同時検索アルゴリズムです。

      構造

    • トライ(Trie): パターンの共通接頭辞を共有する木構造
    • 失敗リンク: KMPの失敗関数を多パターンに一般化
    • 出力リンク: マッチしたパターンを報告
    • ___CODE_BLOCK_26___

      計算量

    • 前処理: O(Σ|patterns|)
    • 検索: O(n + k) (kはマッチ数)
    • 3.2.7 Suffix Array と LCP Array

      接尾辞配列は、テキストの全ての接尾辞を辞書順にソートした配列です。1990年代に開発され、バイオインフォマティクスで広く使用されています。

      定義

      ___CODE_BLOCK_27___

      LCP配列(Longest Common Prefix)

      隣接する接尾辞の最長共通接頭辞の長さです。

      ___CODE_BLOCK_28___

      実装(SA-IS法の簡易版)

      ___CODE_BLOCK_29___

      3.3 C言語における文字列の本質

      3.3.1 文字列リテラルとメモリ配置

      C言語の文字列は、他の高級言語とは根本的に異なります。文字列型は存在せず、「ヌル文字で終端される文字の配列」として実装されます。

      文字列リテラルのメモリ配置

      ___CODE_BLOCK_30___

      プロセスのメモリレイアウト

      ___CODE_BLOCK_31___

      3.3.2 ヌル終端の歴史と設計判断

      なぜC言語はヌル終端を採用したのでしょうか?

      1970年代のコンテキスト

    • メモリの希少性: 文字列長を別途保存するのは贅沢だった
    • PDP-11 アーキテクチャ: ヌルチェックが効率的
    • UNIXとの親和性: ファイル名など、可変長文字列が必要

    ヌル終端 vs 長さプレフィックス

    ___CODE_BLOCK_32___

    ヌル終端の利点と欠点

    利点:

    • メモリ効率(追加のメタデータ不要)
    • 任意の長さの文字列
    • シンプルなAPI

    欠点:

    • 長さ取得がO(n)
    • バッファオーバーフローのリスク
    • バイナリデータに0x00を含められない
    • 3.3.3 文字列操作の危険性

      バッファオーバーフローの歴史

      1988年のMorris Wormは、fingerdプログラムのgets()関数を悪用しました。これは最初の大規模なインターネットワームでした。

      ___CODE_BLOCK_33___

      2003年のSQL Slammer

      SQL Serverのスタックバッファオーバーフローを悪用し、わずか10分で75,000台以上のサーバーに感染しました。

      現代の防御機構

    • スタックカナリア: バッファとリターンアドレスの間に検知用の値を配置
    • ASLR: アドレス空間のランダム化
    • NX/DEP: スタック上のコード実行禁止
    • SSP: スタック破壊検出
    • ___CODE_BLOCK_34___

      3.4 Libft文字列関数の実装

      3.4.1 ft_strlen - 文字列長の測定

      これまで学んだ理論を基に、実際の実装に入りましょう。

      基本実装

      ___CODE_BLOCK_35___

      なぜsize_tを使うのか

      ___CODE_BLOCK_36___

      ポインタ演算版

      ___CODE_BLOCK_37___

      最適化版(ワード単位処理)

      現代のCPUは一度に複数バイトを処理できます。この特性を活用した最適化です。

      ___CODE_BLOCK_38___

      3.4.2 ft_strcpy / ft_strlcpy - 文字列コピー

      ft_strcpy(危険な関数)

      ___CODE_BLOCK_39___

      ft_strlcpy(安全な代替)

      OpenBSDで開発された、より安全な文字列コピー関数です。

      ___CODE_BLOCK_40___

      使用例と安全性

      ___CODE_BLOCK_41___

      3.4.3 ft_strcat / ft_strlcat - 文字列連結

      ft_strcat(危険な関数)

      ___CODE_BLOCK_42___

      ft_strlcat(安全な代替)

      ___CODE_BLOCK_43___

      3.4.4 ft_strcmp / ft_strncmp - 文字列比較

      ft_strcmp

      ___CODE_BLOCK_44___

      なぜunsigned charにキャストするのか

      ___CODE_BLOCK_45___

      ft_strncmp

      ___CODE_BLOCK_46___

      3.4.5 ft_strchr / ft_strrchr - 文字検索

      ft_strchr

      ___CODE_BLOCK_47___

      ft_strrchr

      ___CODE_BLOCK_48___

      3.4.6 ft_strnstr - 部分文字列検索

      ___CODE_BLOCK_49___

      3.4.7 ft_strdup - 文字列複製

      ___CODE_BLOCK_50___

      3.5 追加の文字列関数

      3.5.1 ft_substr - 部分文字列抽出

      ___CODE_BLOCK_51___

      3.5.2 ft_strjoin - 文字列結合

      ___CODE_BLOCK_52___

      3.5.3 ft_strtrim - 文字列トリム

      ___CODE_BLOCK_53___

      3.5.4 ft_split - 文字列分割

      ___CODE_BLOCK_54___

      3.5.5 ft_itoa - 整数から文字列への変換

      ___CODE_BLOCK_55___

      3.5.6 ft_strmapi - 文字列変換

      ___CODE_BLOCK_56___

      3.5.7 ft_striteri - 文字列反復処理

      ___CODE_BLOCK_57___

      3.6 実践的なアプリケーション

      3.6.1 簡易的なシェルのコマンドパーサー

      ___CODE_BLOCK_58___

      3.6.2 URLパーサー

      ___CODE_BLOCK_59___

      3.6.3 設定ファイルパーサー

      ___CODE_BLOCK_60___

      3.7 パフォーマンス最適化とベンチマーク

      3.7.1 文字列操作のコスト

      ___CODE_BLOCK_61___

      3.7.2 キャッシュ効率

      ___CODE_BLOCK_62___

      3.8 セキュリティとベストプラクティス

      3.8.1 安全な文字列操作のガイドライン

      ___CODE_BLOCK_63___

      3.8.2 よくある脆弱性パターンと対策

      ___CODE_BLOCK_64___

      3.9 本章のまとめ

      3.9.1 学習した理論

    • 形式言語理論
    - アルファベット、文字列、言語の数学的定義 - チョムスキー階層 - 有限オートマトンと正規表現

    • 文字列アルゴリズム
    - ナイーブ法(O(nm)) - KMP法(O(n+m)) - Boyer-Moore法(O(n/m) 平均) - Rabin-Karp法(ハッシュベース) - Aho-Corasick法(複数パターン) - Suffix Array(高度なテキスト処理)

    • C言語の文字列
    - ヌル終端文字列の設計思想 - メモリレイアウトと安全性 - 歴史的なセキュリティ問題

    3.9.2 実装したLibft関数

    | 関数 | 用途 | 計算量 | |------|------|--------| | ft_strlen | 文字列長 | O(n) | | ft_strcpy | コピー(危険) | O(n) | | ft_strlcpy | 安全なコピー | O(n) | | ft_strcat | 連結(危険) | O(n+m) | | ft_strlcat | 安全な連結 | O(n+m) | | ft_strcmp | 比較 | O(n) | | ft_strncmp | n文字比較 | O(n) | | ft_strchr | 文字検索 | O(n) | | ft_strrchr | 逆方向検索 | O(n) | | ft_strnstr | 部分文字列検索 | O(nm) | | ft_strdup | 複製 | O(n) | | ft_substr | 部分文字列 | O(n) | | ft_strjoin | 結合 | O(n+m) | | ft_strtrim | トリム | O(n) | | ft_split | 分割 | O(n) | | ft_itoa | 整数→文字列 | O(log n) | | ft_strmapi | マップ | O(n) | | ft_striteri | イテレート | O(n) |

    3.9.3 次のステップ

    次章では、メモリ操作関数を学びます:

    • メモリ階層とキャッシュの仕組み
    • 仮想メモリとページング
    • ポインタの深い理解
    • ft_memset, ft_bzero, ft_memcpy, ft_memmove の実装

    文字列操作とメモリ操作は密接に関連しており、両方の理解がより効率的で安全なコードを書く基礎となります。