Unix では,データの保存や通信のために テキストファイルを利用することが多いため, C言語には.文字列処理用の標準ライブラリ関数が数多く用意されている. 今回は,これらの文字列関数について理解し,関数のクローンを作成してみよう.
プログラムの入力データや出力データをファイルに保存する際, データの表現形式としては,次の2種類がある:
なお,C言語の場合,ソースファイルはテキスト形式であり, プログラムファイルはバイナリ形式である.
既に知っている通り,Cで取り扱われる文字列は,次のように分類される:
たとえば,"Hello World" のように, プログラムの実行中に内容が変わることのない文字列.
たとえば,char s[256] の場合, 256 文字までの文字列を記録できる. 内容の書き換えも可能.
たとえば,char *p = "constant"; では, 文字列定数 "constant" が記録されているメモリ領域の先頭アドレスが ポインタ p に代入される.
文字列処理のプログラミングに失敗したくなければ, これらの違いをよく理解しておく必要がある. 以前の説明を読み直すこと.
文字列をうまく処理できれば, プログラミング言語の処理系さえも作成できるようになる. ここでは超低レベルな「プログラミング言語処理系」を作ってみたい.
この言語で使えるコマンドは, とりあえず,"end" ただ1つだけとしよう. まず,失敗例を List 1 に示す.
#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 に示す.
#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 のように変更しよう.
#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 と同一である.
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○○( ) 関数の定義例も紹介されている.
なお,ポインタ版の定義例が教科書 p.126 に紹介されているので, この課題では配列版を定義すること.
この課題ではポインタ版を定義すること. (ただし,メイン関数側では配列を使う必要がある. strcat 側では配列を使わないこと.)
動作テスト用コードの例:
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" にもなったハズ
余裕のある人は,その他の文字列処理関数についてもクローンを作成してみるとよい. (例: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) { ... }
ついでに,よくある冗長なコードの例:
while (...) { ... if (条件式) break; else { ... } }
このように if 文で break や return する場合には, 直後の処理を else ブロック化するのは余計. 次のように書けば済む:
while (...) { ... if (条件式) break; ... }