Linux環境のC言語でファイルの入出力を試してみました。
本記事では、ファイルの開き方、読み込み方、書き込み方、閉じ方を一通り説明します。
変更履歴
- 2021 / 06 / 25 タイトルを修正しました。参考文献のリンクを修正しました。
- 2021 / 06 / 21 概要を追加しました。セクション名を一部変更しました。誤字を修正しました。
概要
- ファイルを開くには
fopen
関数を使います。 - ファイルを1バイトずつ読み込むには
fgetc
関数を使います。 - ファイルに1バイトずつ書き込むには
fputc
関数を使います。 - ファイルを閉じるには
fclose
関数を使います。 - 標準ライブラリでの入出力で
FILE
構造体はファイルを表す中心的な存在です。 - 戻り値のみでエラーか判断できない場合は
ferror
関数を使いエラーが起きたのかを判断します。
動機
ファイルに関する操作を理解することでLinuxへの理解を深められるのではないかと思い調査と記事を書きました。

前提知識
下記に挙げた内容に関して詳細な説明は省略します。
- C言語の基本的な知識(条件分岐やポインタなど)
- C言語で書かれたプログラムをコンパイルし実行できる
動作環境
この記事を書くにあたり使用した環境は以下の通りです。
- コンパイラ:gcc(バージョン9.3.0)
- OS:Ubuntu(バージョン20.04.2 LTS)
作成したプログラム
最初にファイル入出力の全体像を掴んでもらうためにサンプルプログラムを載せます。詳細な説明はこのあとに説明します。このサンプルプログラムは引数で与えられたファイルを開き、最初の行を読み込み、 output.txt
というファイル名で読み込んだ行を書き込み、最後にファイルを閉じます。なお、このプログラムでは簡略化のため最大で256バイト読み込むようにしています。
# include <stdio.h>
# include <stdlib.h>
# define BUFFER_SIZE 256
static char buffer[BUFFER_SIZE];
static int buffer_index = 0;
int main(int argc, char *argv[]) {
// 1. ファイルを開く。
FILE *fr = fopen(argv[1], "r");
// NULLが返却された場合、ファイルを開くことに失敗しているので、
// プログラムを異常終了する。
if (!fr) {
perror(argv[0]);
exit(EXIT_FAILURE);
}
// 2. ファイルの先頭一行を読み込む。
int c;
while ((c = fgetc(fr)) != EOF) {
if (buffer_index >= BUFFER_SIZE) break;
if (c == '\n') break;
buffer[buffer_index] = c;
buffer_index++;
}
// 3. ファイルを閉じる。
// EOFが返った場合ファイルを閉じることに失敗しているので、
// プログラムを異常終了する。
if (fclose(fr) == EOF) {
perror(argv[0]);
exit(EXIT_FAILURE);
}
// 読み込みに失敗した場合、プログラムを異常終了する。
if (ferror(fr)) {
perror(argv[0]);
exit(EXIT_FAILURE);
}
// 4. 書き込み用のファイルを開く。
FILE *fw = fopen("output.txt", "w");
if (!fw) {
perror(argv[0]);
exit(EXIT_FAILURE);
}
// 5. 書き込む
for (int j = 0; j < buffer_index; j++) {
if (fputc(buffer[j], fw) == EOF) {
// 書き込みに失敗した場合、プログラムを異常終了する。
perror(argv[0]);
exit(EXIT_FAILURE);
}
}
if (fclose(fw) == EOF) {
perror(argv[0]);
exit(EXIT_FAILURE);
}
exit(EXIT_SUCCESS);
}
このプログラムで使用した標準ライブラリの関数は次の4つです。
-
fopen
: ファイルを開く -
fclose
: ファイルを閉じる -
fgetc
: ファイルから1バイト読み込む -
fputc
: ファイルに1バイト書き込む
以降で順にこれらの関数について説明します。
ファイルを開く・閉じる
ファイルを開くには fopen
を、閉じるには fclose
を使います。それぞれの定義は次のとおりです。
FILE *fopen(const char *pathname, const char *mode);
int fclose(FILE *stream);
どちらの関数も FILE
という型を使っています。 FILE
はファイルを表す構造体です。この構造体の中にはファイルディスクリプタ1と言われる、ファイルを表す番号が含まれています。プログラムを書く側から隠蔽されていますが、標準ライブラリでは内部でファイルディスクリプタを使ってカーネルとやり取りをしていることになるでしょう。ただ、標準ライブラリを使って入出力をする際にはこのあたりを意識せずに使用できます。
fopen
では pathname
で開くファイルのパスを指定し、 mode
でモードを指定します。モードとはファイルをどういった用途で使用するかを示すもので、関数の挙動に影響します。よく使われるであろうモードは次のとおりです。
モード | 意味 |
---|---|
r | 読み込み専用で開く。読み込み開始位置は先頭にセットされる。 |
w | 書き込み専用で開く。ファイルが存在する場合は中身を空にした上で書き込む。ファイルが存在しない場合は新たに作成する。書き込み開始位置は先頭にセットされる。 |
a | 追加専用で開く。ファイルが存在しない場合は新たに作成する。書き込み開始位置は最後にセットされる。 |
ここで、位置という言葉が出てきました。位置とは、例えるなら、テキストエディタのカーソルのようなものです。このあと説明する fgetc
や fputc
などの関数は、この位置を起点としてファイルの読み書きを行います。ファイルはそれぞれ内部で位置を状態として持っており、位置を変更することでファイルの読み込みや書き込みの場所を変更できます。 (ここでは詳しく解説しませんが、 fseek
関数で位置を明示的に指定できます。)
fclose
では stream
で指定したファイルを閉じます。基本的には fopen
で開いた FILE
を引数に渡すことになると思います。ベストプラクティスとして、よく使い終わったファイルはなるべく早く閉じろ、というものがあります。これはファイルというリソースはさまざまなプロセス間で共有されるものなので、一つのプログラムがいつまでも専有するべきではないからです。使い終わったら早めに fclose
を呼び出すといいでしょう。
上記のプログラムでは読み込むファイル、書き込むファイルを開くときにこれらの関数を使用しました。該当する箇所は以下です。
// 1. ファイルを開く。
FILE *fr = fopen(argv[1], "r");
// NULLが返却された場合、ファイルを開くことに失敗しているので、
// プログラムを異常終了する。
if (!fr) {
perror(argv[0]);
exit(EXIT_FAILURE);
}
// ~ 中略 ~
// 3. ファイルを閉じる。
// EOFが返った場合ファイルを閉じることに失敗しているので、
// プログラムを異常終了する。
if (fclose(fr) == EOF) {
perror(argv[0]);
exit(EXIT_FAILURE);
}
// 4. 書き込み用のファイルを開く。
FILE *fw = fopen("output.txt", "w");
if (!fw) {
perror(argv[0]);
exit(EXIT_FAILURE);
}
// ~ 中略 ~
if (fclose(fw) == EOF) {
perror(argv[0]);
exit(EXIT_FAILURE);
}
上記のプログラムでは戻り値を確認して、処理がうまく行かない場合プログラムを異常終了させています。 fopen
はエラーが起きると NULL
を返します。 fclose
はエラーが起きると EOF
を返します。それぞれエラーが起きた際には perror
を使ってエラー内容を表示させています。
関数の詳細については次のページで確認できます。
読み込み・書き込み
ファイルを開くとファイルの読み込み・書き込みができるようになります。 fgetc
は読み込みができ、 fputc
は書き込みができます。どちらも1バイトづつ処理します。それぞれの関数の定義は次のとおりです。
int fgetc(FILE *stream);
int fputc(int c, FILE *stream);
それぞれ引数で FILE
構造体を受け取るようになっていることがわかります。 fopen
で開いたファイルを引数で指定できるということです。
また、それぞれ戻り値が int
型になっています。値は読み込み、書き込みした文字になります。なぜ char
型ではないのかというとエラーが起きた際に EOF
が返されるからです。 EOF
は -1
の int
型の値なので、戻り値が int
型になっているというわけです。
書き込みを行う fputc
関数は、第一引数で書き込む文字を int
型で受け取るようになっています。こちらは関数内部で unsigned char
にキャストされ、書き込まれます。
上記のプログラムではこれらの関数を次の箇所で使用しています。
// 2. ファイルの先頭一行を読み込む。
int c;
while ((c = fgetc(fr)) != EOF) {
if (buffer_index >= BUFFER_SIZE) break;
if (c == '\n') break;
buffer[buffer_index] = c;
buffer_index++;
}
// 〜中略〜
// 読み込みに失敗した場合、プログラムを異常終了する。
if (ferror(fr)) {
perror(argv[0]);
exit(EXIT_FAILURE);
}
// 5. 書き込む
for (int j = 0; j < buffer_index; j++) {
if (fputc(buffer[j], fw) == EOF) {
// 書き込みに失敗した場合、プログラムを異常終了する。
perror(argv[0]);
exit(EXIT_FAILURE);
}
}
それぞれ戻り値を確認し、エラーが起きていればプログラムを異常終了します。 fputc
はエラーが発生すると EOF
を返します。 fgetc
はファイルの最後に到達した場合時 EOF
を返しますが、エラーが発生したときも EOF
を返します。これでは戻り値を見ただけで判断できないので、 ferror
を使用して FILE
にエラーが発生したか確認しています。このように、戻り値だけではファイルの最後に達したのか、エラーが発生したのか、判断できない関数は他にもあり、その場合は ferror
を使って判断するのが基本のようです。
関数の詳細については次のページで確認できます。
参考文献
- 書籍: ふつうのLinuxプログラミング第2版 青木 峰郎著
- Webサイト: 苦しんで覚えるC言語 (https://9cguide.appspot.com/ )
- Webサイト: JM Project - Linux関連のマニュアルページの日本語版 (http://linuxjm.osdn.jp/ )