文字列処理1(基本)

文字と文字列の処理を理解しよう. さらに,標準ライブラリ関数を利用して, プログラミング言語っぽいものを作ってみよう.

教科書の該当範囲:第6章(6.2.3),第10章

文字の処理

文字の計算?

文字データ処理の例として,まずは,アルファベットを表示してみよう: 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("%3d [%c]\n", c, c);	// "番号 [文字]"
	}
	return (0);
}
$ cc ascii.c -o ascii
$ ./ascii
...
文字の書式付き入出力

まずは,慣れ親しんだ方法として, 書式付き入出力 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");
	}
	...
}
初期状態 [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文字以上の文字列を入力するとどうなる?

代入はできるが,他の変数も変わってしまう場合がある.

また,安全対策を施した場合はどうなる?


スクリプト言語処理系の作成

Step 1. 基本部分

文字列処理の応用として, 集計用スクリプト言語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() は文字配列への書式付き出力ね.
		"%%" は,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
終了.

$

まだ,データの入力も集計もできません.

Step 2. 機能追加

前回のファイル処理も採り入れるよ. データファイルから整数列を入力し,合計を求めよう:

...
/*
小計(ファイル内の合計)を計算する関数
引数 *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
終了.

$

データファイルに対して入力・集計できるようになった.

Step 3. さらなる機能追加

実用上必要になりそうな機能を追加しよう:

...

// ヘルプを表示する関数
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
...
Step 4. 自由に機能追加

もし余裕があれば,自由に機能を追加・改良してみよう. 案:


本日の課題

レポートを提出せよ.

質問 Q1〜Q3 に回答し,電子メールで提出せよ.