このページでは、C言語の標準関数である strtok
関数について解説していきます!
strtok
関数は文字列を区切り文字で分離する関数です。strtok
関数を利用することで、例えば下記のような文字列をカンマ(','
)で区切った文字列に分離するようなことができます。
April,May,June,July,Octobor
分離後の文字列は下記のようになります。
April
May
June
July
Octobor
一見便利そうな関数ですが、この関数、正直かなりクセが強いです…。他の関数に比べると使い方が難しいですし、注意すべきことも多いです。
このページでは strtok
関数と strtok
関数の動作、さらにその動作から考察できる strtok
関数の注意点について解説していきます。
Contents
strtok
関数
strtok
関数は、指定した区切り文字で文字列を分離する関数です。
ただし、1回の strtok
関数で分離できるのは、”文字列の先頭から最初の区切り文字の直前の文字まで” のみです。
さらに文字列を分離したい場合は、複数回 strtok
関数を実行する必要があります。
例えば "abc+def+ghi"
を区切り文字 '+'
で分離する場合、最初の strok
関数実行時に取得できるのは "abc"
のみになります。さらに、2回目に strtok
関数を実行すると "def"
が、3回目に strtok
関数を実行すると "ghi"
が取得できます。
また、初回の strtok
関数実行と2回目以降の strtok
関数実行時で引数を変更したりする必要があって結構ややこしいです。
この辺りも踏まえて、strtok
関数の解説をしていきたいと思います。
strtok
関数の定義
strtok
関数の定義ファイル、関数定義は下記の通りです。
#include <string.h>
char* strtok(char* str1, const char* str2);
スポンサーリンク
strtok
関数の引数
strtok
関数の第1引数 str1
には、”分離を行いたい文字列” が格納された配列やメモリのアドレスを指定します。
複数回同じ文字列に対して strtok
関数を実行する場合は、2回目以降は str1
に NULL
を指定します。
第2引数 str2
には、第1引数 str1
を分離する際の “区切り文字” の文字列が格納された配列やメモリのアドレスを指定します。
例えば a
を区切りにして文字列を分離したいのであれば、第2引数には "a"
を指定します('a'
ではないことに注意)。また a
と b
と c
を区切りにして文字列を分離したいのであれば、"abc"
を指定します。
strtok
関数の返却値
文字列が分離できた場合、分離後の文字列の先頭アドレスを返却します。
文字列が分離できない場合、NULL
を返却します。
NULL
が返却される具体的なケースについては、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
関数の内部で使用する分離処理を開始するアドレスです
おそらくこのアドレスを考慮した方が strtok
関数の動作が分かりやすいと思います
また “分離開始アドレス” という名前は、説明しやすくするために私が便宜上名付けたものであり、一般的な用語ではないので注意してください
strtok
関数の第1引数 str1
に NULL
以外を指定した場合、”分離開始アドレス” は、str1
になります。
そして、この場合は str1
から順に後ろ側に向かって第2引数 str2
で指定された区切り文字(のいずれか)を探索することになります。
もし区切り文字が見つかった場合、strtok
関数は最初に見つけた区切り文字をヌル文字 '\0'
に置き換え、”分離開始アドレス” を返却して終了します。今後、この strtok
関数の返却アドレスを token
としたいと思います。
ここで token
の指す文字列に注目すると、ヌル文字 '\0'
は文字列の終端を表すわけですから、token
が指す文字列は “分離開始アドレスの位置の文字” 〜 “元々区切り文字であった文字の直前” となります。したがって、token
は区切り文字で分離された文字列として扱うことができ、これにより区切り文字で分離した文字列が1つ得られたことになります。
ただし、1回の strtok
関数の実行で得られる分離後の文字列は1つのみです。さらに分離後の文字列を取得したい場合、再度 strtok
関数を実行する必要があります。
次の分離後の文字列を得るためには、前述の通り、strtok
関数の第1引数には NULL
を指定する必要があります。
NULL
を指定する理由としてポイントになるのが、前回の strtok
関数の実行により str1
が指す文字列の最初の区切り文字がヌル文字に置き換えられている点です。最初の区切り文字がヌル文字に置き換えられているわけですから、str1
が指す文字列に区切り文字は存在しないことになります。
したがって、もし次の分離文字列を取得するために strtok
関数の第1引数に str1
を再び指定してしまうと、区切り文字が見つかる前に文字列の終端まで探索が行われることになります。
区切り文字が見つかる前に文字列の終端まで探索された場合、strtok
関数は何もせずに分離開始アドレス(この場合は str1
)を返却して終了します。
すなわち、strtok
関数の返却アドレスである token
が指す文字列は1回目に実行した時と同じものになることになり、次の分離後の文字列を取得することができません。
なので、本当は strtok
関数の第1引数としては、先ほど strtok
関数でヌル文字に置き換えられた文字の1文字後ろ位置のアドレスを指定する必要があります。
でも、わざわざこのアドレスを指定するのは面倒ですよね?
なので strtok
関数では、第1引数で NULL
を指定すれば、直前にヌル文字に置き換えた位置の1文字後ろの位置のアドレスを “分離開始アドレス” に自動的に設定するようになっています。このようなことが可能なのは、strtok
関数の内部で、次に実行された時の “分離開始アドレス” を保持してくれているからです。
そして、初回の strtok
関数実行時のように、”分離開始アドレス”(つまり、前回ヌル文字に置き換えた位置の1文字後ろの位置のアドレス)から最初に見つけた区切り文字をヌル文字に置き換え、”分離開始アドレス” を返却します。
なので、返却されたアドレス token
が指す文字列は、区切り文字で分離した2つ目の文字列として扱うことができます。
これ以降は、第1引数に NULL
を指定して strtok
関数を実行すると同様の動作をしてくれます。ですので、strtok
関数が NULL
を返却するまでループしてやれば、区切り文字で分離した文字列全てを取得することができることになります(どんな時に NULL
が返却されるかは、次の strtok 関数の例外的な動作で説明します)。
基本的な strtok
関数の動作はこんな感じだと思います。
strtok
関数の例外的な動作
ただ、例外的に上記とは異なる動作をするケースもあります。
分離開始アドレスの文字が区切り文字である場合
1つ目のケースは、”分離開始アドレス” の文字が区切り文字である場合です。この場合、”分離開始アドレス” を、次に最初に現れる “区切り文字以外の文字” の位置のアドレスに更新してから探索が行われます。
ですので、例えば区切り文字が連続して現れるような文字列の場合も、分離後の文字列が空文字列となるようなケースは存在しません。
区切り文字が見つからなかった場合
2つ目のケースは、区切り文字が見つからなかった場合です(区切り文字より前にヌル文字が見つかった場合)。この場合、strtok
関数は単純に “分離開始アドレス” の返却のみを行います。ヌル文字への置き換えは行いません。
そしてこの場合、次に第1引数に NULL
を指定して strtok
関数を実行した場合の “分離開始アドレス” は NULL
になるようです。
分離開始アドレスの指す文字がヌル文字である場合
3つ目のケースは “分離開始アドレス” がヌル文字を指しているケースです。この場合、strtok
関数は NULL
を返却します。
ですので、”分離開始アドレス” から始まる文字列が空文字列である場合は strtok
関数は NULL
を返却します。
また、”分離開始アドレス” から始まる文字列に “区切り文字しか存在しない” 場合も strtok
関数は NULL
を返却します。
これは、分離開始アドレスの指す文字が区切り文字である場合で説明したように、”分離開始アドレス” の指す文字が “区切り文字” である場合、次に最初に現れる “区切り文字以外の文字” の位置のアドレスに “探索位置アドレス” が更新されるからです。
分離開始アドレスが NULL
である場合
4つ目のケースは “分離開始アドレス” が NULL
であるケースです(ヌル文字じゃないよ)。この場合も、strtok
関数は NULL
を返却します。
区切り文字が見つからなかった場合で説明したように、前回の strtok
関数実行時に区切り文字が見つからなかった場合、次回の strtok
関数実行時の “分離開始アドレス” は NULL
となります。
したがって、前回の strtok
関数実行時に区切り文字が見つからなかった場合、次回の strtok
関数実行時の返却アドレスは必ず NULL
となります。
スポンサーリンク
strtok
関数の注意点
strtok
関数の動作はなんとなく理解できたでしょうか?次は、上記の動作から考察される strtok
関数の注意点について解説していきたいと思います。
第1引数に指定した文字列は strtok
関数内部で変更される
strtok 関数の動作で解説した通り、第1引数が指す文字列に存在する区切り文字は strtok
関数内部でヌル文字に置き換えられます。
したがって、変更されたくない文字列に対して strtok
関数は実行しないように注意してください。
変更されたくない文字列に対して strtok
を実行しなければならない場合は、一旦他のメモリや配列にその文字列をコピーし、コピーした文字列に対して strtok
関数を実行するようにしましょう。
例えば、下記のように strtok
関数実行後に str
を printf
で表示したとしても、元々 str
が指していた文字列を表示することはできません(下記の場合は "aa,bb,cc,dd"
ではなく "aa"
が表示されます)。
#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
関数を実行すれば、元々の文字列をそのまま表示することができます。
#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
関数を実行するようにしましょう。
例えば下記の場合、str
は const
指定されているので “読み取り専用” の配列となります。ですので、下記を実行するとプログラムは異常終了します。
#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
は “読み取り専用” のメモリを指すことになるので、実行するとプログラムは異常終了します(リテラルは変更できない)。
#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
関数を実行すれば、正常に文字列の分離を行うことができます。
#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
関数の返却アドレスを配列 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
のものだからです。before
は main
関数で宣言された変数ですので、main
関数が終了するまで生存し続けます。
こんな感じで、変数の生存期間も意識しながら strtok
関数を利用する必要がある場面もあるので注意してください。
空文字列は取得できない
これも strtok 関数の動作で解説した通り、区切り文字が連続して現れる場合、その最初の区切り文字以外は無視して文字列の分離が行われます。
したがって、分離後の文字列(つまり strtok
関数の返却アドレスの文字列)は空文字列にはなり得ません。
空文字列が不要という場合は良いのですが、区切り文字と区切り文字の間の文字列が “空文字列である” ということを知りたいような場合は、strtok
関数を使うとその情報が欠落してしまう事になるので注意してください。
strtok
関数の途中で他の文字列に対する strtok
関数を実行しない
strtok 関数の動作で解説した通り、strtok
関数では、次に第1引数に NULL
を指定して strtok
関数実行された時に備え、strtok
関数内部で “次に分離を開始するアドレス” を保持しています。
したがって、ある文字列に対する strtok
関数を実行している途中で、他の文字列に対する strtok
関数の実行を挟むと、その内部で保持しているアドレスが上書きされてしまいますので注意してください。
例えば下記では、main
関数の中で文字列 str
を行単位に分離('\n'
を区切り文字として分離)し、さらにその各行 line
を parseLine
の中で ','
で分離する処理を行なっています。
#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
関数を利用するという手もあります。
#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
関数が実行されても正常な位置から分離を再開することが可能です。
strtok 関数の動作の解説時に使用した “分離開始アドレス” は、この strtok_r
関数の第3引数で取得することができるアドレスのことになります
上記のソースコードを 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
に格納されているアドレスから分離を再開するようにしています。
このように、”次に分離を開始するアドレス” さえ変数に保持しておけば、途中で他の文字列に対して strtok
や strtok_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
関数を作ってみても面白いと思いますので是非挑戦してみてください!