[Linux][C言語]ファイル入出力をやってみる

Linux環境のC言語でファイルの入出力を試してみました。

本記事では、ファイルの開き方、読み込み方、書き込み方、閉じ方を一通り説明します。

# 変更履歴

* 2021 / 06 / 25 タイトルを修正しました。参考文献のリンクを修正しました。
* 2021 / 06 / 21 概要を追加しました。セクション名を一部変更しました。誤字を修正しました。


# 概要

* ファイルを開くには `fopen` 関数を使います。
* ファイルを1バイトずつ読み込むには `fgetc` 関数を使います。
* ファイルに1バイトずつ書き込むには `fputc` 関数を使います。
* ファイルを閉じるには `fclose` 関数を使います。
* 標準ライブラリでの入出力で `FILE` 構造体はファイルを表す中心的な存在です。
* 戻り値のみでエラーか判断できない場合は `ferror` 関数を使いエラーが起きたのかを判断します。


# 動機

ファイルに関する操作を理解することでLinuxへの理解を深められるのではないかと思い調査と記事を書きました。

<details>Linuxではファイルが重要な役割を持っていて、データだけではなく、デバイスやネットワーク越しのリソースなどをファイルとして抽象化します。つまり、ファイルの操作を理解することはLinuxへの理解にも繋がると思います。思えば大学生のときにLinuxの授業で初めてやったのもファイル入出力でした。（ちなみに、その時の記憶はほとんど忘れてます :joy: ）</details>


# 前提知識

下記に挙げた内容に関して詳細な説明は省略します。

- C言語の基本的な知識（条件分岐やポインタなど）
- C言語で書かれたプログラムをコンパイルし実行できる


# 動作環境

この記事を書くにあたり使用した環境は以下の通りです。

- コンパイラ：gcc（バージョン9.3.0）
- OS：Ubuntu（バージョン20.04.2 LTS）

# 作成したプログラム

最初にファイル入出力の全体像を掴んでもらうためにサンプルプログラムを載せます。詳細な説明はこのあとに説明します。このサンプルプログラムは引数で与えられたファイルを開き、最初の行を読み込み、 `output.txt` というファイル名で読み込んだ行を書き込み、最後にファイルを閉じます。なお、このプログラムでは簡略化のため最大で２５６バイト読み込むようにしています。

``` c:finout.c
#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);
}
```

このプログラムで使用した標準ライブラリの関数は次の４つです。

- `fopen`： ファイルを開く
- `fclose`： ファイルを閉じる
- `fgetc`： ファイルから１バイト読み込む
- `fputc`： ファイルに１バイト書き込む

以降で順にこれらの関数について説明します。


# ファイルを開く・閉じる

ファイルを開くには `fopen` を、閉じるには `fclose` を使います。それぞれの定義は次のとおりです。

``` c
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` を呼び出すといいでしょう。

上記のプログラムでは読み込むファイル、書き込むファイルを開くときにこれらの関数を使用しました。該当する箇所は以下です。

``` c:読み込むファイルを開く・閉じる
  // 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);
  }
```

``` c:書き込むファイルを開く
  // 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` を使ってエラー内容を表示させています。

関数の詳細については次のページで確認できます。

https://linuxjm.osdn.jp/html/LDP_man-pages/man3/fopen.3.html

https://linuxjm.osdn.jp/html/LDP_man-pages/man3/fclose.3.html

# 読み込み・書き込み

ファイルを開くとファイルの読み込み・書き込みができるようになります。 `fgetc` は読み込みができ、 `fputc` は書き込みができます。どちらも１バイトづつ処理します。それぞれの関数の定義は次のとおりです。

``` c
int fgetc(FILE *stream);

int fputc(int c, FILE *stream);
```

それぞれ引数で `FILE` 構造体を受け取るようになっていることがわかります。 `fopen` で開いたファイルを引数で指定できるということです。

また、それぞれ戻り値が `int` 型になっています。値は読み込み、書き込みした文字になります。なぜ `char` 型ではないのかというとエラーが起きた際に `EOF` が返されるからです。 `EOF` は `-1` の `int` 型の値なので、戻り値が `int` 型になっているというわけです。

書き込みを行う `fputc` 関数は、第一引数で書き込む文字を `int` 型で受け取るようになっています。こちらは関数内部で `unsigned char` にキャストされ、書き込まれます。

上記のプログラムではこれらの関数を次の箇所で使用しています。

``` c:読み込み
  // 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);
  }
```

``` c:書き込み
  // 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` を使って判断するのが基本のようです。

関数の詳細については次のページで確認できます。

https://linuxjm.osdn.jp/html/LDP_man-pages/man3/fgetc.3.html

https://linuxjm.osdn.jp/html/LDP_man-pages/man3/puts.3.html


# 参考文献

1. 書籍：　ふつうのLinuxプログラミング第２版　青木 峰郎著
1. Webサイト：　苦しんで覚えるC言語　（https://9cguide.appspot.com/ ）
1. Webサイト：　JM Project - Linux関連のマニュアルページの日本語版　（http://linuxjm.osdn.jp/ ）


[^1]: ファイルディスクリプタについては[こちら](https://e-words.jp/w/%E3%83%95%E3%82%A1%E3%82%A4%E3%83%AB%E3%83%87%E3%82%A3%E3%82%B9%E3%82%AF%E3%83%AA%E3%83%97%E3%82%BF.html)に説明があります。