文字列処理2(関数クローン作成)

文字列処理の標準ライブラリ関数を利用するだけでなく, それらのクローン(clone;そっくりさん,等価な関数)を自分自身で作成し, 仕組みを理解しよう.

なお,文字列は,文字の配列であり, 連続的なメモリ領域に格納される. このため,ポインタを利用すると, 効率的な文字列処理が可能となる.

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

文字列処理関数の利用・作成

ライブラリ関数のオンラインマニュアル

C言語の標準ライブラリ関数のマニュアルは, Linux 等のunix系OS では一般に, 次のようなコマンドで閲覧できる:

$ man 3 関数名
大抵の場合,番号 3 は省略 OK. ただし,unix コマンドと同名の関数の場合,省略 NG. 例えば,man printf を試してみよう. マニュアルには関数以外のものも収録されている.
関数のマニュアルには, ヘッダファイル名,引数,戻り値,機能,等について, 簡潔に記述されている.

操作方法としては,ページ移動はカーソルキー,[Spc]キー,[B]キー等, 終了は [Q]キーとなっている.

操作はテキスト閲覧用プログラム less と同じ. てゆーか,man は標準 UI として less を利用している.
Step 1. 標準ライブラリ関数の利用

基本的な文字列処理関数は, 入出力関数やファイル処理関数と同様に, 標準ライブラリ libc として,すでに用意されている. まず,その中でも超基本的な関数 strlen()strcmp()strcpy() を使ってみよう: string.c

これらの内,strcmp()strcpy() については前回も利用した.
$ cp ~/tmpl.c string.c
$ gedit string.c &
#include <stdio.h>
#include <string.h>	// 標準文字列処理関数 str*() の宣言

#define	BUFLEN	256		// 文字列バッファのサイズ
#define	BUFFMT	"%255s"		// 文字列バッファの入力書式(文字列定数,基本の技)

int main(void)
{
	char	buf[BUFLEN];		// 入力文字列のバッファ(文字配列)

	char	*fmt = BUFFMT;		// 入力書式(文字列定数へのポインタ,基本の技)
/*					// or 入力書式の自動生成(上級者向けの技)
	char	fmt[16];			// 入力書式のバッファ
	sprintf(fmt, "%%%ds", BUFLEN - 1);	// 入力書式の自動生成
*/
	int	n;			// 文字列の長さ
	int	d;			// 文字列の比較結果
	char	pre[BUFLEN] = "";	// 直前の入力文字列

	while (1) {
		printf("文字列(%d文字以内)> ", BUFLEN-1);
		scanf(fmt, buf);

		n = strlen(buf);		// 文字列の長さの測定
		printf("文字数=%d\n", n);

		d = strcmp(buf, pre);		// 文字列の内容の比較
		if (d == 0) break;	// 同内容の文字列が続いたら終了

		strcpy(pre, buf);		// 文字列の内容の代入(pre = buf)
	}
	return (0);
}

このプログラムでは,文字列をキーボード入力し, 文字数(文字列長)を表示する. もし,同じ内容の文字列が連続したら終了する.

$ cc string.c -o string
$ ./string
文字列(255文字以内)> hello
文字数=5
文字列(255文字以内)> bye
文字数=3
文字列(255文字以内)> bye
文字数=3
$
Step 2. 標準関数クローンの作成1

次に,標準文字列処理関数のクローンを作成してみたい. まずは,strlen() について, 仕様を確認しよう:

$ man strlen
補足:

クローン関数およびテスト用メイン関数の ソースコードは概ね次のようになるだろう: (string.c を改造)

#include <stdio.h>
// #include <string.h>		// 標準文字列関数は不使用に

// strlen() クローンの定義例1(配列版)
int mystrlen1(char s[])
{
	int	n = 0;	// 文字数・要素番号

//	while (s[n] != '\0') {		// これでも良いですが...
	while (s[n]) {		// '\0' == 0 → 条件不成立なので,コンパクトに記述
		n++;	// 文字数・要素番号のカウント(次の文字へ)
	}
	return (n);
}

// strlen() クローンの定義例2(ポインタ版)
int mystrlen2(char *s)
{
	int	n = 0;	// 文字数

//	while (*s != '\0') {
	while (*s) {
		n++;	// 文字数のカウント
		s++;	// ポインタの前進(参照先を次の文字へ)
	}
	return (n);
}

#define	...
...

int main(void)
{
	...

	while (1) {
		printf(...);
		scanf(...);

	//	n = strlen(buf);	// 標準関数は不使用に
		n = mystrlen1(buf);	// クローン関数1
	//	n = mystrlen2(buf);	// クローン関数2
		printf("文字数=%d\n", n);

		...
	}
	return (0);
}

動作的には,配列版とポインタ版は, どちらも標準関数と同じである. 各自で確認しよう.

ソースコード的にも両者はあまり変わらないが, 実行効率的には,一般に,ポインタ版の方が優れている. たとえば,文字列 s 内の1文字にアクセスする場合を考えよう. 配列版では「s[i]」等と記述することになるが, これは内部的には「*(s + i)」という, 2手順の計算(アドレス計算「s+i」と間接参照「*」) として実行される.

一方,ポインタ版では間接参照「*s」の1手順だけで済み, 配列版よりも計算量が少ない. また,要素番号の変数 i も不要となり, メモリ使用量も少なくできる.

ただし,この strlen() クローンの場合, 単純すぎるので両者の性能は同じ. 両者を比較すると,配列版では s[i] の計算(s+i)1回分, ポインタ版でも s++ の計算1回分が他方にないものであり, 計算量的には互角となっている.
しかし,より複雑なプログラムの場合, s[i] とか *s を2回以上使うなら, 配列版では無駄が多く, ポインタ版が有利となる.
正しい結果が得られれば何でも良いだろって? プログラムの用途によっては,処理効率を考慮して作らないと, 重い,遅い,使えねー,要らねー,ってなりますよ.
Step 3. 標準関数クローンの作成2

では,strlen() と同様に, strcpy()strcmp() についても クローンを定義してみたい.

仕様確認:

$ man strcmp
$ man strcpy

ソースコード: (string.c を改造)

...
// #include <string.h>		// 標準関数は不使用に

// strcpy() のクローンの定義例
char *mystrcpy(char *dst, char *src)
{
	char	*r;	// 戻り値
	r = dst;	// コピー先の先頭アドレス
	while (1) {
		*dst = *src;	// 1文字ずつ代入
		if (*src == '\0') break;
			// 終端記号も代入して終了
		src++;		// 代入元の次の文字へ
		dst++;		// 代入先も次の文字へ
	}
	return (r);
}

// strcmp() のクローンの定義例
int mystrcmp(char *s1, char *s2)
{
	while (*s1 == *s2) {
		if (*s1 == '\0') break;
		s1++;
		s2++;
	}
	return (*s1 - *s2);
}

#define ...
...

int main(void)
{
	...
	while (1) {
		...

	//	d = strcmp(buf, pre);	// 標準関数は不使用に
		d = mystrcmp(buf, pre);	// クローン関数
		...

	//	strcpy(pre, buf);	// 標準関数は不使用に
		mystrcpy(pre, buf);	// クローン関数
	}
	return (0);
}

練習問題

標準ライブラリ関数のクローンを作成せよ.

次の基本的な文字処理・文字列処理のライブラリ関数について, クローンを定義してみよう.

文字種検査関数では,仮引数の文字 c は, char型ではなく, int型であることに注意しよう.

なぜ,文字処理なのに整数なのか? この工夫によって,isdigit(getchar()) のように使えて, 実引数が EOF(非文字,符号付き整数 -1)の場合でも, よろしく処理できるようになっている.
なお,実引数は,isdigit('j') のように,char型でも OK. 自動的に型変換(char型→int型へ格上げ)される.
上級者向け:数値と文字列の相互変換・入出力関数を作成せよ.

これまで,入出力関数で数値を入出力する際, scanf("%d"...) や printf("%f"...) 等としてきた. 実はこれらの関数内では, 数値そのものを入出力している訳ではなく, 文字列(数字列)を入出力している. また,そのために,数値を文字列へ変換したり, 逆に文字列を数値へ変換している.

次の関数について,クローンを作成せよ: (標準ライブラリ関数を極力,使わずに...)