これまでにも利用してきた文字列処理のための 標準ライブラリ関数の仕組を理解しよう. そして,プログラミング言語処理系などで必要となるテキスト処理 (テキストからトークンへの分解,トークンの解釈・変換,等) の基礎部分を作成してみよう.
文字入出力の標準ライブラリ関数:(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桁であることを利用して, バッファサイズを決め打ちしてよい.
提出: