文字と文字列の処理を理解しよう. さらに,標準ライブラリ関数を利用して, プログラミング言語っぽいものを作ってみよう.
文字データ処理の例として,まずは,アルファベットを表示してみよう: ascii.c
$ cp ~/tmpl.c ascii.c $ vim ascii.c
#include <stdio.h>
int main(void)
{
char c;
for (c = 'a'; c <= 'z'; c++) { // 'a', 'b', 'c', ..., 'z'
printf("'%c' : %d\n", c, c); // '文字' : 番号
}
return (0);
}
実は,コンピュータ内部では, すべての文字は整数(ASCII コード番号)として取り扱われている.
つまり,int型と同様に, char型データも計算とか比較とか可能. 例:'a'+1 → 'b'
char型データを文字(画像)として表示する.
char型データのASCIIコード番号を表示する.
$ cc ascii.c -o ascii $ ./ascii 'a' : 97 'b' : 98 ...
まずは,慣れ親しんだ方法として, 書式付き入出力 scanf() とprintf() を 文字データの入出力に使ってみよう: cio1.c
$ cp ~/tmpl.c cio1.c $ vim cio1.c
#include <stdio.h>
int main(void)
{
char c;
printf("半角英数字を連続入力 > ");
while (1) {
if (scanf("%c", &c) == EOF) break; // 1文字ずつの入力と入力終了の判断
printf("[%c]", c); // 1文字ずつの出力
}
return (0);
}
文字入力の書式は,scanf() でも "%c". (printf() の文字出力と同様.)
$ cc cio1.c -o cio1
$ ./cio1
半角英数字を連続入力 > abc de f
[a][b][c][ ][d][e][ ][
][Ctrl]+[D]
$
実行結果は,少々,わかりづらいですが... 文字列(復数の文字の連続)を入出力しているようにも見えるが... あくまでも1文字ずつの入出力の反復を実行している. scanf() のキーボード入力はバッファリング (メモリ内に一時的に蓄積)されるので, 1文字タイプ毎の即座に1文字表示される訳ではない. [Enter]キーのタイプでバッファからの読み取り処理が開始される. また,[Enter]キーで改行文字も入力されるし, [Space]キーで空白文字も入力される.
次に,新たな方法として, 書式なし入出力 getchar() と putchar() も 使ってみよう:
$ cp cio1.c cio2.c $ vim cio2.c
...
while (1) {
c = getchar(); // 1文字ずつの入力
if (c == EOF) break; // 入力終了の判断
putchar(c); // 1文字ずつの出力
}
...
$ cc cio2.c -o cio2 $ ./cio2 ...
実行結果は,書式付き版とほぼ同じ. 表示的には "[" と "]" を省略したが, それ以外での主な違いは,入力の EOF の取り扱い方法だけ.
文字の入出力を文字列版へ改造しよう:
$ cp cio1.c sio.c $ vim sio.c
#include <stdio.h>
#define BUFLEN 10
int main(void)
{
char buf[BUFLEN];
printf("半角英数字を連続入力 > ");
while (1) {
if (scanf("%s", buf) == EOF) break;
printf("[%s]", buf);
}
return (0);
}
文字列の入出力の書式は "%s".
$ cc sio.c -o sio
$ ./sio
半角英数字を連続入力 > abc de f
[abc][de][f][Ctrl]+[D]
入力された文字列は空白で区切られて 文字配列 buf[] に代入される. なお,空白文字は代入されない.
C言語の文字列(string)は,文字データの配列によって構成される.
かなり手抜きな合理的な言語仕様となっている.
文字配列の初期化・代入・比較の方法を理解しよう: str.c
$ cp ~/tmpl.c str.c $ vim str.c
#include <stdio.h>
int main(void)
{
// 文字配列の初期化
char s[10] = "abc"; // これは特例としてOK.便利
// char s[10] = {'a', 'b', 'c', '\0'}; // これでも同じだが面倒
printf("初期状態 [%s]\n", s);
// 文字配列への代入(文字配列の書き換え)
// s = "de"; // これは無理.アドレス同士の代入になっている.
// 左辺 s もアドレス(配列の先頭アドレス),右辺 "de" もアドレス.
// 左辺はポインタ変数ではないので代入できない.
s[0] = 'd'; // これはOKだが面倒
s[1] = 'e';
s[2] = '\0'; // 付け忘れそうだし...
printf("代入後 [%s]\n", s);
// 文字列同士の比較
if (s == "de") { // これはNG.アドレスを比較している(内容を比較していない)
printf("BINGO\n");
}
return (0);
}
$ cc str.c -o str $ ./str 初期状態 [abc] 代入後 [de] # まぁ,面倒だったが,代入はできた. # えー,BINGO じゃないの?比較できていない. $
Cの文字列は,配列データなので, 単独データ用の演算子(==,=,+,等)は 適用できません. 代わりに,文字列処理用のライブラリ関数を利用します.
代入・比較の標準的な方法:
...
#include <string.h> // 文字列処理関数のプロトタイプ宣言
...
int main(void)
{
...
// 文字配列への代入(文字配列の書き換え)
strcpy(s, "de"); // 文字列代入関数
printf("代入後 [%s]\n", s);
// 文字列同士の比較
if (strcmp(s, "de") == 0) { // 文字列比較関数...内容が等しいとゼロ
printf("BINGO\n");
}
...
}
... $ ./str 初期状態 [abc] 代入後 [de] # 簡単に代入できた. BINGO # 比較もできた.
文字列処理では,配列処理と同様に, バッファオーバランに注意しよう: buf.c
$ cp ~/tmpl.c buf.c $ vim buf.c
#include <stdio.h>
#define BUFLEN 16 // バッファのサイズは16文字分ですが...
int main(void)
{
char buf1[BUFLEN] = "buf1";
char buf2[BUFLEN] = "buf2";
char buf3[BUFLEN] = "buf3";
while (1) {
printf("buf2(15文字以内)> "); // ...15文字分!! 1文字分は終端記号に予約
if (scanf("%s", buf2) == EOF) break; // 安全対策なし(何文字でも入力できてしまい,オーバラン)
// if (scanf("%15s", buf2) == EOF) break; // 安全対策(15文字までしか入力させない)
printf("buf1:[%s]\n", buf1);
printf("buf2:[%s]\n", buf2);
printf("buf3:[%s]\n", buf3);
printf("\n");
}
printf("\n");
return (0);
}
$ cc buf.c -o buf $ ./buf buf2(15文字以内)> abcdefg buf1:[buf1] buf2:[abcdefg] # はい,buf2 に代入できた.だから何? buf3:[buf3] buf2(15文字以内)> ...
ここで,わざと16文字以上の文字列を入力するとどうなる?
また,安全対策を施した場合はどうなる?
文字列処理の応用として, 集計用スクリプト言語Kの処理系を創作してみよう. このプログラムでは,数値データの集計作業を ユーザのコマンド入力に従って進めてゆくよ.
まずは終了コマンド exit だけの基本版を用意しよう: k.c
$ cp ~/tmpl.c k.c $ vim k.c
#include <stdio.h>
#include <string.h>
#define BUFLEN 256 // 文字列バッファのサイズ
#define BUFFMT "%255s" // バッファの入力書式...この方法はイマイチ
/*
もし,BUFLEN の値を変えたら,BUFFMT の数字も変える必要があります.
要するに二度手間...
面倒ってほどの手間ではないけど,間違えてトラブる危険性は高い.
*/
int main(void)
{
int total = 0; // 合計
char cmd[BUFLEN]; // コマンドの文字列バッファ
char *fmt = BUFFMT; // 書式文字列...イマイチな方法
/*
char fmt[16]; // 書式文字列...イケてる方法
sprintf(fmt, "%%%ds", BUFLEN-1); // 書式文字列を自動生成する
*/
/*
sprintf() は文字配列への書式付き出力ね.stdio.h
"%%" は,1文字の文字列 "%" を表わすよ.
"%d" は,数値 BUFLEN-1 の文字列..."255" に置き換わるよ.
"s" は そのまま "s" だよ.
結局,fmt の内容は "%255s" になるね.
バッファサイズ変更の手間は #define BUFLEN の1箇所だけで済むよ.
*/
while (1) {
// コマンドの入力
printf("命令 > ");
if (scanf(fmt, cmd) == EOF) break;
// コマンドの解釈・実行
if (strcmp(cmd, "exit") == 0) break; // exit コマンド
// この辺りに他のコマンドを追加してゆくよ
else { // コマンドが1個だけならこの else は冗長.break 直後なので.
printf("エラー:不明なコマンド:%s\n", cmd);
}
printf("\n");
}
printf("終了.\n\n");
return (0);
}
$ ./k 命令 > exit 終了. $
まだ,データの入力も集計もできません.
前回のファイル処理も採り入れるよ. データファイルから整数列を入力し,合計を求めよう:
...
/*
小計(ファイル内の合計)を計算する関数
引数 *file:入力ファイル名
戻り値:小計
*/
int sum(char *file)
{
FILE *fp = NULL;
int t = 0;
int x;
fp = fopen(file, "r");
if (fp == NULL) {
perror("オープン失敗");
return (0);
}
while (fscanf(fp, "%d", &x) != EOF) {
t += x;
}
printf("小計:%d\n", t);
if (fp != NULL) fclose(fp);
return (t);
}
int main(void)
{
...
char arg[BUFLEN]; // コマンド引数の文字列バッファ
...
while (1) {
...
else if (strcmp(cmd, "sum") == 0) { // sum コマンド
scanf(fmt, arg); // データファイル名の入力
total += sum(arg); // ファイル内の小計を合計
}
else if (strcmp(cmd, "show") == 0) { // show コマンド
printf("合計:%d\n", total); // 現在の合計を表示
}
...
}
...
}
$ cat data.txt # 事前に,適当なデータファイルを用意しておいてね 1 2 3 $ ./k 命令 > show 合計:0 命令 > sum data.txt 小計:6 命令 > show 合計:6 命令 > sum data.txt 小計:6 命令 > show 合計:12 命令 > exit 終了. $
データファイルに対して入力・集計できるようになった.
実用上必要になりそうな機能を追加しよう:
...
// ヘルプを表示する関数
void help()
{
printf("exit\n");
printf("help\n");
printf("reset\n");
printf("show\n");
printf("sum ファイル名\n");
}
...
int main(void)
{
...
while (1) {
...
else if (strcmp(cmd, "reset") == 0) { // reset コマンド
total = 0;
}
else if (strcmp(cmd, "help") == 0) { // help コマンド
help();
}
...
}
...
}
$ ./k 命令 > help exit help reset show sum ファイル名 命令 > sum data.txt 小計:6 命令 > show 合計:6 命令 > reset 命令 > show 合計:0 ...
もし余裕があれば,自由に機能を追加・改良してみよう. 案:
質問 Q1〜Q3 に回答し,電子メールで提出せよ.
メールの送信形式を必ず「テキスト形式 or プレーンテキスト」に変更せよ.