はじめての C

C programming note*1
プログラム cat3 には、入力した文字を そのまま 標準出力する 関数 do_one() が 含まれています。

void do_one(FILE *fp)
{
int c;

while )((c = fgetc(fp))( != EOF)
fputc(c, stdout);
}

do_one() を、その入出力を 配列を使って 行単位で扱う 関数 cat() に つくり直します。
#define MAX_SIZE (80 + 1 + 1) /* plus '\n' + '\0' */

void cat(FILE *fp)
{
char buf[MAX_SIZE];

while (fgets(buf, MAX_SIZE, fp) != NULL)
fputs(buf, stdout);
}

出入力用の 標準ライブラリ関数 fgetc() と fputc() とが、それぞれ fgets()、fputs() に 替わっています。
まず、文字列の扱い、

C には 「文字列 string のための データ型」 は 準備されていない。 では どのようにして 文字列を 扱うのかというと、文字型 char type の 配列 array として 扱うことになる。(p122)

こんな かんじ、ですか → char array[n] or char *array

たとえば "hello" という 5文字分の 長さの 文字列が s という名前の 配列に 入っているようすは 次のように なっている。


s[0] s[1] s[2] s[3] s[4] s[5] ... s[n]
h e l l o \0

文字列の終わりを 示すために、文字列の 最後の文字の すぐ後ろに '\0' という コードが 入っている。

これは ナル文字 null character と 呼ばれ、C では 文字列の終端を表す シルシとして 使われる。

文字列の終わりに この '\0' が 必要なので、文字列用に 使う 配列の 要素の数は、文字列 + 1 に なる。(p122-123)

次は 行 line の 定義、

UNIX の ファイルは、ただの 一次元の バイト列だ。 それ以外の 何の構造も もっていない。

行 line という 概念は、その一次元の バイト列に 「改行」の コードが 入っていることだけで 実現されている

改行のコードは '\n' で 表わされる。(p123)

fputs() は ちょっと 置いといて、次は 入力用の 標準ライブラリ関数 fgets() と その引数、

fgets() は 3つの 引数を とる。 最後の - 3つ目の - 引数で それが 取り扱う ストリームを 指定する。

1つ目の 引数で、1行分の データ格納場所の 先頭アドレスを 指定する。

配列の名前を (添字を 指定せずに) 書いた場合は、その1つ目の要素へのポインタ、つまり 先頭のアドレスという 意味になる。

この格納場所は、呼び出す側で 配列を 宣言するなどして 確保しておく 必要がある。(p124)

残りの引数、

第2引数だが、ここには この格納場所の サイズ (Byte 単位) を 指定する。

fgets() は、指定された ストリームから MAX_SIZE - 1 文字に なるまで、または 改行文字が 現れるまで (いずれか 先に 到達したほうまで) 読み込み、それを 第1引数で 指定された場所に 入れる。(p125)

読み込んだ 行の処理は、

読み込まれた 最後の文字の 後には、続けて 1つの ナル文字が 書かれる。

たとえば fgets() が "Dill" という 文字列と 改行コードからなる 1行を d という配列に 読み込んだときの 状態は、


d[0] d[1] d[2] d[3] d[4] d[5] ...
D i l l \n \0

のように なっている。

改行コード '\n' も 入ったあとに ナル文字 '\0' が 入っているわけだ。(P125)

今度は マクロの NULL のほう、

fgets() は 通常は 1番目の 引数と 同じ値を そのまま 関数の値として 渡すが、ファイルの末尾など ストリームの終端に 達すると NULL を 返す。

これは ナルポインタ null pointer と 呼ばれ 「どこも指していない ポインタ」を 意味する。

fopen() が エラーのとき 返すのも これである。

少し わかりにくく なってきたので、途中だけど、K&R 2nd から fgets() のコードを 書き写してみます。(p201)

/* fgets: get at most n chars from iop */
char *fgets(char *s, int n, FILE *iop)
{
register int c;
register char *cs;

cs = s;
while (--n > 0 && (c = getc(iop)) != EOF)
if ((*cs++ = c) == '\n')
break;
*cs = '\0';
return (c == EOF && cs == s) ? NULL : s;
}

細かいところは 置いといて、getc() が 使われていますね。
while文が 終了するのは、改行文字に 到達したか、または この getc() が EOF - end of file ファイル終了の サイン - を 返したときです。
また while から 抜け出すときには、どちらも 文字列の後に '\0' が 追加されています。
そして fgets() の 関数としての 戻り値は char型の ポインタなので、ファイルの 終端になると null pointer を 返すわけです。
では、なぜ NULL を 戻り値として 設定するのか、

(たとえば) fopen() が 正常に オープン処理を 行なったときに 返すのは 「ストリームへのポインタ」だ。 そこで、エラーを示す 値は それと 区別できる 必要がある (たまたま ポインタが その - ストリームへのポインタの - 値になっては 困るわけだ。

NULL は 「どのアドレスも 指していないことが 保証された 値」 であり、ポインタが 取り得る どの (正常な) 値とも 区別することが できるので、ポインタ (アドレス) を 返す 関数では、この fgets() や fopen() に限らず、エラー時の 値として NULL を 返すのが 一般的である。(p125)

また、プログラム作成のときの 注意点として、

ポインタの値を 0 と 比較したり ポインタに 0 を 代入すると、それは コンパイラによって 自動的に null pointer の値として 読み替えられるが、(それだと) 混乱を まねくので、0 とは 書かず NULL という (マクロとして 定義された) 名前を 必ず 使うようにすべきだ。(p127)

あと、残っている fputs() ですが、やはり K&R にある コードを 写すと、

/* fputs: put string s on file iop */
int fputs(char *s, FILE *iop)
{
int c;

while (c = *s++)
putc(c, iop);
return ferror(iop) ? EOF : 0;
}

予想どおり、ここでは 出力関数 putc() が 使われていました。

*1:「作ってわかる Cプログラミング」