【C言語】fgets 関数について解説(テキストファイルの読み込み)

fgets関数の解説ページアイキャッチ

このページでは、C言語の fgets 関数について解説していきます!

fgets 関数はテキストファイルの読み込みを行う関数です。プログラムからテキストファイルを読み込みたくなることは結構多いので、いろんな場面で活躍してくれる関数だと思います。是非使い方を覚えていってください!

fgets 関数

fgets 関数はテキストファイルから1行分の文字列を取得する関数になります。

fgets 関数のプロトタイプ宣言は下記のようになります。

fgets

#include <stdio.h>

char *
fgets(char * restrict str, int size, FILE * restrict stream);

各引数の意味は下記のようになります(後述で各引数の意味も含めて詳細を解説していきます)。

  • str:取得した文字列の格納先となるバッファの先頭アドレス
  • size:一度に取得する最大文字数 +1
  • stream:読み込み先のストリーム

ストリームと聞くと難しく感じるかもしれませんが、次に紹介する fgets 関数の使用例 の通り、fopen でファイルを開く場合は、fopen の返却値であると考えて良いです(fgets 関数を使用する場合 fopen 時には読み込みモードを指定しておく必要があります)。

ストリームに関しては下記ページでも解説していますので、詳しく知りたい方は下記ページもご参照いただければと思います。

C言語のファイル入出力の解説ページアイキャッチC言語のファイル入出力について解説

fgets 関数の使用例

fgets 関数のイメージが湧きやすいよう、まずは fgets 関数を利用したサンプルを紹介したいと思います。

そのサンプルは下記となります。

fgets関数の使用例

#include <stdio.h>

#define MAX_STR_LEN 10

int main(void) {

    char str[MAX_STR_LEN];

    FILE *stream = fopen("text.txt", "r");
    if (stream == NULL) {
        printf("fopen error\n");
        return 0;
    }

    while (fgets(str, MAX_STR_LEN, stream) != NULL) {
        printf("[%s]\n", str);
    }

    fclose(stream);
    return 0;
}

このソースコードのプログラムでは、text.txt というファイルを fgets 関数によって読み込んで文字列を取得し、その取得した文字列を [ ] で括って表示を行う処理を行なっています。

ファイルの最後まで文字列の取得と表示を行い、ファイルの最後まで表示が完了したら、プログラムを終了するようになっています。

例えば text.txt の中身が下記のようなものであれば、

1234
abcdefghijklmnopq
rstuvwxyz

実行結果としては下記のように表示が行われることになります。[ ]で囲んだ部分が、一度の fgets 関数の実行によって取得できた文字列となります。

[1234
]
[abcdefghi]
[jklmnopq
]
[rstuvwxyz]

ポイントは、text.txt の2行目が2回の fgets 関数の実行に分けて取得されているところだと思います。この辺りの理由も含めて、fgets 関数の詳細な解説を行なっていきたいと思います。

スポンサーリンク

fgets 関数の動作

次は、fgets 関数の動作について、fgets 関数の使用例 で紹介したソースコードを参照しながら解説していきたいと思います。

fgets 関数によって取得できる文字列

fgets 関数は、引数 stream のストリームから文字列を取得する関数です。引数 stream に同じものを指定して再度実行した場合には、前回読み込んだ次の文字から読み込みが開始されることになります。

fgetsを実行するたびに読み込み位置が変化する様子

引数 size が十分大きいものであると考えると、fgets 関数は1行分の文字列を読み込む関数であると考えることができます。

もう少し具体的にいうと、fgets 関数は「読み込みを開始した最初の文字から行の終わり」までを読み込み、読み込んだ文字を str を先頭アドレスとするバッファ(配列など)に格納します。読み込みを開始する位置が行の始まりであれば、1行分読み込むことができます。

そして、読み込んだ文字の後ろ側にはヌル文字('\0')が追加で格納されます。もし改行されている行であれば、改行文字も含んでバッファに格納されます。

読み込んだ文字とヌル文字がバッファに格納される様子

ただし、現実的には fgets 関数によって読み込むことができる “最大の文字数” は size - 1 となります。

MEMO

1文字が1バイトであるとは限らないため、”最大の文字数” ではなく “最大のバイト数” が  size - 1 であるといった方が正しいですが、このページでは1文字1バイトであることを前提とし、以降も文字数という言葉を使って解説していきます

ですので、size - 1 文字読み込んでも行の終わりに到達しない場合、その最初の文字から size - 1 文字のみが読み込まれ、バッファに格納されることになります(この場合も最後にはヌル文字('\0')が格納される)。

最大size-1文字しか取得できない様子

そのため、テキストファイルの行の長さや引数 size の大きさによっては、必ずしも1行分の文字列を読み込むことができるわけではないという点に注意してください。

例えば、fgets 関数の使用例 で紹介したソースコードにおいては引数 sizeMAX_STR_LEN、すなわち 10 としていますので、fgets 関数で読み込まれる最大の文字数は 9 となります。

実行例を示す際に使用した text.txt の内容は下記でしたので、

1234
abcdefghijklmnopq
rstuvwxyz

2行目に関しては 9 文字を超えるため1度に1行分を取得することができず、下記のように2回に分けて fgets 関数から取得されて表示されていました(改行文字も含めて取得されるので、最後に改行が入っています)。

[abcdefghi]
[jklmnopq
]

それに対して1行目と3行目の文字数は 9 以下ですので、fgets 関数で一度に1行分の文字列を取得することができています。

[1234
]
[rstuvwxyz]

もちろん引数 size に指定する値を大きくすれば、2行目も一度に1行分取得することができるようになります。ただ、引数 size に応じてバッファのサイズも大きくする必要がある点に注意してください。これに関しては、次の fgets 関数に必要なバッファのサイズ で説明します。

また、text.txt の最後の行に関しては改行が行われていませんので、上記のように最後の行の文字列取得結果には改行文字が含まれないことになります。

fgets 関数に必要なバッファのサイズ

前述の通り fgets 関数では一度に最大 size - 1 文字が読み込まれ、その読み込んだ文字+ヌル文字、すなわち最大 size 文字が str を先頭アドレスとするバッファに格納される可能性があります。

したがって、 str を先頭アドレスとするバッファのサイズは、最低でも size 文字を格納できるサイズである必要があります。もし、バッファが size 文字を格納できないようなサイズである場合、fgets 関数の中でそのバッファを超えて文字を格納することになり、バッファオーバーランが起こってプログラムに脆弱性が生まれる可能性もあります。

バッファオーバーランが発生する様子

このようなバッファオーバーランが起こらないよう、fgets 関数の第2引数 size には、str を先頭アドレスとするバッファのサイズ(例えば char 型の配列の要素数)を指定することが多いです。

必ず取得した文字がバッファ内に収まる様子

fgets 関数の使用例 におけるバッファは配列 str になります。配列 str の要素数は MAX_STR_LEN なので、最大 MAX_STR_LEN 個の文字を格納することが可能です。

さらに fgets 関数を実行する際に、第1引数に配列 str の先頭アドレスを指定しているため、読み込まれた文字は配列 str に格納されていくことになります。この時、第2引数には配列 str の要素数である MAX_STR_LEN を指定しているため、fgets 関数で一度に読み込まれる文字数は最大 MAX_STR_LEN - 1 となり、読み込んだ文字の後ろにヌル文字を追加したとしても全ての文字を配列 str 内に収めることができます。

スポンサーリンク

fgets 関数の返却値

続いて、fgets 関数の返却値について説明します。

fgets 関数は正常に文字列の取得ができた場合、第1引数で指定した str と同じアドレスを返却します。

それに対し、すでにファイルの最後まで読み込みが終わっている stream に対して fgets 関数が実行された場合や、fgets 関数内でエラーが発生した場合は NULL を返却します。

したがって、まだ stream から fgets 関数で読み込みできるかどうかは、fgets 関数の返却値が NULL であるかどうかで判断することができます。

fgets 関数の使用例 で示したソースコードの下記部分では、上記を利用してファイルの最後まで文字列を取得することを実現しています。 

ファイルの最後まで読み込むループ

while (fgets(str, MAX_STR_LEN, stream) != NULL) {
    printf("[%s]\n", str);
}

ただし、前述の通り、fgets 関数は内部でエラーが発生した場合にも NULL を返却するため、fgets 関数の返却値だけでは、エラーが発生したのか、ファイルの最後まですでに読み込みが完了したのかの判別ができません。

もし、この2つを判別したいのであれば、下記で示す feof 関数や ferror 関数を利用してください。

feof

#include <stdio.h>

int feof(FILE *stream);

ferror

#include <stdio.h>

int ferror(FILE *stream);

両者とも引数 stream の状態をチェックする関数で、feof 関数は stream が終端まですでに到達しているかどうかをチェックし、すでに到達している場合は 0 以外の値を返却します。

ですので、fgets 関数が NULL を返却した後に、fgets 関数の引数に指定した stream と同じものを feof 関数の引数に指定して実行することで、fgets 関数が NULL を返却した理由を知ることができます。具体的には、feof 関数が 0 以外の値を返却した場合、fgets 関数が NULL を返却した理由が「全ての文字列を正常に読み込んだから」であると判断することができます(返却値が 0 の場合は「エラーが発生したから」と判断)。

また ferror 関数は、stream に対する処理でエラーが発生したかどうかをチェックし、エラーが発生している場合は 0 以外を返却します。

ですので、fgets 関数が NULL を返却した後に、fgets 関数の引数に指定した stream と同じものを ferror 関数の引数に指定して実行することで、fgets 関数が NULL を返却した理由を知ることができます。具体的には、ferror 関数が 0 以外の値を返却した場合、fgets 関数が NULL を返却した理由が「エラーが発生したから」であると判断することができます(返却値が 0 の場合は「全ての文字列を正常に読み込んだから」と判断)。

例えば、fgets 関数の使用例 で示したソースコードにおいて、正常にファイルの読み込みが終了した場合は read success!!! を、エラー終了した場合は read failure... を表示するようにしたい場合、feof 関数を利用すれば下記のように記述することができます。

feofを利用したストリームのチェック

#include <stdio.h>

#define MAX_STR_LEN 10

int main(void) {

    char str[MAX_STR_LEN];

    FILE *stream = fopen("text.txt", "r");
    if (stream == NULL) {
        printf("fopen error\n");
        return 0;
    }

    while (fgets(str, MAX_STR_LEN, stream) != NULL) {
        printf("[%s]\n", str);
    }

    if (feof(stream) != 0) {
        printf("read success!!!\n");
    } else {
        printf("read failure...\n");
    }
   
    fclose(stream);
    return 0;
}

また、ferror 関数を利用した場合は下記のように記述することができます。

ferrorを利用したストリームのチェック

#include <stdio.h>

#define MAX_STR_LEN 10

int main(void) {

    char str[MAX_STR_LEN];

    FILE *stream = fopen("text.txt", "r");
    if (stream == NULL) {
        printf("fopen error\n");
        return 0;
    }

    while (fgets(str, MAX_STR_LEN, stream) != NULL) {
        printf("[%s]\n", str);
    }

    if (ferror(stream) == 0) {
        printf("read success!!!\n");
    } else {
        printf("read failure...\n");
    }
   
    fclose(stream);
    return 0;
}

ちなみに、fgets 関数は、引数 stream に書き込みモードで開いたストリームを指定することで、簡単にエラーを発生させることができます。

例えば下記のように fopen 実行部分を変更すれば、最初に fgets 関数が実行された際にエラーを発生させることができます。ただし、モード "w"fopen しているため、実際に実行すると text.txt の中身が空になってしまうので気をつけてください。試す場合は text.txt が空になっても問題ない状態にしてから試してください(text.txt を退避しておくなど)。

fgets関数でエラーを発生させる

FILE *stream = fopen("text.txt", "w");

fgets 関数利用時の注意点

最後に fgets 関数利用時の注意点について記載しておきます。

fgets 関数を利用する場合は読み取りモードで開く

先ほど少し触れましたが、fgets 関数で読み込みを行う場合、当然読み込みモードを指定してファイルを開く(ストリームを開く)必要があるので注意してください。

例えば、下記のように書き込みモードを指定して取得したストリームに対して fgets 関数を実行するとエラーになります。

書き込みモードになっている

FILE *stream = fopen("text.txt", "w");

スポンサーリンク

改行文字が含まれる場合と含まれない場合がある

また、fgets 関数で取得した文字列には改行文字が含まれる場合と含まれない場合があります。例えばファイルの最後の行に改行がない場合、その行の取得結果には改行文字は含まれません。

そのため、全ての行に改行文字が含まれている or 全ての行に改行文字が含まれていないことを前提にして処理を行うようにすると、うまくプログラムが動作しない可能性があるので注意してください。

もし、全ての行を改行なしで統一したい場合は、例えば下記のように改行文字をヌル文字で上書きするような対策が考えられます。

改行文字をヌル文字で上書き

#include <stdio.h>
#include <string.h>

#define MAX_STR_LEN 10

int main(void) {

    char str[MAX_STR_LEN];

    FILE *stream = fopen("text.txt", "r");
    if (stream == NULL) {
        printf("fopen error\n");
        return 0;
    }

    while (fgets(str, MAX_STR_LEN, stream) != NULL) {
        size_t len = strlen(str);
        if (len > 0) {
            if (str[len - 1] == '\n') {
                str[len - 1] = '\0';
            }
        }
        printf("[%s]\n", str);
    }
   
    fclose(stream);
    return 0;
}

まとめ

このページでは、C言語の標準ライブラリ関数である fgets 関数について解説しました!

fgets 関数を利用することで、テキストファイルから1行分の文字列を読み込んで取得することができます。ただし、fgets 関数で読み込める最大文字数は size - 1 のみであり(sizefgets 関数の第2引数として指定)、1行の文字数が size - 1 を超えた場合、一度の fgets 関数の実行だけでは1行分の文字列を取得することができないので注意してください。

size を大きくすれば、一度に読み込める文字数も増えますが、size に応じてあらかじめ用意しておくバッファのサイズも大きくする必要がありますので、この点にも注意が必要です。

C言語といえども、結構テキストファイルを扱いたくなるような場面も多いと思います。そんな時に fgets 関数が活躍することも多いですので、是非使い方を覚えておいてください!

同じカテゴリのページ一覧を表示