これまでにも利用してきた文字列処理のための 標準ライブラリ関数の仕組を理解しよう. そして,プログラミング言語処理系などで必要となるテキスト処理 (テキストからトークンへの分解,トークンの解釈・変換,等) の基礎部分を作成してみよう.
文字入出力の標準ライブラリ関数:(stdio.h)
どちらの関数でも戻り値は, 入出力が成功の場合には入出力した文字, 失敗の場合には EOF となる. なお,文字は char 型ではなく, int 型としていることに注意しよう. (EOF が int 型の -1 であるため.)
文字列入出力の標準ライブラリ関数:(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; // 文字が入力されるよ 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); }
$ ./fio < fio.c | less
ライブラリ関数とクローン関数とが同じ結果となるか確認しよう.
数字列(数字の文字列)を数値化したり, その逆も必要な場合がある.
文字種を検査する標準ライブラリ関数:(ctype.h)
これらの関数の戻り値は,条件式と同様な真偽値であり, 正解の場合には 0 以外,不正解の場合には 0 となる.
数字列(数字の文字列)を数値化する標準ライブラリ関数:(stdlib.h)
これらの関数は手軽に使えるものではあるが, エラー処理が不完全である. 完全なエラー処理が必要な場合には, strtol() や strtod() を使うとよい.
標準ライブラリ関数 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/-1 char *s; // 数字列内の数字へのポインタ // 先頭の空白を除去 s = str; while (isspace(*s)) s++; // 符号を取得 sgn = +1; if (*s == '-') { sgn = -1; s++; } else if (*s == '+') { s++; } // 絶対値を算出 val = 0; while (isdigit(*s)) { val = val*10 + (int)(*s - '0'); 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 ...
行文字列からトークン(単語,部分文字列)を取り出してみよう. この処理は,sscanf(..., "%s", ...) や argv などで利用される.
例:"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; // or ... s = &str[0]; t = tok; // or ... t = &tok[0]; or ... なし 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] ...
このようにテキストをトークンに分解できれば, 数式計算やスクリプトの処理に応用できそうだ. しかし,空白を省略できないのは不便だ...
区切文字だけに頼らずにトークンを抽出する方法として, 文字種の変化部分を区切とみなしてみよう. (もちろん空白区切もそのまま使えるように.)
例:"var=var+123;" → "var","=","var","+","123",";"
tok-2.c:(tok-1.c を元に改造)
... /* 文字列から数字トークンまたは数字以外トークンを1個だけ抽出する関数 数字トークン:数字だけを含む文字列 数字以外トークン:数字以外・空白以外だけを含む文字列 (sgetword() の改良版) str : 処理対象の行文字列 tok : トークンの文字列バッファ return : 次に処理すべき文字のアドレス */ 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] ...
これで数式計算にも便利に使えそうだ.
問題 A 〜 C のいずれか1問に取り組め:
(もちろん,テスト用メイン関数なども必要. ソースファイル名を atof.c などとせよ.)
(もちろん,テスト用メイン関数なども必要. ソースファイル名を printd.c などとせよ.)
$ ./calc 数式 > 123+45 168 数式 > 1*2-3 -1 数式 > 1+2*3 9 # 本来なら 1+(2*3)=7 のハズだが... # 簡単のため演算子の優先順位を考慮せず... # (1+2)*3=9でよい 数式 > -1 式が変 # 最初のトークンは数字のハズ 数式 > 1++2 式が変 # 演算子は1文字のハズ ...
簡単のための仮定・制限:
なお,数字列のバッファ配列が必要となる場合, int型の整数の表現可能範囲(±20億程度) が最大 10桁であることを利用して, バッファサイズを決め打ちしてよい.
提出: