文字列操作関数郡

Cプログラミング講座



0.参考にした書籍内のページ

今回の講座は、「プログラミング言語C、第2版」のP.127〜130に書かれている文字列操作関数のソースを参考にしています。

通常とは少し変わったコードで文字列をコピーする関数を作っていたので、その他の文字列操作関数も自作してみて、最終的に組み合わせ、文字列置換関数を作るまでを解説してみようと思います。


1.文字列コピー関数strcpyの自作

string.hというヘッダファイルで定義されているstrcpy,strcat,strlenなどの文字列操作関数は、ちょっとしたループと配列操作で作ることができます。
たとえば、第1引数の文字列領域に第2引数の文字列をコピーする関数strcpyなら、一例としてこのような書き方があります。

//文字列コピー関数mystrcpy
//sはコピー先、tはコピー元
void mystrcpy(char *str1, char *str2){
	while (*str1++ = *str2++)
	;
}

補足として、本来string.hで定義されているstrcpyは、コピーされた文字列領域の先頭アドレス(ここでいうsの初期値)を返しますが、この関数の型をvoidとしているので返せません。
仕様が違うのもありますし、ライブラリ関数と名前がバッティングするのも避けたいので、関数名はmystrcpyとしています。
同じ振る舞いをするように書き換えたければ、初期のstr1の値を保存しておくために、変数を宣言するか配列のようにコピー処理を行う、などの処理が必要になるでしょう。

唯一の処理文である「*str1++ = *str2++」について演算子の順序で解説をしておくと

  1.  「*」によりアドレスsとtの指し示す先を参照
  2.  「=」によりstr1の指し示す先の値にstr2の指し示す先の値を代入
  3.  変数の後にあるので後置演算であるインクリメント「++」により最後にstr2およびstr1のアドレスの値を1つ進める

という処理を行っています。
この一文により、代入およびポインタの値を進める処理を同時に行っています。
ですが、一目見ただけでは何を目的としているコードなのかわかりにくいので、コメントは必須です。
また、これは古い表現であり、近年は特に用いられるテクニックではないようです、私も少し考えてみましたが、行数が少なくなることが、どれほどプログラムに影響を与えるのかは、いまいちわかりません。
今回の講座を作る際に用いた書籍が古いものであったので、上記のようなコードが掲載されていたことも述べておきます。

また、今説明した式はwhileの条件式となっているので、Cでは代入文そのものの持つ値は最終的に代入にした後の左辺の値であることを考えれば、str2(コピー元)の文字列中のヌル文字をコピーした後にループを抜ける事になります。


2.strcpy以外の文字列操作関数strcat,strlen,strstr

strcpyと似たような処理をする関数として、第1引数の文字列の後に第2引数の文字列を連結する関数strcatがあります。
第1引数の末尾を求める処理を行った後に、そこからstrcpyを行うことにより実装する事が出来ます。
mystrcpyと同じく、ライブラリ関数strcatとは返り値の処理や関数の型が異なります。

//文字列連結関数mystrcat
//sは連結先、tは連結元
void mystrcat(char *str1, char *str2){
	while(*str1)  //文字列終端までiを進める
		str1++;
	while(*str1++ = *str2++)  //そこからコピー
	;
}

文字列の長さを整数で返す関数strlenも、少ない行数でこのように書く事ができます。

//文字列の長さを求める関数mystrlen
//str:長さを求めたい文字列
int mystrlen(char *str){
	char *p = str;  //文字列の先頭を保存しておく
	while(*p)  //ヌル文字までstrを進める
		p++;
	return p - str;  //初期値と差を取り、要素数を求める
}

最後に、第1引数の文字列中から、第2引数の文字列パターンが最初に現れる関数strstrについて説明します。
たとえば第1引数が"abcde"、第2引数が"cd"だった場合は、第1引数中のcを指し示すポインタを返します。
ちなみに第2引数のパターンが現れない場合はヌル文字を返します。
こちらも自作してみましょう。
ポイントは、文字列の完全な照合が成功した場合に返す必要があるので、照合開始位置を記憶しておく必要があるということです。

//文字列捜索関数mystrstr
//str1:捜索元文字列、str2:照合する文字列
char *mystrstr(const char *str1, const char *str2){
	int i;  //文字列照合に使う添え字
	do {
		if (*str1 == *str2){  //str2の1文字目と一致した時
			i = 0;
			while (str1[i] == str2[i]){  //照合する、文字が異なればループ終了
				i++;
				//照合した次の文字になる
				if (str2[i] == '\0')    //ヌル文字まで進んだなら、照合終了
					return str1;  //str1に含まれるstr2の先頭を返す
				}
			
			}
		
		//str1の末尾に達するまでループ
		} while(*str1++);
	//str1の終端まで達したら、NULLを返す(str2はstr1中に含まれない)
	return NULL;
	
}

以上、ここまでstrcpy、strcat、strlen、strstr、を紹介しました。
次はこれらを用いて、テキストエディタなどで用いられる置換処理を行う関数mystrrepを作成してみようと思います。
もしポインタが文字列の最中を指す場合、そのポインタを文字列として捉らえればそこから終端のヌル文字までを文字列として考える。という点にさえ留意してくれれば、今までの関数の組み合わせで作ることができます。
文字列はポインタなどの持つアドレスから始まり、ヌル文字で終了するという使い方。これは様々なプログラムにおいて柔軟な文字列の使い方として用いられているので、是非ともこの感覚に慣れ親しんでください。


3.四つの関数を組み合わせて作る文字列置換関数mystrrep

/* mystrrep.c                                 */
/* 文字列全文 置換される文字列 置換する文字列 */
/* と実行時に引数を渡す                       */
#include <stdio.h>
#define STR_LENGTH 256	//文字列の長さ最大値

char *mystrstr(const char *str1, const char *str2);
int mystrlen(const char *str);
void mystrcpy(char str1[STR_LENGTH], const char *str2);
void mystrcat(char str1[STR_LENGTH], const char *str2);
int mystrrep(char str1[STR_LENGTH], const char *str2, const char *str3);

/*
 * 関数:main
 * 引数:文字列全文 置換される文字列 置換する文字列
 * 機能:コマンドライン引数に示す条件で文字列置換を行う
 * 返り値:正常終了時0、エラー時1
 */
int main(int argc, char *argv[]){
	char str[STR_LENGTH];	//置換処理を行う配列
	
	if (argc < 4){	//引数の数をチェック
		printf("コマンドライン引数が少なすぎます\n文字列全文 置換される文字列 置換する文字列 と指定して下さい\n");
		return 1;
	}
	
	if (mystrlen(argv[1]) >= STR_LENGTH){	//文字列全文が長すぎないかチェック
		printf("元の文字数が多すぎます\n");	//オーバーする場合は処理を終了
		return 1;
	}
	
	mystrcpy(str, argv[1]);	//argv[1]を大きく取った配列領域にコピー
	if (mystrrep(str, argv[2], argv[3])){  //置換を行う、置換できた場合は表示
		return 1;
	}
	puts(str);
	return 0;
}

/*
 * 関数名:mystrstr
 * 引数:文字列str1と、そこから検索する文字列str2
 * 機能:str1中から、最初に現れるstr2のパターンを検索し、その先頭を指すポインタを返す
 * 返り値:str1内のstr2パターンの先頭を指すポインタ
 */
char *mystrstr(const char *str1, const char *str2){
	int i;	//文字列照合に使う添え字
	do {
		if (*str1 == *str2){	//str2の1文字目と一致した時
			i = 0;
			while (str1[i] == str2[i]){	//照合する、文字が異なればループ終了
				i++;				//照合した次の文字になる
				if (!str2[i])		//ヌル文字まで進んだなら、照合終了
					return (char *)str1;	//str1に含まれるstr2の先頭を返す
			}
			
		}
		
		//str1の末尾に達するまでループ
	} while(*str1++);
	//str1の終端まで達したら、NULLを返す(str2はstr1中に含まれない)
	return NULL;
	
}

/*
 * 関数名:mystrlen
 * 引数:文字列または文字列の先頭ポインタ
 * 機能:文字列の長さを求める
 * 返り値:文字列の長さ、ヌル文字は含まない
 */
int mystrlen(const char *str){
	const char *p = str;	//文字列の先頭を保存しておく
	while(*p)	//ヌル文字までstrを進める
		p++;
	return p - str;	//初期値と差を取り、要素数を求める
}

/*
 * 関数名:mystrcpy
 * 引数:コピー先の文字列領域str1、コピー元文字列str2
 * 機能:str2をstr1にコピーする、str1の配列領域は充分大きいとする
 * 返り値:なし
 */
void mystrcpy(char str1[STR_LENGTH], const char *str2){
	while(*str1++ = *str2++)	//代入、評価、増分全てを1行で処理
		;
}

/*
 * 関数名:mystrcat
 * 引数:結合先文字列str1、結合元文字列str2
 * 機能:str2をstr1の後にコピーする、str1の配列領域は充分大きいとする
 * 返り値:なし
 */
void mystrcat(char str1[STR_LENGTH], const char *str2){
	while(*str1)	//文字列終端までiを進める
		str1++;
	while(*str1++ = *str2++)	//そこからコピー
		;
}

/*
 * 関数名:mystrrep
 * 引数:文字列str1、文字列中の置換したい文字列str2、置換する文字列str3
 * 機能:str1中の全てのstr2パターンをstr3に置換する
 * 返り値:配列領域をオーバーする場合置換せずに1、置換できた場合0を返す
 */
int mystrrep(char str1[STR_LENGTH], const char *str2, const char *str3){
	char temp[STR_LENGTH], *p;	//一時保存配列tempとポインタ操作用変数p
	int count = 0;	//str2のパターンをカウントする変数
	
	p = str1;	//str1の先頭からチェック
	while (p = mystrstr(p, str2)){	//まずはstr1中のstr2の出現数を求める
		p += mystrlen(str2);	//発見したら、pの位置をずらして続きをチェック
		count++;
	}
	if (mystrlen(str1) - count * (mystrlen(str2) - mystrlen(str3)) >= STR_LENGTH){
		//置換しても領域をオーバーしないかチェック
		printf("置換後の文字数が多すぎます\n");	//オーバーする場合は処理を終了
		return 1;
	}
	
	p = str1;	//実際の置換を先頭から行う
	while(p = mystrstr(p, str2)){	//str2のパターンが見つかる間ループ
		mystrcpy(temp, p + mystrlen(str2));	//置換される文字以降を一時保存
		*p = '\0';	//str2が現れる地点で文字列を切る
		mystrcat(str1, str3);	//str3を繋げたあとに
		mystrcat(str1, temp);	//元の文字列のstr2パターン以降を戻す
		
		p += mystrlen(str3);
		//置換による無限ループを回避するために、置換した部分は検索しない
	}
	return 0;
}

mystrrepは様々な文字列操作関数を組み合わせて作られている文字列置換関数です。
第1引数に元の文字列、第2引数に置換対象の文字列、第3引数に最終的に置換される文字列を渡すと、文字列を置換してくれます。
作成にあたっては、実際の文字列置換の際に気をつけておくことをチェックする必要があります。

アルゴリズムを紹介しておきます。
文字列"abcde"中の"cde"を"ab"に置換する事を考えます。
関数から見れば、str1が"abcdab"、str2が"cd"、str3が"abab"と渡されます。
まずは関数mystrstrにより、str1中にstr2のパターンがないか探します。
この場合ですと、mystrstrが返す値はstr1中の'c'を指すポインタですので、この地点からstr3で置換すればいいわけです。
ですが今回のように、str2がstr3よりも短いケースですと、最初str2があった部分をオーバーしてstr3が上書きされてしまいます。
なので、一時的にstr2のパターンが終わる以降を保存しておかなければなりません。
サンプルコードですと、文字列tempに"ab"が保存されます。
次に、str2のパターンが現れる地点に文字列の終端を示す'\0'(ヌル文字)を代入すれば、そこで文字列を終了させることができます。

その後、str1とstr3において関数strcatを適用すれば、str1の終端にstr3のパターンを無事挿入することができ、
このときstr1は"ababab"となるわけです。
あとは退避させておいたtempをstr1に結合すればいいので、strcatを適用すれば、str1は"abababab"となり、置換が成功します。

今回の文字列置換関数mystrrepはstr1に現れるstr2のパターン「すべてを」置換します。
置換した後の文字列に置換対象の文字列が含まれていた場合、関数strstrによる検索位置を調節しておかないと、置換したばかりの文字列を再び置換されるべき文字列として判定してしまい、以後無限ループに陥ってしまいます。
これに関しては、置換後に置換した分の文字数を進めることにより、置換が終わった地点を走査の先頭とし、次の置換を行えば解決できます。
その他に、「元の配列の領域をオーバーしないか」というチェックも、実際に置換を行うと何文字になるのかを求めて、置換後の文字列も格納できるのか判定しなければなりません。

以上の事に気をつけながら関数strrepを作成し、あとはmain関数を、コマンドライン引数を受け取り、関数strrepを実行して文字列を置換後表示するように書いて完成です。


4.テキストファイル処理に……

文字列操作を発展させれば、テキストファイルに対する操作なども既存の操作から外れた柔軟な操作ができるので、例えば「特定の文字列パターンが先頭に現れる行のみを抜き出して、新たなファイルを作成する」とか、単純な結合ではなく「2つのファイルの内容が何行かおきに現れるような新しいファイルを作る」など、目的にあった処理ができます。

コーディングする手間はかかってしまいますが、いちいちテキストエディタで目的の処理を手作業で行うよりもはるかに楽なはずです。

今までに習ってきたライブラリ関数や、世の中にあるちょっとした便利ツールも、プログラムを学習した今ならば「意外と簡単なアルゴリズムで動いているのでは?」と思える事も少なくありません。

もしあなたが持っているテキストファイルに、単純な処理をさせたいなと思い、手作業では時間がかかるが目的に合う操作がエディタに無い、という状況になった時など、プログラムを自作してみて、処理させてみてはどうでしょうか。


第1回講座へ
第3回講座へ

Cプログラミング講座 トップページへ


ページ作成者:elde
連絡先:s111058@wakayama-u.ac.jp