コンピューター:C言語講座:strtok、2バイト文字について
概要
C言語講座はシステムコール関連の話題が多かったのですが、今回は突然文字列処理についてです。Cの標準ライブラリには数多くの文字列処理関数が用意されていますが、その中で知っていると大変便利なのがstrtok()でしょう。文字列中からトークンを切り出すものですが、文字列処理では非常にありがたいものです。しかしこの関数ならではの危険も知っておく必要があります。
strtok関数
strtok()はCの標準ライブラリとしては比較的特殊で、strtok()自身がstaticなデータを保持します。使い方として以下のサンプルを御覧ください。
char *ptr;
ptr=strtok("abc,def;ghi",",;");
printf("%s\n",ptr); /*
"abc"が表示される */
ptr=strtok(NULL,",;");
printf("%s\n",ptr); /*
"def"が表示される */
ptr=strtok(NULL,",;");
printf("%s\n",ptr); /*
"ghi"が表示される */
このように初めに第1引数に操作したい文字列を指定し、第2引数に分離文字列を指定すると分離文字列中のどれかと一致したところまでの文字列が切り出されます。2回目以降は第1引数にNULLを与えれば前回の続きとなります。ここがポイントで、NULLを与えて前回の続きが出来るということはstrtok()内で前回の内容(アドレス)を保持しているということです。
これは非常に重要なポイントで、一連の処理が終るまで、他の文字列を対称にした処理が出来ない、ということになります。こんなことは当り前だ、と思っていても関数分割されているとついつい気がつかないうちに失敗することも多いです。
char *ptr;
ptr=strtok("abc,def;ghi",",;");
printf("%s\n",ptr); /*
"abc"が表示される */
SubFunc();
ptr=strtok(NULL,",;");
printf("%s\n",ptr); /*
"def"が表示される */
SubFunc();
ptr=strtok(NULL,",;");
printf("%s\n",ptr); /*
"ghi"が表示される */
SubFunc();
こんな感じにstrtok()の合間で関数を呼び出していて、そこでもついついstrtok()を使っている場合に大変なことになります。
しかしプログラム中にある処理をしながら別のI/Oを行なっていてそこで文字列処理をする、ということは以外と多く、その場合はstrtok()には十分気をつけるか、あるいは自分でstrtok()のマルチ動作可能版を作らないといけません。
さらに、strtok()の第1引数で渡した文字列はstrtok()に変更されてしまう点も注意が必要です。strtok()はトークンを切り出す際に文字列にどんどん'\0'を入れていきます。したがって、strtok()の第1引数に渡した文字列は再利用が出来ません。
2バイト文字
Cの標準ライブラリではその他にも2バイト文字、つまり漢字を扱う場合には注意が必要です。例えば上記のstrtok()で漢字を扱ってみましょう。漢字コードはEUCとします。□は全角スペースとします。
char *ptr;
ptr=strtok("あいう?□えお","□"); /*
□は全角スペース */
printf("%s\n",ptr); /* "あいう?"が表示される
*/
ptr=strtok(NULL,"□");
printf("%s\n",ptr); /* "えお"が表示される
*/
はたしてこのサンプルはこのように動くでしょうか?まず、"あいう?□えお"のコードを見てみましょう。
あ
い う ? □ え お
a4a2 a4a4 a4a6 a1a9 a1a1 a4a8 a4aa (EUC)
strtok()の動作は第2引数中の文字と同じ物があったらそこで切る、というものです。そうすると、?の1バイト目のa1と分離文字列□の1バイト目のa1が一致した時点で切られてしまいます。実際は以下のように切断されてしまいます。
あ
い う ??? え お
a4a2 a4a4 a4a6 , a9 , a4a8 a4aa (EUC)
a9というコードはASCIIコードでもありませんので、実際は画面には空白が表示されるかも知れません。いずれにせよ、目的通りには動きません。同様の現象はstrpbrk()でも起きます。
これに対応するには2バイトコードかどうかを判定しながら1バイトで比較するのか、2バイトで比較するのかを考えるようなstrtok(),strpbrk()を自分で作らないといけません。
これ以外にも漢字コードがSJISですと、更に問題は増えます。EUCは1バイト目も2バイト目もASCIIコードとは重ならないことになっていますが、SJISは1バイト目は重なりませんが、2バイト目は重なるものもあるのです。例えば、
表 示
955c 8ea6 (SJIS)
"表"の2バイト目は5cで、これは"\"(バックスラッシュ)と同じコードです。これを知らずにstrtok()を使って、
char *ptr;
ptr=strtok("表示\\abc","\\");
このようにしてしまうと、結果は、
??? 示
a b c
95 , 8ea6 , 61 62 63
となってしまいます。この問題はstrtok(),strpbrk()だけでなく、strchr(),strrchr()などでも起こります。これに対応するにもやはり2バイトコードかどうかを判定しながら動作する関数を自作しないといけません。
サンプル
ここでは上記のstrtok()の重複呼び出しの問題、第1引数書き換えの問題、2バイトコードの問題の対応例として、strtok()同様の動作をするものをサンプルとして示します。strpbrk()も対応版が含まれています。漢字コードがSJISの場合はSJISをデファインしてコンパイルします。
重複呼び出しの解決の為にstrtok()がstaticに持つものを引数で呼び出し側が与えるようにしました。ST_STRという構造体がそうです。StrTok()呼び出し前にInitStrTok()で初期化、使用後はFreeStrTok()で開放します。
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define issjiskanji(c) ((0x81 <= (unsigned char)(c&0xff) && (unsigned char)(c&0xff) <= 0x9f) \
|| (0xe0 <= (unsigned char)(c&0xff) && (unsigned char)(c&0xff) <= 0xfc))
#define issjiskana(c) ((0xa1 <= (unsigned char)(c&0xff) && (unsigned char)(c&0xff) <= 0xdf))
#define iseuckanji(c) (0xa1 <= (unsigned char)(c&0xff) && (unsigned char)(c&0xff) <= 0xfe)
#define iseuckana(c) (0x8e == (unsigned char)(c&0xff))
#ifndef SJIS
#define iskanji(c) (iseuckanji(c)||iseuckana(c))
#else
#define iskanji(c) issjiskanji(c)
#endif
typedef struct {
char *buf;
char *ptr;
}ST_STR;
char *StrPBrk(str, sep)
char *str;
char *sep;
{
register char *ptr, *p;
register int k_flag;
for (ptr = str; *ptr != '\0'; ptr++) {
if (iskanji(*ptr)) {
k_flag = 1;
}
else {
k_flag = 0;
}
for (p = sep; *p != '\0'; p++) {
if (iskanji(*p)) {
if (k_flag) {
if ((*ptr == *p) && (*(ptr + 1) == *(p + 1))) {
return (ptr);
}
}
p++;
}
else {
if (!k_flag) {
if (*ptr == *p) {
return (ptr);
}
}
}
}
if (k_flag) {
ptr++;
}
}
return (NULL);
}
int InitStrTok(st_str)
ST_STR *st_str;
{
st_str->buf = NULL;
st_str->ptr = NULL;
return (0);
}
int FreeStrTok(st_str)
ST_STR *st_str;
{
free(st_str->buf);
return (0);
}
char *StrTok(str, sep, st_str)
char *str;
char *sep;
ST_STR *st_str;
{
char *p, *p2;
if (str != NULL) {
if (st_str->buf != NULL) {
free(st_str->buf);
}
st_str->buf = strdup(str);
st_str->ptr = st_str->buf;
}
if (st_str->buf == NULL || st_str->ptr == NULL) {
return (NULL);
}
while (1) {
if (*(st_str->ptr) == '\0') {
st_str->ptr = NULL;
return (NULL);
}
p = StrPBrk(st_str->ptr, sep);
if (p == NULL) {
p = st_str->ptr;
st_str->ptr = NULL;
return (p);
}
else if (p == st_str->ptr) {
if(iskanji(*p)){
st_str->ptr+=2;
}
else{
st_str->ptr++;
}
}
else {
if (iskanji(*p)) {
*p = '\0';
p++;
*p = '\0';
}
else {
*p = '\0';
}
p2 = p + 1;
p = st_str->ptr;
st_str->ptr = p2;
return (p);
}
}
}
void main()
{
char str[512];
char sep[512];
char *ptr;
ST_STR st;
setbuf(stdout, NULL);
printf("sep=");
gets(sep);
printf("str=");
gets(str);
InitStrTok(&st);
for (ptr = StrTok(str, sep, &st); ptr != NULL; ptr = StrTok(NULL, sep, &st)) {
printf("%s\n", ptr);
}
FreeStrTok(&st);
}
細かく解説はしませんが、自分で考えてみてください。なお、このコードは他を参考にして作ったわけではなく、自分で考えたもので、他の人のアドバイスが入っていませんので、効率などの面で改良の余地はたくさんあると思いますが、そのへんも自分で検討してみてください。FreeBSDなどのライブラリのソースも参考にしてみると良いでしょう。
また、このように自作しておけば、strtok()と微妙に違う機能がほしい場合、例えば'\'で分離文字をエスケープできる、とか、""で囲ってある場合は分離しない、分離文字が連続している場合に空の文字列を返す、などの別バージョンへ発展することも可能です。