Lesson 10
ファイル処理
Lesson 10
Chapter 1
ファイル処理
本Lessonでは、ファイルを読み書きする方法について学習します。ファイルは大きく分けて「テキストファイル」と「バイナリファイル」の2種類があります。C言語の「ファイル処理」を学習する上で必要な知識になりますので、最初にファイルの種類について解説します。
テキストファイル
「テキストファイル」とは、文字を並べたファイルのことです。テキストエディタなどの、テキストファイルに対応したソフトウェアで開くと、文字の並びを画面に表示したり、編集したりできます。テキストファイルの中には、文字を表す文字コードの値や、特定の文字エンコーディングで文字を表現した値が並んでいます。
バイナリファイル
テキストファイルとは異なり、文字を並べたわけではないファイル、文字の並びとしては解釈できないファイルのことを「バイナリファイル」と呼びます。例えば、画像や音声のファイルは、一般にバイナリファイルです。また、C言語のコンパイラが生成するオブジェクトファイルや実行ファイルも、バイナリファイルです。バイナリファイルには色々な形式があり、それぞれの形式に対応したソフトウェアで扱えます。
FILE型
FILE型は、ファイルを管理するための情報をまとめた構造体に、typedef宣言を使ってFILEという別名を付けたものです。つまりファイルポインタ(FILE*型のポインタ)は、構造体を指すポインタです。ファイルポインタは次のように宣言します。ポインタ名のつけ方は、通常のポインタ名や変数名と同じです。
ファイルポインタの宣言
FILE* ファイルポインタ名;
ファイルを操作する
ファイルを開く
ファイルを開くには、次のようにfopen関数を呼び出します。fopen関数の戻り値は、ファイルポインタの初期化に使うか、宣言済みのファイルポインタに代入します。なお、ファイルが開けなかったときの処理については後述します。
ファイルを開く(ファイルポインタの初期化)
FILE* ファイルポインタ名 = fopen(ファイル名, モード);
ファイルを開く(ファイルポインタに代入)
ファイルポインタ名 = fopen(ファイル名, モード);
ファイル名の部分には"pointer5.c"のようなファイル名の文字列を書きます。"Chapter2/pointer"のようなパスを書くこともできます。
モードに関しては次のような文字列で指定します。入力・出力・追加の3種類があります。ファイル名で指定したファイルが存在するかどうかによって、動作が変わります。
モード | 意味 | ファイルが存在する場合 | ファイルが存在しない場合 |
---|---|---|---|
r | read(入力) | ファイルを先頭から読み込み | エラー |
w | write(出力) | 既存ファイルを破棄して上書き | 新規作成 |
a | sppend(追加) | 既存ファイルの末尾に追加書き込み | 新規作成 |
r+ | read(入力) | 既存ファイルを読み書き | エラー |
w+ | write(出力) | 既存ファイルを破棄して読み書き | 新規作成 |
a+ | sppend(追加) | 既存ファイルの末尾に追加読み書き | 新規作成 |
上記のモードには、次のようなバリエーションがあります。特に必要がない場合には、上記の「r」「w」「a」をそのまま使えば大丈夫です。「r+」「w+」「a+」のように「+」を付けると、開いたファイルに対して読み書きの両方を実行します。ファイル有無による動作の違いは「+」を付けない場合の「r」「w」「a」と同様です。
ファイルを閉じる
ファイルを閉じるには、次のようにfclose関数を呼び出します。引数はファイルポインタです。
ファイルを閉じる
fclose(ファイルポインタ);
ファイルの文字列を読み込む
ファイルを読み書きするための関数は数多くありますが、代表的な関数を紹介します。fgets (エフゲットエス)関数を使って、ファイルから文字列を読み込んでみましょう。次のようにファイルポインタを指定すると、ファイルから文字列を読み込むことができます。
ファイルから文字を読み込む
fgets(配列名, 要素数, ファイルポインタ);
fgets関数は改行を区切りとして、ファイルから1行の文字列を読み込み、改行「\n」も含めて文字配列に格納します。格納した文字列の末尾には、自動的にヌル文字「\0」をつけます。もしファイルの1行が長くて、引数で渡された配列に格納できない場合は、この配列に格納できる範囲で、1行を途中まで読み込みます。残りの部分は、再びfgets関数を呼び出せば読み込めます。ファイルの文字コードがASCIIの場合、配列に格納できる最大の文字数(ナル文字を除く)は「配列の要素数-1」です。
fgets関数を使ってみましょう。簡単なプログラムから始めます。テキストファイルから最初の1行だけを読み込んで画面に出力するプログラムを書いてください。fopen関数でファイルを開き、fgets関数でファイルから1行を読み込んで、画面に出力し、fclose関数でファイルを閉じます。
file1.c
#include <stdio.h>
int main(void) {
// ファイルを読み込み
FILE* fp = fopen("pointer5.c", "r");
// 読み込んだ文字列を格納する配列
char str[100];
// ファイルから1行の文字列を読み込む
fgets(str, sizeof(str), fp);
// 読み込んだ文字列を表示
printf("%s", str);
// ファイルを閉じる
fclose(fp);
return 0;
}
実行結果
file1.exe
#include <stdio.h>
ファイルから1行だけを読み込むことができました。次は繰り返しを使って、ファイルの末尾まで読み込んでみましょう。ファイルを1行ずつ末尾まで読み込むには、fgets関数の戻り値を使います。ファイルの末尾に達すると、fgets関数はNULL (ヌル)を返します。そこで、fgets関数の戻り値を調べて、NULLでない限り読み込みを繰り返せば、ファイルの末尾まで読み込めます。前回のプログラムを改造して、テキストファイルの末尾まで1行ずつ読み込んで画面に出力するプログラムを書いてください。while文やfor文による繰り返しを使います。
file2.c
#include <stdio.h>
int main(void) {
// ファイルを読み込み
FILE* fp = fopen("pointer5.c", "r");
// 読み込んだ文字列を格納する配列
char str[1000];
// ファイルから1行の文字列を読み込んで表示
while(fgets(str, sizeof(str), fp)) printf("%s", str);
// ファイルを閉じる
fclose(fp);
return 0;
}
実行結果
file2.exe
#include <stdio.h>
int main(void) {
int a = 1;
puts("変数aの整数の値");
printf("%d\n", a);
puts("変数aのアドレスの値");
printf("%p\n", &a);
return 0;
}
上記のプログラムでは、while文の条件式にfgets関数の呼び出しを書きました。NULLの値は0なので、fgets関数の戻り値がNULL以外ならば繰り返しを続け、NULLならば繰り返しを終了します。「fgets(…) !=NULL」のように書いても構いません。
ファイルに文字列を書き込む
fgets関数と対となる機能を持つfputs(エフプットエス)関数があります。getは入力、putは出力を意味します。次のようにfputs関数を呼び出すと、文字配列の内容(文字列)をファイルに出力します。puts関数は自動的に改行を出力しますが、fputs関数は改行を出力せずに、文字列だけを出力します。
ファイルに文字列を書き込む
fputs(配列名, ファイルポインタ);
fgets関数とfputs関数を使って、ファイルをコピーするプログラムを害いてみましょう。前回のプログラムを改造し、テキストファイルを1行ずつ読み込んで、別のファイルに1行ずつ書き込むプログラムを書いてください。ここでは、読み込むファイルを入力ファイル、書き込む先のファイルを出力ファイルと呼ぶことにします。今回のプログラムでは、入力ファイルと出力ファイルを同時に開いて、入力ファイルから出力ファイルに1行ずつコピーします。同時に開いた複数のファイルを区別するには、ファイルポインタを使います。例えば、ファイルxとファイルyを同時に開くと、ファイルポインタxとファイルポインタyが得られます。ファイルを読み書きしたり、ファイルを閉じたりする際には、ファイルポインタを指定することによって、どちらのファイルを操作するのかを切り替えます。ファイルポインタxを指定すればファイルxを、ファイルポインタyを指定すればファイルyを操作できます。
file3.c
#include <stdio.h>
int main(void) {
// 入力ファイルを読み込み
FILE* inputfp = fopen("pointer5.c", "r");
// 出力ファイルを読み込み
FILE* outputfp = fopen("output1.c", "w");
// 読み込んだ文字列を格納する配列
char str[1000];
// ファイルから1行の文字列を読み込んで書き込み
while(fgets(str, sizeof(str), inputfp)) fputs("%s", outputfp);
// ファイルを閉じる
fclose(inputfp);
fclose(outputfp);
return 0;
}
上記のプログラムでは、入力ファイルのファイルポインタ(inputfp)と、出力ファイルのファイルポインタ(outputfp)を使い、複数のファイルを同時に開いて操作します。出力したファイルをコマンドプロンプト上に表示してえみましょう。コマンドは「type output.c」のように「type ファイル名」を実行します。
実行結果file3.exe
>gcc -o file3 file3.c --exec-charset=cp932
>file3
>type output1.c
#include <stdio.h>
int main(void) {
int a = 1;
puts("変数aの整数の値");
printf("%d\n", a);
puts("変数aのアドレスの値");
printf("%p\n", &a);
return 0;
}
ファイルの文字を1文字ずつ読み書きする
fgets関数とfputs関数は1行ずつ読み書きするのに対して、
fgetc関数とfputc関数は1文字ずつ読み書きを行います。
ファイルの末尾に到達すると、fgetc関数はEOF (End Of File、ファイルの末尾)という値を返します。fgetc関数の戻り値がEOFかどうかを調べれば、ファイルの末尾に到達したかどうかが分かります。
ファイルから1文字読み込む
fgetc(ファイルポインタ);
ファイルに1文字書き込む
fputc(ファイルポインタ);
fgetc関数と文字コード、文字エンコーディングの関係
fgetc関数はファイルから1バイトを読み込みます。そのため、ASCIIのように1文字が1バイトの文字コードを使ったファイルについては、fgetc関数は1文字を読み込むことになります。一方、UTF-8のように1文字が1バイトではない文字エンコーディングを使ったファイルについては、fgetc関数が1文字を読み込むとは限らない(1文字を構成する複数バイトの1バイト分だけを読み込むことがある)ので、注意してください。
fgetc関数とfputc関数を使ってみましょう。テキストファイルを1文字ずつ読み込んで、別のファイルに1文字ずつ書き込むプログラムを書いてください。1行単位でファイルをコピーする「file3.c」とコードが似ているため、「file3.c」を修正してコードを書くとよいでしょう。
file4.c
#include <stdio.h>
int main(void) {
// 入力ファイルを読み込み
FILE* inputfp = fopen("pointer1.c", "r");
// 出力ファイルを読み込み
FILE* outputfp = fopen("output2.c", "w");
// 読み込んだ文字列を格納する配列
char str[1000];
// ファイルから1文字の文字列を読み込んで書き込み
for (int c; (c = fgetc(inputfp)) != EOF; fputc(c, outputfp));
// ファイルを閉じる
fclose(inputfp);
fclose(outputfp);
return 0;
}
実行結果
file3.exe
>gcc -o file4 file4.c --exec-charset=cp932
>file4
>type output2.c
#include <stdio.h>
int main(void) {
int a = 1;
puts("変数aのアドレスの値");
printf("%p\n", &a);
a = 8;
puts("変数aのアドレスの値");
printf("%p\n", &a);
return 0;
}

Lesson 10
Chapter 2
テキストとバイナリ
バイナリファイルを操作する
バイナリファイルを1バイトずつ読み書きするには、fopen関数のモードにbをつけてファイルを開いた上で、fgetc関数やfputc関数を使います。fopen関数のモードが異なるだけで、fgetc関数やfputc関数の使い方はテキストファイルの場合と同じです。fgetc関数の戻り値がEOFかどうかを調べれば、ファイルの末尾を検出できることも同様です。
ファイルをバイナリファイル用のモードで読み込むプログラムを書いてみましょう。ファイルをバイナリファイル用のモードで開き、1バイトずつ読み込み、16進数で出力するプログラムを書いてください。使用するバイナリファイルは、コンパイル後のexeファイルで構いません。
file5.c
#include <stdio.h>
int main(void) {
// 入力ファイルを読み込み
FILE* fp = fopen("binary.exe", "rb");
int c;
// ファイルから1文字の文字列を読み込んで書き込み
for (int i = 1; (c = fgetc(fp)) != EOF; i++) {
printf("%02x%c", c, i%16?' ':'\n');
}
puts("");
// ファイルを閉じる
fclose(fp);
return 0;
}
実行結果
file5.exe
50 4b 03 04 14 00 06 00 08 00 00 00 21 00 a4 53
c5 cf 4e 01 00 00 08 04 00 00 13 00 00 00 5b 43
6f 6e 74 65 6e 74 5f 54 79 70 65 73 5d 2e 78 6d
6c ac 93 cb 4e c3 30 10 45 f7 48 fc 43 e4 2d 8a
………
6c 50 4b 05 06 00 00 00 00 0a 00 0a 00 7c 02 00
00 05 17 00 00 00 00
1バイトずつ16進数で出力しています。結果が見やすくなるように、1バイトを2桁で出力し、16バイトごとに改行しました。fopen関数のモードを変えるだけで、テキストファイルを1文字ずつ入出力するのと同じ方法で、バイナリファイルを1バイトずつ入出力できます。次は複数のバイトをまとめて入出力する方法を学習しましょう。
複数バイトをまとめて読み書きする
大きなファイルを扱うときや、配列を格納したファイルを扱うときには、複数のバイトをまとめて読み書きできると便利です。こういった場合は、次のようなfread (エフリード)関数とfwrite (エフライト)関数を使います。
ファイルから複数バイトを読み込む
fread(配列名, 要素のバイト数, 要素数, ファイルポインタ)
ファイルに複数バイトを書き込む
fwrite(配列名, 要素のバイト数, 要素数, ファイルポインタ)
fread関数とfwrite関数は、いずれも配列を使います。fread関数はファイルから読み込んだ値を配列に格納し、逆にfwrite関数は配列に格納された値をファイルに書き込みます。引数には配列名のほか、配列の要素1個あたりのバイト数(要素のサイズ)や、読み書きを行う要素数を指定します。fread関数を使う際には、次のような注意点があります。
・要素数
読み込んだ値が配列の範囲を越えないように、配列の要素数以下の要素数を指定します。指定した要素数を読み込む前に、ファイルの末尾に到達した場合には、fread関数は読み込めた要素だけを配列に格納します。
・戻り値
fread関数の戻り値は、実際に読み込んだ要素数です。ファイルの末尾に到達した場合は、戻り値が0になるので、末尾かどうかが分かります。
fread関数とfwrite関数を使って、ファイルをコピーしてみましょう。ファイルをバイナリファイル用のモードで開き、末尾まで100バイトずつ読み込んで、別のファイルに100バイトずつ書き込むプログラムを害いてください。以前に書いた、1文字単位でファイルをコピーするプログラムに似ているので、このプログラムを改造するとよいでしょう。
fread関数とfwrite関数を使うには、作業用の配列が必要です。今回は100バイトずつ読み書きするために、char型で要素数が100の配列を宣言します。char型は1バイトと決められているので(Chapter9)、この配列のサイズは100バイトです。ファイルをコピーするには、fread関数とfwrite関数を繰り返し呼び出します。fread関数の戻り値が0より大きい限り繰り返しを続ければ、ファイルの末尾までコピーできます。
file6.c
#include <stdio.h>
int main(void) {
// 入力ファイルを読み込み
FILE* inputfp = fopen("pointer3.c", "rb");
// 出力ファイルを読み込み
FILE* outputfp = fopen("output4.c", "wb");
// 読み込んだ文字列を格納する配列
char str[100];
//読み込んだ要素数
int n;
// ファイルから1文字の文字列を読み込んで書き込み
while ((n = fread(c, 1, sizeof c, ifp)) > 0) fwrite(c, 1, n, ofp);
// ファイルを閉じる
fclose(inputfp);
fclose(outputfp);
return 0;
}
上記のプログラムでは、while文を使ってfread関数とfwrite関数を繰り返し呼び出します。前述のようにchar型は1バイトと決められているので、要素のバイト数には1を指定しました。1の代わりに「sizeof (char)」を指定しても構いません。一方で要素数については、fread関数には配列の要素数(sizeof c)を指定し、fWrite関数には実際に読み込んだ要素数(n)を指定しました。fread関数の戻り値が0より大きい(要素が読み込めている)間、繰り返しを続けます。
実行結果file6.exe
>gcc -o file6 file6.c --exec-charset=cp932
>file6
>type output6.c
#include <stdio.h>
int main(void) {
puts("int型のポインタ変数ptrAの宣言");
int* ptrA;
puts("long型の宣言");
long a;
puts("int型のポインタ変数にlong型のアドレスを格納");
ptrA = &a;
return 0;
}
