【C言語】strtok関数の使い方と注意点(文字列を区切り文字で分離する関数)

strtok関数の使い方の解説ページアイキャッチ

このページでは、C言語の標準関数である strtok 関数について解説していきます!

strtok 関数は文字列を区切り文字で分離する関数です。strtok 関数を利用することで、例えば下記のような文字列をカンマ(',')で区切った文字列に分離するようなことができます。

April,May,June,July,Octobor

分離後の文字列は下記のようになります。

  • April
  • May
  • June
  • July
  • Octobor

一見便利そうな関数ですが、この関数、正直かなりクセが強いです…。他の関数に比べると使い方が難しいですし、注意すべきことも多いです。

このページでは strtok 関数と strtok 関数の動作、さらにその動作から考察できる strtok 関数の注意点について解説していきます。

strtok 関数

strtok 関数は、指定した区切り文字で文字列を分離する関数です。

ただし、1回の strtok 関数で分離できるのは、”文字列の先頭から最初の区切り文字の直前の文字まで” のみです。

さらに文字列を分離したい場合は、複数回 strtok 関数を実行する必要があります。

例えば "abc+def+ghi" を区切り文字 '+' で分離する場合、最初の strok 関数実行時に取得できるのは "abc" のみになります。さらに、2回目に strtok 関数を実行すると "def" が、3回目に strtok 関数を実行すると "ghi" が取得できます。 

また、初回の strtok 関数実行と2回目以降の strtok 関数実行時で引数を変更したりする必要があって結構ややこしいです。

この辺りも踏まえて、strtok 関数の解説をしていきたいと思います。

strtok 関数の定義

strtok 関数の定義ファイル、関数定義は下記の通りです。

strtok関数
#include <string.h>
char* strtok(char* str1, const char* str2);

スポンサーリンク

strtok 関数の引数

strtok 関数の第1引数 str1 には、”分離を行いたい文字列” が格納された配列やメモリのアドレスを指定します。

複数回同じ文字列に対して strtok 関数を実行する場合は、2回目以降は str1NULL を指定します。

第2引数 str2 には、第1引数 str1 を分離する際の “区切り文字” の文字列が格納された配列やメモリのアドレスを指定します。

例えば a を区切りにして文字列を分離したいのであれば、第2引数には "a" を指定します('a' ではないことに注意)。また abc を区切りにして文字列を分離したいのであれば、"abc" を指定します。

strtok 関数の返却値

文字列が分離できた場合、分離後の文字列の先頭アドレスを返却します。

文字列が分離できない場合、NULL を返却します。

NULL が返却される具体的なケースについては、strtok 関数の動作で解説します。

strtok 関数の基本的な使い方

strtok 関数の基本的な使い方の例は下記のようになります。

strtok関数の基本的な使い方
#include <stdio.h>
#include <string.h>

int main(void) {
    char str[] = "aa,bb,cc,dd"; /* 分離する文字列 */
    char delim[] = ","; /* 区切り文字 */
    char *token; /* 分離後の文字列を指すポインタ */
    
    /* 文字列を分離 */
    token = strtok(str, delim);

    /* 文字列が分離できなくなるまでstrtokを実行 */
    while (token != NULL) {
        /* 分離後の文字列を表示 */
        printf("%s\n", token);

        /* 文字列を分離 */
        token = strtok(NULL, delim);
    }

    return 0;
}

実行すると、下記のように "aa,bb,cc,dd" が ',' で分離した状態で表示されます。

aa
bb
cc
dd

前述の通り、同じ文字列に対して分離を何回も行いたい場合、1回目と2回目以降で第1引数を変更する必要があります。

そのため、ループで strtok 関数を実行したい場合は、上記のように1回目の strtok をループ外で実行した後、2回目以降の strtok はループ内で実行するように記述するのが良いと思います。

スポンサーリンク

strtok 関数の動作

続いて、strtok 関数が文字列分離をどのようにして行なっているのか?について解説していきたいと思います。

これを理解することで、strtok 関数の返却値の具体的な意味や、2回目以降に第1引数に NULL を指定する理由、 strtok 関数の注意点等が見えてきます。

一点補足しておくと、ここで解説する strtok 関数の動作は、私が strtok 関数を実際に使用してみて確認した strtok 関数の処理結果から “推測したもの” になります。ソースコードを読んで確認したというわけではありません。

細かい点が間違っていたり、環境によって挙動が異なったりする可能性があるので注意してください。

また、図で補足しながら説明していきますが、図では区切り文字を '+' のみとして説明していますので、このことを頭に入れて図を解釈していただけると助かります。

strtok 関数の基本的な動作

strtok 関数の基本的な動作は「”分離開始アドレス” から最初に見つけた区切り文字をヌル文字 '\0' に置き換えてから “分離開始アドレス” を返却する」です。

strtok関数の動作を1枚絵で示した図

MEMO

“分離開始アドレス” は strtok 関数の内部で使用する分離処理を開始するアドレスです

おそらくこのアドレスを考慮した方が strtok 関数の動作が分かりやすいと思います

また “分離開始アドレス” という名前は、説明しやすくするために私が便宜上名付けたものであり、一般的な用語ではないので注意してください

strtok 関数の第1引数 str1NULL 以外を指定した場合、”分離開始アドレス” は、str1 になります。

そして、この場合は str1 から順に後ろ側に向かって第2引数 str2 で指定された区切り文字(のいずれか)を探索することになります。

strtok関数が区切り文字を探索する様子

もし区切り文字が見つかった場合、strtok 関数は最初に見つけた区切り文字をヌル文字 '\0' に置き換え、”分離開始アドレス” を返却して終了します。今後、この strtok 関数の返却アドレスを token としたいと思います。

strtok関数が、最初に見つけた区切り文字をヌル文字に置き換え、分離開始アドレスを返却する様子

ここで token の指す文字列に注目すると、ヌル文字 '\0' は文字列の終端を表すわけですから、token が指す文字列は “分離開始アドレスの位置の文字” 〜 “元々区切り文字であった文字の直前” となります。したがって、token は区切り文字で分離された文字列として扱うことができ、これにより区切り文字で分離した文字列が1つ得られたことになります。

tokenの指す文字列が区切り文字で分離した文字列になっていることを示す図

ただし、1回の strtok 関数の実行で得られる分離後の文字列は1つのみです。さらに分離後の文字列を取得したい場合、再度 strtok 関数を実行する必要があります。

1回のstrtok関数の実行では文字列がまだ分離しきれていない様子

次の分離後の文字列を得るためには、前述の通り、strtok 関数の第1引数には NULL を指定する必要があります。

NULL を指定する理由としてポイントになるのが、前回の strtok 関数の実行により str1 が指す文字列の最初の区切り文字がヌル文字に置き換えられている点です。最初の区切り文字がヌル文字に置き換えられているわけですから、str1 が指す文字列に区切り文字は存在しないことになります。

分離後の文字列に区切り文字が存在しないことを示す図

したがって、もし次の分離文字列を取得するために strtok 関数の第1引数に str1 を再び指定してしまうと、区切り文字が見つかる前に文字列の終端まで探索が行われることになります。

区切り文字が見つかる前に文字列の終端まで探索された場合、strtok 関数は何もせずに分離開始アドレス(この場合は str1)を返却して終了します。

すなわち、strtok 関数の返却アドレスである token が指す文字列は1回目に実行した時と同じものになることになり、次の分離後の文字列を取得することができません。

なので、本当は strtok 関数の第1引数としては、先ほど strtok 関数でヌル文字に置き換えられた文字の1文字後ろ位置のアドレスを指定する必要があります。

2回目のstrtok関数実行時に第1引数に指定すべきアドレスを示す図

でも、わざわざこのアドレスを指定するのは面倒ですよね?

なので strtok 関数では、第1引数で NULL を指定すれば、直前にヌル文字に置き換えた位置の1文字後ろの位置のアドレスを “分離開始アドレス” に自動的に設定するようになっています。このようなことが可能なのは、strtok 関数の内部で、次に実行された時の “分離開始アドレス” を保持してくれているからです。

2回目のstrtok関数実行時に第1引数にNULLを指定した時の分離開始アドレスを示す図

そして、初回の strtok 関数実行時のように、”分離開始アドレス”(つまり、前回ヌル文字に置き換えた位置の1文字後ろの位置のアドレス)から最初に見つけた区切り文字をヌル文字に置き換え、”分離開始アドレス” を返却します。

2回目のstrtok関数実行時の処理を示す図

なので、返却されたアドレス token が指す文字列は、区切り文字で分離した2つ目の文字列として扱うことができます。

2回目のstrtok関数の返却値tokenが区切り文字で分離された文字列になっている様子を示す図

これ以降は、第1引数に NULL を指定して strtok 関数を実行すると同様の動作をしてくれます。ですので、strtok 関数が NULL を返却するまでループしてやれば、区切り文字で分離した文字列全てを取得することができることになります(どんな時に NULL が返却されるかは、次の strtok 関数の例外的な動作で説明します)。

基本的な strtok 関数の動作はこんな感じだと思います。

strtok 関数の例外的な動作

ただ、例外的に上記とは異なる動作をするケースもあります。

分離開始アドレスの文字が区切り文字である場合

1つ目のケースは、”分離開始アドレス” の文字が区切り文字である場合です。この場合、”分離開始アドレス” を、次に最初に現れる “区切り文字以外の文字” の位置のアドレスに更新してから探索が行われます。

分離開始アドレスの指す文字が区切り文字の場合に、分離開始アドレスが次に現れる区切り文字以外の位置に移動される様子を示す図

ですので、例えば区切り文字が連続して現れるような文字列の場合も、分離後の文字列が空文字列となるようなケースは存在しません。

区切り文字が見つからなかった場合

2つ目のケースは、区切り文字が見つからなかった場合です(区切り文字より前にヌル文字が見つかった場合)。この場合、strtok 関数は単純に “分離開始アドレス” の返却のみを行います。ヌル文字への置き換えは行いません。

そしてこの場合、次に第1引数に NULL を指定して strtok 関数を実行した場合の “分離開始アドレス” は NULL になるようです。

分離開始アドレスの指す文字がヌル文字である場合

3つ目のケースは “分離開始アドレス” がヌル文字を指しているケースです。この場合、strtok 関数は NULL を返却します。

ですので、”分離開始アドレス” から始まる文字列が空文字列である場合は strtok 関数は NULL を返却します。

また、”分離開始アドレス” から始まる文字列に “区切り文字しか存在しない” 場合も strtok 関数は NULL を返却します。

これは、分離開始アドレスの指す文字が区切り文字である場合で説明したように、”分離開始アドレス” の指す文字が “区切り文字” である場合、次に最初に現れる “区切り文字以外の文字” の位置のアドレスに “探索位置アドレス” が更新されるからです。

strtok関数がNULLを返却する場合2

分離開始アドレスが NULL である場合

4つ目のケースは “分離開始アドレス” が NULL であるケースです(ヌル文字じゃないよ)。この場合も、strtok 関数は NULL を返却します。

区切り文字が見つからなかった場合で説明したように、前回の strtok 関数実行時に区切り文字が見つからなかった場合、次回の strtok 関数実行時の “分離開始アドレス” は NULL となります。

したがって、前回の strtok 関数実行時に区切り文字が見つからなかった場合、次回の strtok 関数実行時の返却アドレスは必ず NULL となります。

スポンサーリンク

strtok 関数の注意点

strtok 関数の動作はなんとなく理解できたでしょうか?次は、上記の動作から考察される strtok 関数の注意点について解説していきたいと思います。

第1引数に指定した文字列は strtok 関数内部で変更される

strtok 関数の動作で解説した通り、第1引数が指す文字列に存在する区切り文字は strtok 関数内部でヌル文字に置き換えられます。

したがって、変更されたくない文字列に対して strtok 関数は実行しないように注意してください。

変更されたくない文字列に対して strtok を実行しなければならない場合は、一旦他のメモリや配列にその文字列をコピーし、コピーした文字列に対して strtok 関数を実行するようにしましょう。

例えば、下記のように strtok 関数実行後に strprintf で表示したとしても、元々 str が指していた文字列を表示することはできません(下記の場合は "aa,bb,cc,dd" ではなく "aa" が表示されます)。

文字列がstrtok関数内で変更される例
#include <stdio.h>
#include <string.h>

int main(void) {
    char str[] = "aa,bb,cc,dd"; /* 分離する文字列 */
    char delim[] = ","; /* 区切り文字 */
    char *token; /* 分離後の文字列を指すポインタ */
    
    /* 文字列を分離 */
    token = strtok(str, delim);

    /* 文字列が分離できなくなるまでstrtokを実行 */
    while (token != NULL) {
        /* 分離後の文字列を表示 */
        printf("分離後の文字列:%s\n", token);

        /* 文字列を分離 */
        token = strtok(NULL, delim);
    }

    printf("分離前の文字列:%s\n", str); /* aaが表示される */

    return 0;
}

下記のように、事前に他の配列にコピーし、コピー先の文字列に対して strtok 関数を実行すれば、元々の文字列をそのまま表示することができます。

コピー後の文字列をstrtokに指定する
#include <stdio.h>
#include <string.h>

int main(void) {
    char str[] = "aa,bb,cc,dd"; /* 分離する文字列 */
    char delim[] = ","; /* 区切り文字 */
    char *token; /* 分離後の文字列を指すポインタ */

    char copy[256];

    strcpy(copy, str);
    
    /* 文字列を分離 */
    token = strtok(copy, delim);

    /* 文字列が分離できなくなるまでstrtokを実行 */
    while (token != NULL) {
        /* 分離後の文字列を表示 */
        printf("分離後の文字列:%s\n", token);

        /* 文字列を分離 */
        token = strtok(NULL, delim);
    }

    printf("分離前の文字列:%s\n", str); /* aa,bb,cc,ddが表示される */

    return 0;
}

第1引数に読み取り専用の文字列を指定してはいけない

また、strtok 関数の第1引数が指す文字列は strtok 関数内部で変更されることになりますので、第1引数に “読み取り専用” の文字列を指定してはいけません。

もし指定して実行すると、strtok 関数内部での文字列変更時に例外が発生し、プログラムが異常終了するはずです。

“読み取り専用” の文字列から区切り文字で分離した文字列を取得したいような場合に関しても、一旦 “書き込み可能” なメモリ(配列や malloc で確保したメモリ)にコピーし、コピー先の文字列に対して strtok 関数を実行するようにしましょう。 

例えば下記の場合、strconst 指定されているので “読み取り専用” の配列となります。ですので、下記を実行するとプログラムは異常終了します。

読み取り専用文字列をstrtokに指定する例1
#include <stdio.h>
#include <string.h>

int main(void) {
    const char str[] = "aa,bb,cc,dd"; /* 分離する文字列 */
    char delim[] = ","; /* 区切り文字 */
    char *token; /* 分離後の文字列を指すポインタ */

    /* 文字列を分離 */
    token = strtok(str, delim);

    /* 文字列が分離できなくなるまでstrtokを実行 */
    while (token != NULL) {
        /* 分離後の文字列を表示 */
        printf("分離後の文字列:%s\n", token);

        /* 文字列を分離 */
        token = strtok(NULL, delim);
    }

    return 0;
}

下記の場合も str は “読み取り専用” のメモリを指すことになるので、実行するとプログラムは異常終了します(リテラルは変更できない)。

読み取り専用文字列をstrtokに指定する例2
#include <stdio.h>
#include <string.h>

int main(void) {
    char *str = "aa,bb,cc,dd"; /* 分離する文字列 */
    char delim[] = ","; /* 区切り文字 */
    char *token; /* 分離後の文字列を指すポインタ */

    /* 文字列を分離 */
    token = strtok(str, delim);

    /* 文字列が分離できなくなるまでstrtokを実行 */
    while (token != NULL) {
        /* 分離後の文字列を表示 */
        printf("分離後の文字列:%s\n", token);

        /* 文字列を分離 */
        token = strtok(NULL, delim);
    }

    return 0;
}

下記のように “読み取り専用” の文字列を通常の配列等の “書き込み可能” なメモリにコピーしたのちに strtok 関数を実行すれば、正常に文字列の分離を行うことができます。

コピー後の文字列をstrtokを実行する
#include <stdio.h>
#include <string.h>

int main(void) {
    const char str[] = "aa,bb,cc,dd"; /* 分離する文字列 */
    char delim[] = ","; /* 区切り文字 */
    char *token; /* 分離後の文字列を指すポインタ */

    char copy[256];

    strcpy(copy, str);
    
    /* 文字列を分離 */
    token = strtok(copy, delim);

    /* 文字列が分離できなくなるまでstrtokを実行 */
    while (token != NULL) {
        /* 分離後の文字列を表示 */
        printf("分離後の文字列:%s\n", token);

        /* 文字列を分離 */
        token = strtok(NULL, delim);
    }

    return 0;
}

スポンサーリンク

分離前の文字列と分離後の文字列の生存期間は同じ

strtok 関数の動作で確認したように、strtok 関数の返却アドレスは、分離前の文字列(初回の strtok 関数に第1引数で指定した文字列)の中の文字を指すアドレスになります。つまり、分離後の文字列は分離前の文字列の一部分であるということになります。

strtok関数の返却アドレスが分離前の文字列を指している様子

なので、分離前の文字列を格納した配列やメモリが解放されてしまったり、他のデータで上書きされてしまうと、分離後の文字列も解放・上書きされることになるので注意してください。

strcpyにより分離後の文字列まで上書きされてしまう様子

分離後の文字列を、分離前の文字列を格納した配列やメモリを解放・上書きした後にも利用したい場合は、別途他の配列やメモリに退避しておく必要があります。

例えば下記では、strtok 関数の返却アドレスを配列 tokens に保存しておき、後から tokens の各要素が指す文字列(つまり分離後文字列)を表示しようとしている例になります。

分離前の文字列を上書き
#include <stdio.h>
#include <string.h>

int main(void) {
    char str[] = "a,b,c,d,e"; /* 分離する文字列 */
    char delim[] = ","; /* 区切り文字 */
    char *token; /* 分離後の文字列を指すポインタ */
    char *tokens[5]; /* 分離後の文字列へのポインタを5個だけ保存する配列 */
    int count; /* 分離後文字列の数をカウントする変数 */
    int i;

    /* 文字列を分離 */
    token = strtok(str, delim);

    count = 0;

    /* 文字列が分離できなくなるまで or 5回分離するまでstrtokを実行 */
    while (token != NULL && count < 5) {
        /* 分離後文字列のアドレスを覚えておく */
        tokens[count] = token;
        count++;

        /* 文字列を分離 */
        token = strtok(NULL, delim);
    }

    /* 分離前の文字列を上書き */
    strcpy(str, "z,y,x,w,u");

    /* 上書き後の文字列が表示されてしまう */
    for (i = 0; i < count; i++) {
        printf("%dつ目の分離後文字列%s\n", i + 1, tokens[i]);
    }

    return 0;
}

ただし、表示する前に分離前の文字列 strを他の文字列で上書きしているため、分離後の文字列ではなく、上書き後の文字列が表示されてしまいます。実行結果は下記のようになります。

1つ目の分離後文字列z,y,x,w,u
2つ目の分離後文字列y,x,w,u
3つ目の分離後文字列x,w,u
4つ目の分離後文字列w,u
5つ目の分離後文字列u

下記のように、分離後の文字列、つまり strtok 関数が返却するアドレスの文字列を他の配列にコピーしておけば、分離前の文字列が上書きされたとしても、分離後の文字列は正常に扱うことができます。

分離後の文字列を他の配列にコピー
#include <stdio.h>
#include <string.h>

int main(void) {
    char str[] = "a,b,c,d,e"; /* 分離する文字列 */
    char delim[] = ","; /* 区切り文字 */
    char *token; /* 分離後の文字列を指すポインタ */
    char copy[5][256]; /* 分離後の文字列そのものを5個だけ保存する配列 */
    int count; /* 分離後文字列の数をカウントする変数 */
    int i;

    /* 文字列を分離 */
    token = strtok(str, delim);

    count = 0;

    /* 文字列が分離できなくなるまで or 5回分離するまでstrtokを実行 */
    while (token != NULL && count < 5) {

        /* 分離後の文字列自体を他の配列にコピー */
        strcpy(copy[count], token);
        count++;

        /* 文字列を分離 */
        token = strtok(NULL, delim);
    }

    /* 分離前の文字列を上書き */
    strcpy(str, "z,y,x,w,u");

    /* 分離後の文字列は分離前の文字列とは
       異なる配列に存在するのでうまく表示できる */
    for (i = 0; i < count; i++) {
        printf("%dつ目の分離後文字列%s\n", i + 1, copy[i]);
    }

    return 0;
}

上記は文字列を上書きしているので割と分かりやすい例なので問題に気づきやすいと思いますが、下記の場合はどうでしょう?どこに問題があるか分かるでしょうか?

分離前の文字列が解放されてしまう
#include <stdio.h>
#include <string.h>

int split(char *tokens[5], char *str, const char *delim) {
    char *token;
    int count;
    int i;
    char copy[256];

    /* 文字列を退避 */
    strcpy(copy, str);

    /* 文字列を分離 */
    token = strtok(copy, delim);

    count = 0;

    /* 文字列が分離できなくなるまで or 5回分離するまでstrtokを実行 */
    while (token != NULL && count < 5) {

        tokens[count] = token;
        count++;

        /* 文字列を分離 */
        token = strtok(NULL, delim);
    }

    for (i = 0; i < count; i++) {
        printf("%s\n", tokens[i]);
    }

    return count;
}

int main(void) {
    char before[] = "a,b,c,d,e"; /* 分離する文字列 */
    char delim[] = ","; /* 区切り文字 */
    char *tokens[5]; /* 分離後の文字列そのものを5個だけ保存する配列 */
    int count; /* 分離後文字列の数をカウントする変数 */
    int i;

    count = split(tokens, before, delim);

    for (i = 0; i < count; i++) {
        printf("%s\n", tokens[i]);
    }

    return 0;
}

私の環境で実行すると下記のように表示されました。main関数での printf の結果が全て文字化けしています。

a
b
c
d
e
????
???
?

??E ?

問題点は、tokens に格納されるアドレスが split 関数の中で変数宣言した配列 copy の中を指しているところです。split 関数の中で変数宣言されているので、split 関数終了時にこの配列は解放されてしまいます。解放された配列はその後参照してはいけません。

にもかかわらず、main 関数で split 関数終了後に copy を指すアドレスの文字列を表示しようとしているので、上記のようにうまく分離後の文字列を表示することができなくなっています。

ちなみに、split 関数における初回の strtok 関数の第1引数を str に変更すると、main 関数側でも正常に分離後の文字列を表示することができます。

これは、str の指すアドレスが main 関数で宣言された配列 before のものだからです。beforemain 関数で宣言された変数ですので、main 関数が終了するまで生存し続けます。

こんな感じで、変数の生存期間も意識しながら strtok 関数を利用する必要がある場面もあるので注意してください。

空文字列は取得できない

これも strtok 関数の動作で解説した通り、区切り文字が連続して現れる場合、その最初の区切り文字以外は無視して文字列の分離が行われます。

したがって、分離後の文字列(つまり strtok 関数の返却アドレスの文字列)は空文字列にはなり得ません。

空文字列が不要という場合は良いのですが、区切り文字と区切り文字の間の文字列が “空文字列である” ということを知りたいような場合は、strtok 関数を使うとその情報が欠落してしまう事になるので注意してください。

strtok 関数の途中で他の文字列に対する strtok 関数を実行しない

strtok 関数の動作で解説した通り、strtok 関数では、次に第1引数に NULL を指定して strtok 関数実行された時に備え、strtok 関数内部で “次に分離を開始するアドレス” を保持しています。

したがって、ある文字列に対する strtok 関数を実行している途中で、他の文字列に対する strtok 関数の実行を挟むと、その内部で保持しているアドレスが上書きされてしまいますので注意してください。

例えば下記では、main 関数の中で文字列 str を行単位に分離('\n' を区切り文字として分離)し、さらにその各行 lineparseLine の中で  ',' で分離する処理を行なっています。

途中で他の文字列に対するstrtokを挟む
#include <stdio.h>
#include <string.h>

void parseLine(char *line) {

    char *token; /* 分離後の文字列を指すポインタ */

    /* 文字列を分離 */
    token = strtok(line, ",");

    /* 文字列が分離できなくなるまでstrtokを実行 */
    while (token != NULL) {
        /* 分離後の文字列を表示 */
        printf("%s\n", token);

        /* 文字列を分離 */
        token = strtok(NULL, ",");
    }

}
int main(void) {
    char str[] = "aa,bb,cc,dd\nee,ff,gg,hh\n"; /* 分離する文字列 */
    char *line; /* 分離後の文字列を指すポインタ */
    
    /* 文字列を分離 */
    line = strtok(str, "\n");

    /* 文字列が分離できなくなるまでstrtokを実行 */
    while (line != NULL) {
        /* 分離後の文字列を表示 */
        printf("%s\n", line);

        parseLine(line);

        /* 文字列を分離 */
        line = strtok(NULL, "\n");
    }

    return 0;
}

しかし、str に対する strtok 関数実行の間に line に対する strtok 関数が実行されているので、上手く行単位に分離することができません。より具体的には、main 関数での2回目の strtok 関数が NULL を返却してしまいます。

これは、str に対する strtok 関数実行時に  strtok 関数内部で保持された “次に分離を開始するアドレス” が、line に対する strtok 関数実行時に他のアドレスに上書きされてしまうからです。

このように、ある文字列に対する strtok 関数を実行している途中で、他の文字列に対する strtok 関数の実行を挟むと文字列分離が正常に動作しません。

前の文字列に対する一連の strtok 関数の実行が完了してから、次の文字列に対して strtok 関数を実行するようにしましょう。

もしくは、strtok_r 関数を利用するという手もあります。

strtok_r関数
#include <string.h>
char* strtok_r(char* str1, const char* str2, char **restart_ptr);

strtok_r 関数は strtok 関数同様に文字列を分離する関数になります。

さらに strtok_r 関数では第3引数により、strtok 関数では関数内部で保持されていた “次に分離を開始するアドレス” を関数呼び出し側で取得する& strtok_r 関数実行時に “次に分離を開始するアドレス” 指定するようなことが可能です。

ですので、strtok_r 関数実行時に “次に分離を開始するアドレス” を取得して保持し、次に strtok_r 関数を実行するときにそのアドレスを指定してやれば、途中で他の文字列に対して strtok 関数や strtok_r 関数が実行されても正常な位置から分離を再開することが可能です。

MEMO

strtok 関数の動作の解説時に使用した “分離開始アドレス” は、この strtok_r 関数の第3引数で取得することができるアドレスのことになります

上記のソースコードを strtok_r 関数を利用するようにしたものが次のソースコードになります。この場合は意図した通りに文字列の分離を行うことができます。

strtok_rの利用例
#include <stdio.h>
#include <string.h>

void parseLine(char *line) {

    char *token; /* 分離後の文字列を指すポインタ */

    /* 文字列を分離 */
    token = strtok(line, ",");

    /* 文字列が分離できなくなるまでstrtokを実行 */
    while (token != NULL) {
        /* 分離後の文字列を表示 */
        printf("%s\n", token);

        /* 文字列を分離 */
        token = strtok(NULL, ",");
    }
}
int main(void) {
    char str[] = "aa,bb,cc,dd\nee,ff,gg,hh\n"; /* 分離する文字列 */
    char *line; /* 分離後の文字列を指すポインタ */
    char *restart = NULL; /* 分離を再開するアドレスを格納するポインタ */
    
    /* 文字列を分離 */
    line = strtok_r(str, "\n", &restart);

    /* 文字列が分離できなくなるまでstrtokを実行 */
    while (line != NULL) {
        /* 分離後の文字列を表示 */
        printf("%s\n", line);

        parseLine(line);

        /* 文字列を分離 */
        line = strtok_r(NULL, "\n", &restart);
    }

    return 0;
}

restart が “次に分離を開始するアドレス” を格納するためのポインタになります。

strtok 関数の動作で解説したように、strtok 関数の第1引数に NULL 以外を指定した場合、分離はその指定したアドレスから開始されます。strtok_r 関数でもこの点は同様です。ですので、おそらく初回の strtok_r 実行時の restart の値はなんでも良いと思います。

で、strtok_r 実行中に strtok_r の内部で restart の値が更新されます。そして、この restart の値が “次に分離を開始するアドレス” になるので、次回以降は strtok_r の第1引数に NULL を、さらに第3引数に restart のアドレスを指定することで、restart に格納されているアドレスから分離を再開するようにしています。

このように、”次に分離を開始するアドレス” さえ変数に保持しておけば、途中で他の文字列に対して strtokstrtok_r が何回実行されたとしても、適切な位置から文字列の分離を再開することができます。

スポンサーリンク

strtok 関数はスレッドセーフではない

これも先ほどと同様の話です。

strtok 関数内部で “次に分離を開始するアドレス” を保持しているので、複数のスレッドから同じタイミングで strtok 関数が実行されるとそのアドレスが意図しないアドレスに上書きされる可能性があります。

マルチスレッドを利用する場合は、strtok 関数は使用せず、strtok_r 関数を利用するようにしましょう。

ちなみにマルチスレッドについては下記ページで解説していますので、興味のある方はぜひ読んでみてください。

入門者向け!C言語でのマルチスレッドをわかりやすく解説

まとめ

このページではC言語における strtok 関数について解説しました!

1回目の実行時と2回目以降の実行時で strtok 関数への第1引数を変更する必要もありますので、複数回 strtok 関数を実行する場合は strtok 関数の基本的な使い方で紹介したように、まずループ外で1回目の strtok 関数を実行し、2回目以降の strtok 関数の実行はループ内で行うようにするのが良いと思います。

また、strtok 関数の注意点にも記載したように strtok 関数には使用時の注意点がたくさんありますので、この辺りを頭において strtok 関数を利用するとバグを防ぎやすいと思います。

結構ややこしい関数ですが、割と使いたくなる場面は多いんじゃないかなぁと思います。少なくとも私は結構使いますね…。例えば CSV ファイルなんかは各項目が , で区切られているので、項目ごとに分離するときに strtok 関数を利用します。

strtok 関数を利用する際には、このページで解説した内容を参考にしていただければと思います!また、自身で strtok 関数を作ってみても面白いと思いますので是非挑戦してみてください!

コメントを残す

メールアドレスが公開されることはありません。 * が付いている欄は必須項目です