このページをはてなブックマークに追加このページを含むはてなブックマーク このページをlivedoor クリップに追加このページを含むlivedoor クリップ

目次

fgets

  • 1行を読み込み指定した空間に格納する。
  • "File GET String"
  • 【エフゲットエス】

fgetsのプロトタイプ

char *fgets(char *line, int maxline, FILE *fp);

戻り値

  • 成功した場合
    • 読み込んだ文字列へのポインタ(文字列の先頭アドレス)
  • 失敗した場合
    • NULL
      • EOFを検出し、かつ格納スペースlineに1文字も読み取っていなかった場合、配列の内容を変化させずに残し、NULLを返す。
      • 読取りエラーが発生した場合もNULLを返すが、この場合の配列の内容は不定である。

fgetsの特徴

  • ファイルfpから読み込み、改行文字またはファイル終端(EOF)を見つけると、末尾をヌル文字に変換して1行の文字列として切り出して(改行も含む)、第1引数で指定された文字配列lineに格納する。
  • 改行文字またはEOFを見つけるよりも前に第2引数で指定された「最大サイズ-1」の文字数に達した場合も、ヌル文字をつけて文字列として切り出す。
  • lineに読み込まれるのはmaxline-1個の文字であり、最後に必ずヌル文字が付け加えられる。
    • 改行文字またはEOFを見つけるよりも前に第2引数で指定されたmaxline-1の文字数に達した場合でも、ヌル文字をつけて文字列として切り出す。
    • 例えば、stdinをファイルに指定していた場合は、stdinに残留した文字列が残っている。
      • 入力に必ず改行が含まれるわけではない。よく改行が存在するかどうかをstrchrなどでチェックするソースコードが存在するが、一般的にそれでも動くが、万全とはいえない。なぜならば、「^Z」などで改行なしで1行が終了している可能性があるからである。
  • 用意された大きさ分だけのデータを格納するので、scanf()やgets()と比べれば安全である。もしscanf()やgets()を使うとしたら、格納文字数に注意が必要になってしまう。
  • 通常はlineを返し、EOFの検出時(ただし条件付)やエラー時にNULLを返す。
  • fgets()と対の関数としてfputs()が存在する。

テクニック

lineから改行を取り除く

strrchrを利用する

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
 
 
 
-
|
!
char *p;
 
fgets(command, COMMAND_LENGTH, stdin);
if (( p = strrchr(command, '\n')) != NULL) {
    *p = '\0';
}

strlenを利用する

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
 
-
|
!
fgets(command, COMMAND_LENGTH, stdin);
if (command[strlen(command) - 1] == '\n') {
    command[strlen(command) - 1] = '\0';
}

stdinの残留データの取扱い

 バッファのサイズを越えてしまうと、残りのデータは入力ストリーム(stdin)に残留したままになってしまい、次のfgets()の呼び出し時に残っている入力ストリームを読み込んでしまう。よって、プログラムによっては対処方法が必要である。

fflushを利用する

 fgets()を実行した直後にfflushでstdinを消去すればよい。しかし、ANSI Cではfflush(stdin);の結果は未定義なため、移植性が低くなってしまう問題がある。

fflush(stdin);

 ちなみに、MS VC2008ではfflush(stdin);はうまく動作する。

 fflushの代わりにrewindを使って、次のようにしても同様にstdinを消去できる。

rewind(stdin);

[補講]fflush()はバッファを強制的にフラッシュし、rewind()はストリームの位置を先頭にセットする関数である。 ◇

mallocを利用する

 バッファのサイズを越えてstdinに残留データが存在したならば、バッファの大きさを拡張して、再度fgets()を呼び出す。

1行には必ず改行が存在するという前提における対処方法

 1行には必ず改行が存在するという前提であれば、残留データを消去する方法がいくつか考えられる。

 まず、scanf()で改行までの入力を読み飛ばす方法である。

Everything is expanded.Everything is shortened.
  1
  2
  3
-
|
!
if (strchr(buf, '\n') == NULL) {
    scanf("%*[^\n]%*c");
}

 %*[^\n]によって\nの1つ前までスキップし、%*cで\nそのものをスキップするという動作らしい。

 次に、whileとfgetsを利用する方法である。

Everything is expanded.Everything is shortened.
  1
  2
  3
-
|
!
if (strchr(buf, '\n') == NULL) {
    while(strchr(fgets(buf, BUF_LEN, stdin), '\n') == NULL);
}

 fgetsでまとめてゴミ箱用のbufに捨てていくのではなく、せっかく捨てるなら1つずつ捨てていき捨てた回数、即ち削除した数をカウントし、それを返す関数を作るとよいかもしれない。

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
-
|
|
-
|
-
|
!
|
!
|
!
int stdin_erase(FILE *fp){
    int c;
    int erase_count = 0; /* 消去した回数を数えるカウンタ */
    while (1) {
        c = fgetc(stdin);
        if ((c == '\n') || (c == EOF)) {
            return erase_count;
        }
        erase_count++;
    }
    return erase_count;
}

fgets+sscanf_sの組み合わせ

 fgetsで1行データを取ってきて、それからsscanf_sで必要なデータを加工してから変数に格納するという手法がよく取られる。

ファイル入出力の中でfgetsを使う

置換洩れを防ぐ

 Windows NT/XPの場合、バイナリモードで開いてfgets()やfputs()を使うと、改行文字の置換洩れが発生することがある。なぜならば、fgets()やfputs()は本来テキストを取り扱う関数だからである。

 よって、

if ((pfile1 = fopen(pfilename1, "r")) == NULL)

ではなく、

if ((pfile1 = fopen(pfilename1, "w")) == NULL)

のようにfopenを使う必要がある。

fgetsのソースコード

『新ANSI C言語辞典』バージョン

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
-
|
-
|
|
|
|
|
|
|
!
|
|
|
|
|
!
char *fgets(char *s, int n, FILE *stream){
    char *const head = s;        /* 先頭ポインタを保存 */
    for (; n > 1; n--) {
        int c = fgetc(steram);    /* 1文字入力 */
        if (c == EOF)
            break;
        *s = c;                    /* 配列に代入 */
        s++;                    /* sを進める */
        if (c == '\n')
            break;
    }
    /* 入力失敗のとき */
    if (s == head || ferror(stream))
        return NULL;
    *s = '\0';
    return head;
}

 ソースコードを解説する。

 2行目で、まず配列の先頭アドレスをポインタheadに格納しておく。headは読み込み専用である。

	char *const head = s;

 3行目〜11行目。streamから1行ずつ読み込み、EOFあるいは改行でない限り、順番に配列へ格納していく。なお、streamに改行しかなかった場合、配列に格納してから、ループを抜ける。これでfgetsが改行も含んで転送することが理解できるはずである。

	for (; n > 1; n--) {
		int c = fgetc(steram);	/* 1文字入力 */
		if (c == EOF)
			break;
		*s = c;					/* 配列に代入 */
		s++;					/* sを進める */
		if (c == '\n')
			break;
	}

 13行目〜14行目。NULLを返す条件である。条件は2つある。1番目の条件はEOFを検出し、かつ格納スペースsに1文字も読み取っていなかった場合である。もしEOFを検出しなかったら、1回は必ず転送しているはずなので、この条件に当てはまらない。2番目の条件はstreamのファイルエラーが検出されたときである。

	if (s == head || ferror(stream))
		return NULL;

 15行目で、必ず最後はヌル文字を文字列の後ろに付ける。

	*s = '\0';

 16行目。エラーが起きなければ、最後はポインタheadを返す。

	return head;

K&Rバージョン

Everything is expanded.Everything is shortened.
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
-
|
|
|
|
|
|
|
|
|
!
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;
}

 ソースコードを解説する。

 5行目。配列sの先頭要素のアドレス(即ち&s[0])をcsにセット。入力値のsを壊さないようにするため。

cs = s;

 6行目。whileのループ処理。iopから1文字ずつgetcしたときEOFでない限り、さらにnが正の数である限り、ループされる。つまり、iopの中身がなくなるか、n-1回1文字ずつの読み込みが終わるかすれば、ループが終わる。

	while (--n > 0 && (c = getc(iop)) != EOF)

 7行目。c(iopからgetcした1文字)をポインタcsが指すアドレス値であるメモリに代入する。そして、それが\nであれば、8行目のbreakでwhileループを抜ける。つまり、\nが来るまで1文字ずつ配列sに代入作業を行う。1回の代入ごとにポインタcsはインクリメントされていく。

		if ((*cs++ = c) == '\n')
			break;

 9行目。while文から抜けたら、格納された文字列の末尾に\0を追加する。

	*cs = '\0';

 10行目。もしcでwhileループから抜けたとき(即ちcがEOFのとき)、かつcsがまったくインクリメントされていなければ、NULLを返す。そうでなければ、配列sの先頭要素のアドレスを返す。

	return (c == EOF && cs == s) ? NULL : s;

参考文献

  • 『新ANSI C言語辞典』
  • 『プログラミング言語C 第2版』
  • 『美しいCプログラミング見本帖』