【C++】std::strtokは非推奨?モダンC++での文字列分割方法を徹底解説

2025-06-01

std::strtok の基本的な使い方と特徴

  1. 文字列の分割(トークン化): strtok は、与えられた文字列を、指定された区切り文字(デリミタ)で区切られた「トークン」と呼ばれる部分文字列に分割します。

  2. 破壊的な操作: strtok の最も重要な特徴の一つは、元の文字列を破壊的に変更するという点です。見つかったトークンの終端にヌル終端文字(\0)を書き込むことで、元の文字列を書き換えます。そのため、文字列リテラル("Hello World"のような変更できない文字列)を第一引数に渡すことはできません。変更可能なchar配列を渡す必要があります。

  3. 状態を保持する: strtok は内部的に静的なポインタを保持しており、これを使って次のトークンを探し続けます。

    • 最初の呼び出しでは、トークン化したい文字列へのポインタを第一引数に渡します。
    • 2回目以降の呼び出しでは、同じ文字列の続きからトークンを探すために、第一引数に NULL を渡します。
  4. 複数の区切り文字: 区切り文字は1文字だけでなく、複数の文字を指定できます。例えば、" ,;." のように指定すると、スペース、カンマ、セミコロン、ピリオドのいずれかが出現するたびにトークンが区切られます。

  5. 返り値:

    • 次のトークンが見つかった場合、そのトークンの先頭へのポインタを返します。
    • もうトークンが見つからない場合、NULL を返します。
  6. スレッドセーフではない: 内部で静的な状態(ポインタ)を保持しているため、複数のスレッドから同時に strtok を呼び出すと、予期せぬ動作を引き起こす可能性があります。マルチスレッド環境では、スレッドセーフな strtok_r (POSIX) や strtok_s (C11) などの代替関数、または std::string を使ったC++らしい文字列操作(std::string::findstd::substrstd::getlineなど)を使用することが推奨されます。

使用例

#include <iostream>
#include <cstring> // strtok を使用するために必要

int main() {
    char str[] = "apple,banana;cherry.grape"; // 変更可能な char 配列
    const char* delimiters = ",;.";          // 区切り文字のセット

    char* token = strtok(str, delimiters); // 最初のトークンを取得

    // トークンがなくなるまでループ
    while (token != nullptr) {
        std::cout << "トークン: " << token << std::endl;
        token = strtok(nullptr, delimiters); // NULL を渡し、次のトークンを取得
    }

    // 元の文字列 str がどうなっているか確認
    std::cout << "元の文字列(変更後): " << str << std::endl;
    // 結果は "apple" となるはずです。なぜなら、最初の区切り文字である ',' が '\0' に置き換えられているためです。

    return 0;
}

出力例

トークン: apple
トークン: banana
トークン: cherry
トークン: grape
元の文字列(変更後): apple
  • C++的なアプローチ: C++では、通常、std::string とアルゴリズム(std::string::findstd::string::substr)を組み合わせて文字列を分割したり、std::stringstream を利用してストリームとして文字列を扱う方が一般的で安全です。
  • 安全性の問題: スレッドセーフでないことや、バッファオーバーフローのリスクがあることから、C++ではより安全で柔軟な std::string クラスのメソッド(find, substr)や、std::stringstreamstd::getline などを使用することが推奨されます。
  • 元の文字列の破壊: 上記の例で示したように、元の文字列が変更されるため、元の文字列を保持したい場合は、strtok を呼び出す前にコピーを作成する必要があります。

例えば、std::stringstd::string::findstd::string::substr を使って同様の処理を行う場合は以下のようになります。

#include <iostream>
#include <string>
#include <vector> // 分割したトークンを保存する場合

int main() {
    std::string s = "apple,banana;cherry.grape";
    std::string delimiters = ",;.";
    std::vector<std::string> tokens;

    size_t lastPos = 0;
    size_t foundPos = s.find_first_of(delimiters, lastPos);

    while (foundPos != std::string::npos) {
        tokens.push_back(s.substr(lastPos, foundPos - lastPos));
        lastPos = foundPos + 1;
        foundPos = s.find_first_of(delimiters, lastPos);
    }
    tokens.push_back(s.substr(lastPos)); // 最後のトークンを追加

    for (const auto& token : tokens) {
        std::cout << "トークン: " << token << std::endl;
    }

    std::cout << "元の文字列(変更なし): " << s << std::endl; // 変更されない

    return 0;
}


    • エラー: strtok の第一引数は char* (非定数ポインタ) を期待しますが、std::string::c_str() や文字列リテラル ("Hello World") は const char* を返します。そのため、コンパイルエラー(invalid conversion from 'const char*' to 'char*')が発生します。
    • 原因: strtok は元の文字列を破壊的に変更するため、変更できない const な文字列を受け入れることができません。
    • トラブルシューティング:
      • 文字列を char 配列にコピーして渡す。
        #include <cstring> // strcpy, strlen 用
        #include <iostream>
        #include <string>
        
        int main() {
            std::string s = "apple,banana,cherry";
            // std::string の内容を、変更可能な char 配列にコピーする
            char* c_str = new char[s.length() + 1];
            std::strcpy(c_str, s.c_str());
        
            char* token = strtok(c_str, ",");
            while (token != nullptr) {
                std::cout << token << std::endl;
                token = strtok(nullptr, ",");
            }
            delete[] c_str; // メモリ解放を忘れない
            return 0;
        }
        
      • そもそも std::string の機能 (find, substr など) や std::stringstream を使うことを検討する。これがC++においてはより推奨される方法です。
  1. 元の文字列が変更されてしまうことを理解していない

    • エラー: strtok を呼び出した後、元の char 配列の内容が意図せず変更されていることに気づく。
    • 原因: strtok はトークンの区切り文字をヌル文字 (\0) に置き換えることで文字列を破壊します。
    • トラブルシューティング:
      • 元の文字列のコピーが必要な場合は、strtok を呼び出す前に strcpy などで別の char 配列にコピーしておく。
      • 変更が許されない場合は、strtok の使用を避け、std::string の非破壊的なメソッドや std::stringstream を使う。
  2. マルチスレッド環境での使用

    • エラー: 複数のスレッドから同時に strtok を呼び出すと、予期せぬ結果になったり、クラッシュしたりする。
    • 原因: strtok は内部で静的なポインタ(状態)を保持しており、この静的変数がスレッド間で共有されるため、競合状態 (race condition) が発生します。
    • トラブルシューティング:
      • strtok_r (POSIX) を使用する: これはスレッドセーフなバージョンで、内部状態を引数として渡すため、スレッドごとに独立したトークン化が可能です。
        #include <iostream>
        #include <cstring> // strtok_r 用
        
        int main() {
            char str[] = "one two three";
            char* saveptr; // 状態を保存するためのポインタ
        
            char* token = strtok_r(str, " ", &saveptr);
            while (token != nullptr) {
                std::cout << "トークン: " << token << std::endl;
                token = strtok_r(nullptr, " ", &saveptr);
            }
            return 0;
        }
        
      • strtok_s (C11/Microsoft固有) を使用する: strtok_r と同様にスレッドセーフなバージョンです。
      • C++の標準ライブラリ(std::string, std::stringstream を使用する: これらが最も安全で推奨されるアプローチです。これらは通常スレッドセーフな設計になっています。
  3. 連続する区切り文字や先頭・末尾の区切り文字の扱い

    • 動作: strtok は連続する区切り文字を1つの区切りとして扱います。また、先頭や末尾の区切り文字は無視されます。
    • : "apple,,banana", で分割すると apple, banana となり、空のトークンは生成されません。",apple,banana," も同様です。
    • トラブルシューティング:
      • この動作が意図通りであれば問題ありません。
      • 空のトークンも抽出したい場合は、strtok ではなく、std::string::findstd::string::substr を組み合わせたり、std::getline (std::stringstream と組み合わせて) を使う必要があります。
  4. NULL を渡し忘れる

    • エラー: 2回目以降の strtok 呼び出しで、第一引数に NULL を渡すべきところで元の文字列ポインタを渡してしまうと、再度最初からトークン化が開始されてしまい、意図しない結果になります。
    • 原因: strtok は内部状態を利用して、次のトークンを探し続けるため、2回目以降の呼び出しでは NULL を渡すことでその内部状態を参照させます。
    • トラブルシューティング: ループ内で strtok(nullptr, delimiters) の形式で呼び出されているか確認する。
  5. メモリリーク (動的に確保した文字列の場合)

    • エラー: new char[] で動的にメモリを確保して strtok に渡した場合、delete[] を忘れるとメモリリークが発生します。
    • トラブルシューティング: 動的に確保したメモリは、使用後に必ず delete[] で解放する。std::unique_ptr<char[]> のようなスマートポインタを使うと、解放忘れを防ぐことができます。

上記のエラーやトラブルシューティングのポイントからもわかるように、std::strtok は以下の点で現代のC++プログラミングには適していません。

  • 機能の限界: 空のトークンを扱えない、複数の文字列を同時にトークン化できない(strtok_r/strtok_s を除く)など、機能的な制限があります。
  • C++のイディオムからの乖離: std::string やその関連機能の方が、より安全で直感的、かつ柔軟な文字列操作を提供します。メモリ管理も自動で行われるため、開発者の負担が少ないです。
  • 安全性の欠如: 元の文字列を破壊的に変更する、スレッドセーフでない、バッファオーバーフローのリスクがあるなど。


std::strtok の基本的な使い方

strtok は、文字列を特定の区切り文字で分割(トークン化)するために使用されます。

構文
char* strtok(char* str, const char* delimiters);

  • delimiters: 区切り文字のセットを含む文字列へのポインタ。
  • str: トークン化したい文字列へのポインタ。最初の呼び出しでは分割対象の文字列を指定し、2回目以降は NULL を指定します。

戻り値

  • もうトークンが見つからない場合、nullptr (C++11以降) または NULL を返します。
  • 次のトークンが見つかった場合、そのトークンの先頭へのポインタを返します。

重要な注意点

  • strtok は、元の文字列を破壊的に変更します(区切り文字をヌル文字 \0 に置き換えます)。そのため、変更できない const char* や文字列リテラルを直接渡すことはできません。変更可能な char 配列を渡す必要があります。

例1: 基本的な文字列のトークン化

最も典型的な strtok の使用例です。

#include <iostream>
#include <cstring> // strtok を使用するために必要

int main() {
    // 変更可能な char 配列を準備する
    char str[] = "apple,banana;cherry.grape";
    const char* delimiters = ",;."; // 区切り文字のセット

    std::cout << "元の文字列: " << str << std::endl;

    // 最初のトークンを取得
    char* token = std::strtok(str, delimiters);

    // トークンがなくなるまでループ
    while (token != nullptr) {
        std::cout << "  トークン: " << token << std::endl;
        // NULL を渡し、次のトークンを取得(内部状態を利用)
        token = std::strtok(nullptr, delimiters);
    }

    // strtok によって元の文字列が変更されていることを確認
    // 最初の区切り文字 (ここでは ',') が '\0' に置き換えられているため、
    // ここでは "apple" だけが表示される
    std::cout << "strtok 後の元の文字列: " << str << std::endl;

    return 0;
}

実行結果

元の文字列: apple,banana;cherry.grape
  トークン: apple
  トークン: banana
  トークン: cherry
  トークン: grape
strtok 後の元の文字列: apple

例2: std::stringstrtok で処理する(注意が必要)

std::string を直接 strtok に渡すことはできません。std::string の内容を一時的に char 配列にコピーする必要があります。

#include <iostream>
#include <string>
#include <cstring> // strtok, strcpy を使用するために必要

int main() {
    std::string s = "one two three four";
    const char* delimiters = " "; // スペースで区切る

    // std::string の内容を、変更可能な char 配列にコピーする
    // s.length() + 1 はヌル終端文字のためのスペース
    char* c_str = new char[s.length() + 1];
    std::strcpy(c_str, s.c_str()); // std::string::c_str() は const char* を返す

    std::cout << "元の std::string: " << s << std::endl;

    char* token = std::strtok(c_str, delimiters);

    while (token != nullptr) {
        std::cout << "  トークン: " << token << std::endl;
        token = std::strtok(nullptr, delimiters);
    }

    std::cout << "strtok 後のコピーされた文字列: " << c_str << std::endl; // 変更されている
    std::cout << "元の std::string: " << s << std::endl; // こちらは変更されない

    // 動的に確保したメモリの解放を忘れない
    delete[] c_str;

    return 0;
}

実行結果

元の std::string: one two three four
  トークン: one
  トークン: two
  トークン: three
  トークン: four
strtok 後のコピーされた文字列: one
元の std::string: one two three four

例3: スレッドセーフな strtok_r (POSIX)

strtok は内部で静的な状態を保持するため、マルチスレッド環境では競合状態 (race condition) が発生し、危険です。POSIX システム (Linux, macOSなど) では、スレッドセーフな strtok_r が提供されています。

構文
char* strtok_r(char* str, const char* delimiters, char** saveptr);

  • saveptr: strtok_r が内部状態を保存するための char* ポインタへのポインタ。このポインタをスレッドごとに独立して保持することで、スレッドセーフになります。
  • delimiters: 区切り文字のセット。
  • str: トークン化したい文字列。最初の呼び出しで指定し、2回目以降は NULL
#include <iostream>
#include <cstring> // strtok_r を使用するために必要

// この関数は、strtok_r を安全に使用する方法を示すためのものです。
// 実際のアプリケーションでは、マルチスレッド環境での使用を想定します。
void processString(char* text, const char* delim) {
    char* saveptr; // 各呼び出し (またはスレッド) ごとに独立した saveptr

    char* token = strtok_r(text, delim, &saveptr);
    while (token != nullptr) {
        std::cout << "  [処理中] トークン: " << token << std::endl;
        token = strtok_r(nullptr, delim, &saveptr);
    }
}

int main() {
    char str1[] = "alpha beta gamma";
    char str2[] = "10,20,30,40";

    std::cout << "--- 最初の文字列の処理 ---" << std::endl;
    processString(str1, " ");

    std::cout << "--- 2番目の文字列の処理 ---" << std::endl;
    processString(str2, ",");

    // str1 と str2 はそれぞれ独立してトークン化され、
    // strtok_r の内部状態が干渉しないことがわかります。
    return 0;
}

実行結果

--- 最初の文字列の処理 ---
  [処理中] トークン: alpha
  [処理中] トークン: beta
  [処理中] トークン: gamma
--- 2番目の文字列の処理 ---
  [処理中] トークン: 10
  [処理中] トークン: 20
  [処理中] トークン: 30
  [処理中] トークン: 40

std::strtok は、C言語との互換性のために存在しますが、現代のC++ではあまり推奨されません。その理由は、元の文字列を破壊すること、スレッドセーフでないこと、そしてより安全で柔軟な代替手段が存在するからです。

std::string::find() と std::string::substr() を使用する方法

元の文字列を破壊せず、最も柔軟な方法の一つです。

#include <iostream>
#include <string>
#include <vector> // 分割したトークンを保存するために使用

int main() {
    std::string s = "apple,banana;cherry.grape";
    std::string delimiters = ",;."; // 区切り文字のセット

    std::vector<std::string> tokens;
    size_t lastPos = 0; // 検索開始位置
    size_t foundPos = s.find_first_of(delimiters, lastPos); // 最初の区切り文字を見つける

    // 区切り文字が見つかる限りループ
    while (foundPos != std::string::npos) {
        // 見つかった区切り文字までの部分文字列をトークンとして抽出
        tokens.push_back(s.substr(lastPos, foundPos - lastPos));
        lastPos = foundPos + 1; // 次の検索開始位置は区切り文字の次から
        foundPos = s.find_first_of(delimiters, lastPos); // 次の区切り文字を見つける
    }
    // 最後のトークン(または区切り文字がない場合の文字列全体)を追加
    tokens.push_back(s.substr(lastPos));

    std::cout << "元の文字列: " << s << std::endl;
    std::cout << "分割されたトークン:" << std::endl;
    for (const auto& token : tokens) {
        std::cout << "  " << token << std::endl;
    }

    return 0;
}

std::stringstream と std::getline() を使用する方法

特定の区切り文字で、ストリームのように文字列を読み込みたい場合に非常に便利です。ただし、この方法は単一の区切り文字にしか対応していません。複数の区切り文字を扱う場合は、事前に文字列を加工するか、上記の方法を組み合わせる必要があります。

#include <iostream>
#include <string>
#include <sstream> // std::stringstream を使用するために必要
#include <vector>

int main() {
    std::string s = "item1,item2,item3,item4";
    char delimiter = ','; // 単一の区切り文字

    std::stringstream ss(s); // 文字列をstringstreamに変換
    std::string segment;
    std::vector<std::string> tokens;

    std::cout << "元の文字列: " << s << std::endl;
    std::cout << "分割されたトークン:" << std::endl;

    // getline を使って、指定された区切り文字までを読み込む
    while (std::getline(ss, segment, delimiter)) {
        tokens.push_back(segment);
        std::cout << "  " << segment << std::endl;
    }

    return 0;
}


ここでは、std::strtok の主要な代替方法をいくつかご紹介します。

std::string::find() と std::string::substr() を組み合わせる

最も一般的で柔軟な方法の一つです。std::string のメンバー関数を使って、文字列内で区切り文字を検索し、その間の部分文字列を抽出します。元の文字列は変更されません。

利点

  • std::string を直接扱うため、メモリ管理の心配がない。
  • 空のトークン(例: "a,,b" の間の空文字列)を抽出するかどうかを制御できる。
  • 複数の区切り文字を柔軟に指定できる。
  • 元の文字列を破壊しない。

欠点

  • コードがやや冗長になることがある。

コード例

#include <iostream>
#include <string>
#include <vector> // 分割したトークンを保存するために使用

// 文字列を複数の区切り文字で分割する関数
std::vector<std::string> splitString(const std::string& str, const std::string& delimiters) {
    std::vector<std::string> tokens;
    size_t lastPos = 0; // 検索開始位置
    // find_first_of: delimitersに含まれる文字が最初に見つかる位置を検索
    size_t foundPos = str.find_first_of(delimiters, lastPos);

    while (foundPos != std::string::npos) {
        // lastPos から foundPos までの部分文字列を抽出
        tokens.push_back(str.substr(lastPos, foundPos - lastPos));
        lastPos = foundPos + 1; // 次の検索開始位置は区切り文字の次から
        foundPos = str.find_first_of(delimiters, lastPos); // 次の区切り文字を見つける
    }
    // 最後のトークン(または区切り文字がない場合の文字列全体)を追加
    // この行がないと、最後のトークンが抽出されない
    tokens.push_back(str.substr(lastPos));

    return tokens;
}

int main() {
    std::string text1 = "apple,banana;cherry.grape";
    std::string delimiters1 = ",;.";

    std::cout << "--- 複数区切り文字での分割 ---" << std::endl;
    std::vector<std::string> result1 = splitString(text1, delimiters1);
    for (const auto& token : result1) {
        std::cout << "トークン: \"" << token << "\"" << std::endl;
    }
    std::cout << "元の文字列は変更されていません: \"" << text1 << "\"" << std::endl;

    std::cout << "\n--- 空のトークンを含む場合 ---" << std::endl;
    std::string text2 = "one,,two,three,"; // 連続するカンマと末尾のカンマ
    std::string delimiters2 = ",";
    std::vector<std::string> result2 = splitString(text2, delimiters2);
    for (const auto& token : result2) {
        std::cout << "トークン: \"" << token << "\"" << std::endl;
    }
    // strtokとは異なり、この方法では空のトークンも抽出されます。
    // (例: "one", "", "two", "three", "")

    return 0;
}

std::stringstream と std::getline() を組み合わせる

文字列をストリームとして扱い、特定の区切り文字(通常は1文字)で読み込む方法です。ファイルから行を読み込むのと似た感覚で使えます。

利点

  • 主に単一の区切り文字に対して非常に効率的。
  • 元の文字列を破壊しない。
  • コードが簡潔で読みやすい。

欠点

  • 連続する区切り文字を「空のトークン」として扱わない場合、追加の処理が必要。
  • 複数の区切り文字を直接指定できない(事前に文字列を加工する必要がある場合も)。

コード例

#include <iostream>
#include <string>
#include <sstream> // std::stringstream を使用するために必要
#include <vector>

int main() {
    std::string data = "value1,value2,value3,value4";
    char delimiter = ','; // 単一の区切り文字

    std::stringstream ss(data); // 文字列をstringstreamに変換
    std::string segment;
    std::vector<std::string> tokens;

    std::cout << "--- std::getline で分割 ---" << std::endl;
    // getline を使って、指定された区切り文字までを読み込む
    while (std::getline(ss, segment, delimiter)) {
        tokens.push_back(segment);
        std::cout << "トークン: \"" << segment << "\"" << std::endl;
    }
    // 最後の部分文字列が区切り文字で終わらない場合、最後のトークンが追加されていない可能性があるため注意が必要。
    // その場合、ループ後に ss.str().substr(ss.tellg()) で残りを取得するなどの対応が必要になる。
    // しかし、上記の例のように文字列が区切り文字で終わらない場合は、最後のトークンも自動的に処理される。

    std::cout << "元の文字列は変更されていません: \"" << data << "\"" << std::endl;

    // 連続する区切り文字の挙動
    std::cout << "\n--- 連続する区切り文字の挙動 ---" << std::endl;
    std::string data2 = "field1::field2:::field3";
    std::stringstream ss2(data2);
    std::string segment2;
    // 空のトークンも抽出される
    while (std::getline(ss2, segment2, ':')) {
        std::cout << "トークン: \"" << segment2 << "\"" << std::endl;
    }

    return 0;
}

正規表現 (<regex>) を使用する

C++11から導入された正規表現ライブラリ (<regex>) を使用すると、非常に複雑なパターンでの文字列分割も可能です。

利点

  • 複数の区切り文字、または特定のパターンを区切りとして指定できる。
  • 元の文字列を破壊しない。
  • 非常に強力で柔軟なパターンマッチングが可能。

欠点

  • std::regex は標準ライブラリだが、一部のコンパイラではパフォーマンスが最適化されていないことがある。
  • コードがやや複雑になり、正規表現の知識が必要。
  • パフォーマンスが他の方法よりも劣る場合がある。
#include <iostream>
#include <string>
#include <regex> // 正規表現ライブラリを使用するために必要
#include <vector>

int main() {
    std::string text = "apple,banana;cherry.grape-kiwi";
    // カンマ、セミコロン、ピリオド、ハイフンのいずれかを区切り文字とする正規表現
    std::regex delimiters_regex("[,;.\\-]");

    std::sregex_token_iterator iter(text.begin(), text.end(), delimiters_regex, -1);
    std::sregex_token_iterator end;

    std::cout << "--- 正規表現での分割 ---" << std::endl;
    for (; iter != end; ++iter) {
        std::cout << "トークン: \"" << *iter << "\"" << std::endl;
    }
    std::cout << "元の文字列は変更されていません: \"" << text << "\"" << std::endl;

    // 空のトークンの扱い (regex_token_iteratorの最後の引数を0にすると区切り文字自体がトークンになる)
    std::cout << "\n--- 空のトークンと正規表現 ---" << std::endl;
    std::string text2 = "one,,two";
    std::regex delimiters_regex2(",");
    // -1 は区切り文字の間の文字列(トークン)を抽出
    // 0 は区切り文字自体をトークンとして抽出
    std::sregex_token_iterator iter2(text2.begin(), text2.end(), delimiters_regex2, -1);
    std::sregex_token_iterator end2;
    for (; iter2 != end2; ++iter2) {
        std::cout << "トークン: \"" << *iter2 << "\"" << std::endl;
    }

    return 0;
}
  • 非常に複雑なパターンで分割したい場合
    std::regex を使用する方法。ただし、パフォーマンスと複雑さを考慮する必要があります。
  • 単一の区切り文字で簡潔に分割したい場合
    std::stringstreamstd::getline() を組み合わせる方法。
  • 最も一般的で柔軟なアプローチ
    std::string::find()std::string::substr() を組み合わせる方法。これは、ほとんどの文字列分割の要件に対応できます。