07 月 15 日(水)1-2h

文字列処理(1)

Unix では,データの保存や通信のために テキストファイルを利用することが多いため, C言語には.文字列処理用の標準ライブラリ関数が数多く用意されている. 今回は,これらの文字列関数について理解し,関数のクローンを作成してみよう.

文字列関数を使いこなせば, プログラミング言語の処理系なども自作できる.

テキストファイルとバイナリファイル

プログラムの入力データや出力データをファイルに保存する際, データの表現形式としては,次の2種類がある:

Wind○ws 等のクローズドシステムでは, (解消されつつはあるようだが) 今なお,バイナリファイルが蔓延している. しかし,現代のネットワーク社会では,オープン化が重要であり, HTML や XML 等,テキストファイルの役割が大きくなってきている.

なお,C言語の場合,ソースファイルはテキスト形式であり, プログラムファイルはバイナリ形式である.

要するに,オープンソースが重要だ. 自作のプログラムを配布する場合, プログラムファイル(バイナリ)ではなく, ソースファイル(テキスト)を公開しよう. そして,コーディングの際には, 多様なシステムで共通に動作するように, 標準規格に準拠した「正しいプログラム」を意識しよう.

文字列の分類

既に知っている通り,Cで取り扱われる文字列は,次のように分類される:

文字列処理のプログラミングに失敗したくなければ, これらの違いをよく理解しておく必要がある. 以前の説明を読み直すこと.


超低レベルな言語処理系

文字列をうまく処理できれば, プログラミング言語の処理系さえも作成できるようになる. ここでは超低レベルな「プログラミング言語処理系」を作ってみたい.

この言語で使えるコマンドは, とりあえず,"end" ただ1つだけとしよう. まず,失敗例を List 1 に示す.

List 1. 言語処理系の失敗プログラム lang.c
#include <stdio.h>

int main()
{
	char cmd[256];

	while (1) {
		printf("コマンド > ");
		scanf("%s", cmd);

		if (cmd == "end") break;	// "end"コマンドで終了したいんだが...
	}
	return (0);
}

このプログラムは,うまく動作しそうに見えるが...

実行例:

$ ./lang
コマンド > end
コマンド > end
...		# 終われねー

[Ctrl]+[C]	# 強制終了
$

失敗の原因としては, List 1 の条件式 (cmd == "end") では, 文字配列 cmd と文字列定数 "end"文字列の内容を比較しているのではなく, 文字列のアドレスを比較しているだけだからだ.

文字列の値は先頭アドレスであり, 文字列の内容ではない.

たとえ,cmd"end" が,「同じ内容」であったとしても, これらのアドレスは異なる(文字列の内容は異なる場所に記録されている)ので, 「等しくない」ということになる. 文字列の内容を比較するには, ライブラリ関数 strcmp( ) を使ばよい. 次のようにコードを修正せよ:

#include <stdio.h>
#include <string.h>
...
		if (strcmp(cmd, "end") == 0) break;	
...

実行例:

$ ./lang
コマンド > end

$	# 終われたー

関数 strcmp( ) は, 文字列の内容を1文字ずつ比較してくれる.


文字列関数の利用

Cでは,文字列処理のためのさまざまな関数が 標準ライブラリ libc 内で定義されている. 次のリストは,ヘッダファイル string.h 内で プロトタイプ宣言されている関数の例である:

用語的に,関数の「宣言」と「定義」との区別に注意せよ. ヘッダには宣言,ライブラリには定義が書かれている. 以前の説明を読み直そう.

ここで,「文字列」と記されている引数については, 定数・ポインタ・配列のどれでも構わない. 一方,「文字配列」な引数に使ってよいのは, 文字配列(または文字配列へのポインタ)だけだ.

もう少し詳しく,strcmp( ) を試してみよう. この関数は2つの文字列の辞書的な順序を比較する. ここで辞書的順序とは, 実際には ASCII コード順のことだが, アルファベット順と思っていて構わない. この関数の戻り値は次のようになる:

この関数を使用したプログラムの例を List 2 に示す.

List 2. 関数 strcmp( ) の利用例 strcmp-1.c
#include <stdio.h>
#include <string.h>

int main()
{
	char  s1[256], s2[256];
	int   d;
	char  c;

	printf("英単語2個 > ");
	scanf("%s %s", s1, s2);
	d = strcmp(s1, s2);
	if (d < 0) {
		c = '<';
	} else if (d == 0) {
		c = '=';
	} else {
		c = '>';
	}
	printf("%s %c %s\n", s1, c, s2);

	return (0);
}

実行例:

$ ./strcmp-1
英単語2個 > bacon  egg
bacon < egg

文字列関数の作成

次に,文字列処理ライブラリ関数と同じように動作する関数 (関数のクローン)を作成してみよう. List 2 を List 3 のように変更しよう.

List 3. strcmp( ) の定義例(配列版)strcmp-2.c
#include <stdio.h>
//#include <string.h>	// 文字列ライブラリを利用しないので不要

// strcmp() のクローン
int myStrcmp(char s1[], char s2[])
{
	int i = 0;

	while (s1[i] == s2[i]) {
		if (s1[i] == '\0') break;
		i++;
	}
	return (s1[i] - s2[i]);
}

int main()
{			// List 2 と同様
	.
	.
	// ... strcmp(...) ...
	... myStrcmp(...) ...
	.
}

たとえば,List 2 のライブラリ関数 strcmp( ) の処理内容は, List 3 のユーザ関数 myStrcmp() と同じである. 動作が変わっていないことを確かめよう.

「まったく同じ」というわけではないが「ほとんど同じ」だ.

また List 4 は,文字配列の代わりにポインタを利用したソースの例である. 動作は List 3 と同一である.

List 4. strcmp() の定義例(ポインタ版)strcmp-3.c
int myStrcmp(char *s1, char *s2)
{
	while (*s1 == *s2) {
		if (*s1 == '\0') break;
		s1++;
		s2++;
	}
	return (*s1 - *s2);
}

List 4 では,List 3 と比較して, 変数と計算が削減されている. つまり,メモリ使用量が少なく,動作が速いので, List 4 の方が「良いプログラム」だ.

なお,計算量については, 表面的には,逆に,カウント処理 ++ が1個増えたかのように見える. しかし,内部的には,配列要素 s[i] の アドレス計算 *(s + i) が複数個減っている. したがって,トータルでは,List 4 の方が効率的ということになる.

なお,教科書 pp.126-130 には, 他の str○○( ) 関数の定義例も紹介されている.


本日の課題

  1. 標準ライブラリ関数 strlen( ) のクローン int myStrlen(char s[]) およびそれをテストするためのメイン関数を定義せよ.
  2. なお,ポインタ版の定義例が教科書 p.126 に紹介されているので, この課題では配列版を定義すること.

    strlen() は文字列の長さを返す.
  3. 標準ライブラリ関数 strcat( ) のクローン char *myStrcat(char *s1, char *s2) およびそれをテストするためのメイン関数を定義せよ.
  4. この課題ではポインタ版を定義すること. (ただし,メイン関数側では配列を使う必要がある. strcat 側では配列を使わないこと.)

    strcat() は2つの文字列を連結し, 連結後の文字列を第1引数の配列に代入する. そして,連結後の文字列の先頭アドレスを返す.

    動作テスト用コードの例:

    char s1[256]="abc";
    char s2[]="def";
    char *p;
    p = myStrcat(s1, s2);
    // p = strcat(s1, s2);	// 標準ライブラリ関数と動作比較
    printf("%s\n", s1);	// s1 = "abcdef" になったハズ
    printf("%s\n", p);	// p = "abcdef" にもなったハズ
    
    ヒント:配列の要素数について, s2 では省略可能だが, s1 では省略不可能. その上,char s1[4] = "abc"; では不足だ. なぜなのかわかるかな?

余裕のある人は,その他の文字列処理関数についてもクローンを作成してみるとよい. (例:strchr( )strstr( )strncmp( )strncpy( ),等)

アドバイス:

レポート提出 注意事項: 以下の点についても厳しくチェックする:


補足

教科書 p.128 の strcpy( ) について, わかり易く書き換えてみる.

まず,高度なコード(教科書の流儀, 高度すぎるのでマネするな!!):

while ((*s = *t) != '\0') {	// こりゃ何だ?
	s++;
	t++;
}

Cでは,工夫次第で, 1個の文に複数の手順を詰め込めてしまう. しかし,短けりゃ良いってものではない. 「過ぎたるは及ばざるがごとし.

基本の作法としては,1文には1手順だけ書こう. わかり易いコード(こちらを推奨):

while (1) {			// 繰り返し...あー
	*s = *t;		// 文字代入...要するに
	if (*s == '\0') break;	// 終了条件...こういうことね
	s++;
	t++;
}

このように, わかり易く,かつ無駄なく, そしてもちろん不足もなくコーディングしよう.

なお,無条件反復 while (1) については,乱用に注意しよう. あくまでも条件反復が基本であり,本当に必要な場合だけ無条件反復を使うべき. まず,冗長な(無駄に長い)書き方(よろしくない例):

while (1) {
	if (条件式a) break;
	...
}

これと同じことをコンパクトに表現できる(こちらがオススメ):

while (条件式b) {
	...
}
条件式a,b の違いに注意せよ. 条件式a は終了条件, 条件式b は継続条件(反復条件), 互いに逆の条件だ.

ついでに,よくある冗長なコードの例:

while (...) {
	...
	if (条件式) break;
	else {
		...
	}
}

このように if 文で break や return する場合には, 直後の処理を else ブロック化するのは余計. 次のように書けば済む:

while (...) {
	...
	if (条件式) break;
	...
}

(c) 2015, yanagawa@kushiro-ct.ac.jp