文字列処理のための標準ライブラリ関数の仕組を理解しよう. そして,プログラミング言語処理系などで必要となるテキスト処理 (テキストからトークンへの分解,トークンの解釈・変換,等) の基礎部分を作成してみよう.
ライブラリ関数の仕様については, オンラインマニュアル(man コマンド)も活用しよう.
文字入出力の標準ライブラリ関数:(stdio.h)
どちらの関数でも戻り値は, 入出力が成功の場合には入出力した文字, 失敗の場合には EOF となる. なお,戻り値の文字は char 型ではなく, int 型となっていることに注意しよう.
文字列入出力の標準ライブラリ関数:(stdio.h)
では,fgets() と fputs() のクローンを作成し, それらの仕組みを理解しよう. (最も基本的な入出力関数 fgetc() と fputc() については, そのまま利用する.)
ソース lio.c:
#include <stdio.h> #include <stdlib.h> /* 行文字列を入力する標準ライブラリ関数 fgets() のクローン buf:行文字列のバッファ配列 n:バッファサイズ fp:入力ファイルのポインタ */ char *myfgets(char *buf, int n, FILE *fp) { int i; // 文字数のカウンタ int c; // 文字が入力されるよ(EOF対応のためintね) char *s; // buf 内の文字へのポインタ s = buf; n--; // 終端記号の余地を空けておく for (i = 0; i < n; i++) { // 最大 n 文字まで... c = fgetc(fp); // 文字を入力 if (c == EOF) break; *s = (char)c; // 文字を buf に書き込む s++; if (c == '\n') break; // 改行文字(書込済)で入力を終了 } if (s == buf) return (NULL); // 何も入力していない場合,関数を終了 *s = '\0'; // 終端記号を書き込む return (buf); } /* 行文字列を出力する標準ライブラリ関数 fputs() のクローン s:行文字列のポインタ fp:出力ファイルのポインタ return:正常終了では 0,異常終了では EOF */ int myfputs(char *s, FILE *fp) { while (*s != '\0') { if (fputc((int)*s, fp) == EOF) return (EOF); s++; } return (0); } #define BUFLEN 256 int main(void) { char buf[BUFLEN]; while (1) { // if (fgets(buf, BUFLEN, stdin) == NULL) break; // fputs(buf, stdout); // ↑ ライブラリ関数を利用する場合 // ↓ クローン関数を利用する場合 if (myfgets(buf, BUFLEN, stdin) == NULL) break; myfputs(buf, stdout); } return (EXIT_SUCCESS); }
$ ./lio < lio.c | less
ライブラリ関数とクローン関数とが同じ結果となるか確認しよう.
数字列(数字の文字列)を数値化したり, その逆も必要な場合がある.
文字種を検査する標準ライブラリ関数:(ctype.h)
数字列(数字の文字列)を数値化する標準ライブラリ関数:(stdlib.h)
標準ライブラリ関数 atoi() のクローンを作成し, その仕組みを理解しよう.
ソース atoi.c:
#include <stdio.h> #include <stdlib.h> // atoi(), etc. #include <ctype.h> // isspace(), isdigit(), etc. /* 数字列を整数値へ変換する関数 atoi() のクローン str:数字列 return:整数値 */ int myatoi(char *str) { int val; // 数値の絶対値 int sgn; // 数値の正負符号 +1 or -1 char *s; // 数字列内の数字へのポインタ // 先頭の空白を除去 s = str; while (isspace(*s)) s++; // 反復対象の命令が1個だけなのでブロック化{ }は省略可 // 正負符号を取得...符号なしの場合もあるよ sgn = +1; // デフォルトで正としておく if (*s == '-') { sgn = -1; s++; } else if (*s == '+') { // このelseの必要性...わかるかな? s++; } // 二択('-'/'+')と見せかけて実は三択ですよ('-'/'+'/なし)わかるかな? // 見かけも三択として if...else if...else と書いてもOKだけど長たらしいよね? // 絶対値を算出 val = 0; while (isdigit(*s)) { // 先頭(上位)から1文字(1桁)ずつ... val = val*10 + (int)(*s - '0'); // 直前までの数値化結果を10倍(1桁上げ) // 下位の桁の数字(ASCIIコード)を数値化し加算 s++; } // 変換結果の整数値を返す return (val*sgn); } #define BUFLEN 256 int main(void) { char buf[BUFLEN]; while (1) { printf("行文字列 > "); if (fgets(buf, BUFLEN, stdin) == NULL) break; printf("atoi() : %d\n", atoi(buf)); // ライブラリ関数 printf("myatoi() : %d\n", myatoi(buf)); // クローン関数 printf("\n"); } printf("終了\n"); return (EXIT_SUCCESS); }
$ ./atoi
行文字列 > +123
atoi() : 123
myatoi() : 123
行文字列 > -45
atoi() : -45
myatoi() : -45
行文字列 > 67ab8
atoi() : 67
myatoi() : 67
...
行文字列 > [Ctrl]+[D]
終了
行文字列からトークン (token;字句単位...要するに単語のような部分文字列)を取り出してみよう.
トークン間の区切としては,空白などの区切文字 (delimiter)を使う場合と,文字種の変化部分を使う場合とがある. まずは簡単に,空白区切によるトークン抽出関数を作成してみよう.
例:"var = var + 123 ;" → "var"/"="/"var"/"+"/"123"/";"
ソース tok-1.c:
#include <stdio.h> #include <stdlib.h> #include <ctype.h> #include <string.h> // strcmp() /* 文字列から単語トークンを1個だけ抽出する関数 (要するに sscanf(str, "%s", tok) の拡張版) str : 処理対象の行文字列 tok : トークンの文字列バッファ return : 次に処理すべき文字のアドレス */ char *sgetword(char *str, char *tok, int n) { char *s; // 行文字列内の文字へのポインタ char *t; // トークン内の文字へのポインタ int i; // 文字番号のカウンタ s = str; // s = &str[0]; と同意 t = tok; // t = &tok[0]; と同意 while (isspace(*s)) s++; // 先頭の空白文字を無視 n--; // 終端記号の分だけコピー可能な文字数を減 for (i = 0; i < n; i++) { if (*s == '\0') break; if (isspace(*s)) break; *t = *s; t++; // or ... tok[i] = *s; s++; } if (t == tok) return (NULL); // トークンなしの場合,エラー *t = '\0'; // or ... tok[i] = '\0'; // トークンを終端 return (s); } #define BUFLEN 256 int main(void) { char line[BUFLEN]; // 行文字列のバッファ配列 char tok[BUFLEN]; // トークン文字列のバッファ配列 char *expr; // line 内の次のトークンへのポインタ while (1) { printf("数式 > "); if (fgets(line, BUFLEN, stdin) == NULL) break; expr = line; while (1) { expr = sgetword(expr, tok, BUFLEN); // tok を抽出,expr を更新 if (expr == NULL) break; if (strcmp(tok, "quit") == 0) goto END; printf("[%s]\n", tok); } } END: printf("終了\n"); return (EXIT_SUCCESS); }
$ ./tok-1 数式 > 123 + 45 # 空白区切の文字列なら... [123] # ...分解される [+] [45] 数式 > 123+45 # 空白区切なしだと... [123+45] # ...分解されない ... 数式 > quit
このようにテキストをトークンに分解できれば, 数式計算やスクリプトの処理に応用できそうだ. しかし,ユーザ目線では,空白を省略できないのは不便だ...
区切文字だけに頼らずにトークンを抽出する方法として, 文字種の変化部分を区切とみなしてみよう. (もちろん空白区切も併用できるように.)
例:"var=var+123;" → "var"/"="/"var"/"+"/"123"/";"
とりあえず簡単に, 数字と数字以外とだけを区別してみると...
ソース tok-2.c:(tok-1.c を元に改造)
... /* 文字列から数字トークンまたは数字以外トークンを1個だけ抽出する関数 数字トークン:数字だけを含む文字列 数字以外トークン:数字以外・空白以外だけを含む文字列 (sgetword() の改良版) str : 処理対象の行文字列 tok : トークンの文字列バッファ return : 次に処理すべき文字のアドレス */// char * sgetword(...)char *sgettok(char *str, char *tok, int n) { char *s; char *t; int i; int d0, d1; // 直前と現在の数字フラグ(数字ならゼロ以外) s = str; t = tok; while (isspace(*s)) s++; d0 = isdigit(*s); // 先頭の文字の数字フラグ n--; for (i = 0; i < n; i++) { if (*s == '\0') break; if (isspace(*s)) break; d1 = isdigit(*s); // 現在の文字の数字フラグを設定 if (d1 != d0) break; // 文字種が変わった場合,トークンを終端 *t = *s; t++; s++; d0 = d1; // 直前の文字の数字フラグを更新 // 次の文字へ進むと,現在の文字が直前の文字となるので } if (t == tok) return (NULL); *t = '\0'; return (s); } int main(void) { ... while (1) { ... while (1) {// expr = sgetword(expr, tok, BUFLEN);expr = sgettok(expr, tok, BUFLEN); ... } } ... }
$ ./tok-2 数式 > 123 + 45 [123] # 空白区切は tok-1 と同様 [+] [45] 数式 > 123+45 [123] # 空白なしでも分解できた [+] [45] ...
これで数式計算にも便利に使えそうだ.
今回作成した関数 sgettok() と標準ライブラリ関数 atoi() 等を利用し, 整数の四則演算だけからなる数式(文字列)を標準入力すると, その式の値を算出するプログラム calc.c を作成せよ.
$ ./calc 数式 > 123 123 数式 > 123+45 168 数式 > 1*2-3 -1 数式 > 1+2*3 9 # これは,本来なら 1+(2*3)=7 のハズだが... # 簡単のため演算子の優先順位を考慮せず... # (1+2)*3=9 として良い. 数式 > -1 式が変 # 最初のトークンは数字のハズなのでエラー. # ...でもこれはOKとして結果を -1 にできる方が良い. 数式 > 1++2 式が変 # 演算子は1文字のハズなのでエラー. # ...これも結果を 3 にできる方が良い. ...
簡単のための仮定・制限:
なお,数字列のバッファ配列が必要となる場合, int型の整数の表現可能範囲(±20億程度) が最大 10桁であることを利用して, バッファサイズを決め打ちしてよい.