今回と次回の記事では標準関数についてお話していきます。今回の内容は標準関数の中でもわかりにくいであろうファイルの読み書きについて説明していきます。
前回の復習
前回の記事では自作ヘッダーファイルのお話からファイル分割について勉強しました。ヘッダーファイルの中にかけるものを把握できていれば大丈夫です。
fopen関数とfclose関数
最初はfopen関数とfclose関数についてです。
関数の名前から察していただけると思いますがファイルを読み込む(open)関数と閉じる(close)関数になります。実際に使い方を見ていきましょう。
#include <stdio.h>
int main() {
FILE *f;
f = fopen("test.txt", "w");
fclose(f);
return 0;
}
実行結果
4行目でFILE型のポインタ変数fを作成し、5行目でfopen関数の呼び出しから戻り値を代入しています。これによりポインタ変数fを用いることでファイル操作が可能となります。具体的な処理方法については後で説明するので一旦おいておきましょう。
6行目ではポインタ変数fを引数にしてfclose関数を呼び出しています。ファイルの読み書きを行う際にはfopenとセットでfcloseを使うことになります。ファイルを閉じ忘れないように注意して下さいね。
fopen関数についての処理をもう少し詳しく見ていきましょう。fopenの引数を見ると『”test.txt”』と『”w”』があるのがわかります。それぞれどんな引数なのかを説明しておきます。まず1つ目の引数の『”test.txt”』については見てもらえばわかるのですがテキスト形式のファイル名になっています。今回は同じフォルダ内にある『”test.txt”』というファイルに対して読み書きの処理をするためにfopenしています。2つ目の引数の『”w”』についてはファイルのモード(オープンモードともいう)といって1つ目の引数で指定したファイルに対して読み込み専用で開くのか、書き込み専用で開くのかなどを指定するためのモードになります。『”w”』はwrite, 『”r”』はread, 『”a”』はappendの頭文字と覚えておくと良いでしょう。『”b”』とか『”+”』が後ろにつくことがありますがそれぞれバイナリと両方といった感じのニュアンスでしょう。
モードについては一つ一つ説明すると長くなるのでpointをまとめるときに表にしておきます。
今回のソースコードは『”w”』のモード(書き込み)でfopenされているためfopenの1つ目の引数で指定したテキストファイル(“test.txt”)が書き込み専用で読み込まれていることになります。この時に指定したファイル名に一致するファイルが存在しない場合は自動的に空のファイルが生成されます。(『”r”』モード(読み込み)でfopenする場合はエラーになります。ご注意ください!)
コンパイルしてサンプルコード(tc1601.c)を実行した後でフォルダの中をのぞいてみてください。『”test.txt”』という名前のファイルが生成されているはずです。
実はfopen関数を実行する際にまれにエラーが発生することがあります。このエラーは文法的な誤りではないためコンパイル時に検出することができず、実行して初めて現れる非常にタチの悪いエラーです。
これに対して何もしないわけにはいきませんよね。そこでC言語には例外処理(エラーハンドリング)を書くことになっています。例外処理と言うものについては第18回で詳しく述べるので今は置いておきますが一言だけ述べておくとエラーが発生した際に行う処理のことを指します。
ファイルモードについては以下の表を参照。
ファイルモード | 説明 |
---|---|
r | 読み込み専用 |
w | 書き込み専用 |
a | 書き込み専用(追記していく) |
r+ | 読み込みに加えて書き込みもできる |
w+ | 書き込みに加えて読み込みもできる |
a+ | 書き込み(追記)に加えて読み込みもできる |
rb | バイナリファイルの読み込み専用 |
wb | バイナリファイルの書き込み専用 |
ab | バイナリファイルの書き込み専用(追記していく) |
rb+ | バイナリファイルの読み込みに加えて書き込みもできる |
wb+ | バイナリファイルの書き込みに加えて読み込みもできる |
ab+ | バイナリファイルの書き込み(追記)に加えて読み込みもできる |
fopen関数でファイルを開いたら必ずfclose関数でファイルを閉じる必要がある。引数にはFILE型のポインタを渡す。
fgetc関数とfputc関数
ファイルを開くfopenと閉じるfcloseについて勉強したところで次はファイルへの書き込みとファイルからの読み込みについて学んでいきます。まずは一番簡単なfgetc関数とfputc関数の2つを紹介します。
『fgetc』と『fputc』についている『c』はchar型を示していて、それぞれの関数が1文字の読み書きを行うことを示しています。試しに先ほどのサンプルコードで生成されたtest.txtに対してファイルの読み書きを試してみましょう。
#include <stdio.h>
int main() {
FILE *fp;
fp = fopen("test.txt", "w"); //書き込みモード
fputc('h', fp);
fputc('i', fp);
fclose(fp);
return 0;
}
実行結果
hi
tc1602.cはfputc関数を用いてファイルに書き込みを行っているサンプルコードになります。今回は6, 7行目で2回fputc関数を使っており、それぞれ『h』と『i』という文字をテキストファイル『test.txt』に書き込んでいます。実際にフォルダの中にある『test.txt』を開いて確認してみて下さい。
tc1602.cを実行したら次はtc1603.cでfgetc関数を試してみて下さい。
#include <stdio.h>
int main() {
FILE *fp;
fp = fopen("test.txt", "r"); //読み込みモード
char ch = fgetc(fp);
fclose(fp);
printf("%c\n", ch);
return 0;
}
実行結果
h
このような結果になったと思います(『test.txt』のファイルの中身を書き換えた人やtc1602.cのfputc時に渡した文字が違う人は結果が違ってきます)。
この結果からわかるようにfgetc関数というのはファイルから1文字読むための関数になります。fgetc関数を使う際にはひとつ注意点があります。それはファイルの中に文字が書かれていない場合やファイルの文字数を超えてfgetc関数を使うとエラーが発生します。これを防ぐために以下のように『EOF』を用いたプログラムを書くことが多いです(EOFとはEnd Of Fileのことで、ファイルの終端を意味します)。
#include <stdio.h>
int main() {
FILE *fp;
fp = fopen("test.txt", "r"); //読み込みモード
int alpha[26];
for(int i = 0; i < 26; i++) {
alpha[i] = 0;
}
char ch;
while((ch = fgetc(fp)) != EOF) {
if(ch < 'a' || 'z' < ch) continue;
alpha[ch - 'a']++;
}
fclose(fp);
for(int i = 0; i < 26; i++) {
printf("|%c : %d回 |", (char)('a' + i), alpha[i]);
}
printf("\n");
return 0;
}
実行結果
|a : 0回 ||b : 0回 ||c : 0回 ||d : 0回 ||e : 0回 ||f : 0回 ||g : 0回 ||h : 1回 ||i : 1回 ||j : 0回 ||k : 0回 ||l : 0回 ||m : 0回 ||n : 0回 ||o : 0回 ||p : 0回 ||q : 0回 ||r : 0回 ||s : 0回 ||t : 0回 ||u : 0回 ||v : 0回 ||w : 0回 ||x : 0回 ||y : 0回 ||z : 0回 |
11行目のようにファイルの終端を検出するまでwhile文で回して使うことでこの問題は解決できます。実際に動かして確かめてみてください。
一方でファイルに1文字書き込む際にはfputc関数を用いる。fputc関数は引数にchar型の値とFILE型のポインタ変数の2つを渡す必要がある。
fread関数とfwrite関数について
少しC言語に詳しい人だとファイルの操作でfread関数とfwrite関数を使ったことがあるかもしれません。しかしこれらについてはあまり使わない方が良いと個人的に考えています。と言うのもfread関数やfwrite関数はfgetc関数やfputs関数に比べてわかりづらいためです。例えば以下のソースコードを見てください。
#include <stdio.h>
int main() {
FILE *fp;
int x[] = {2, 3, 5, 7, 11, 13};
int y[sizeof(x) / sizeof(int)];
char name[100];
printf("バイナリファイルの名前を拡張子つきで入力して下さい : "); //sample.datとか適当に
scanf("%s", name);
/*ファイルに書き込み*/
fp = fopen(name, "wb");
fwrite(x, sizeof(int), sizeof(x) / sizeof(int), fp);
fclose(fp);
/*ファイルから読み込み*/
fp = fopen(name, "rb");
fread(y, sizeof(int), sizeof(x) / sizeof(int), fp);
fclose(fp);
for(int i = 0; i < sizeof(x) / sizeof(int); i++) {
printf("%d ", y[i]);
}
printf("\n");
return 0;
}
fwrite関数とfread関数を使ったコードを書いてみました。先ほどまで勉強してきたfgetc関数と比べて引数が多く一見しただけではどの値が何を表しているのかがわかりにくいですね。一般的にコードを書く際にはわかりやすいコードを書くことが良いとされています(可読性というやつですね)。なぜ読みやすいコードが良いのかというと、結果的に同じ処理を行うプログラムがあった時にわかりやすく書かれていた方がメンテナンスを行いやすく作業量が減るからです。他にも管理の引き継ぎで送られてきたコードが解読パズルのような絶望コードだったら嫌になりますよね。
それらの事情からfread関数とfwrite関数についてはあえて使う必要がないため個人的には非推奨としているわけです(ちゃんと理解しないで使うとバグるのもありますが(^ ^))。
freadとfwriteを使う場合はバイト数と要素の個数を混同しないよう十分気をつけて下さいね。不安な人はfgetc関数やfputc関数とこの後登場するfgets関数とfputs関数を使うようにしましょう!(バイト数とか面倒なことを考えなくても済むのでおすすめです)
fgets関数とfputs関数
fgets関数とfputs関数について説明をしていきます!
とは言っても、fgetc関数とfputc関数が理解できていればその文字列バージョンと考えるだけなので新しく何かを覚える必要は全くありません。『fgets』と『fputs』についている『s』はstring(char*型)を示していて、それぞれの関数が文字列の読み書きを行うことを示しています。
文字列と言っても1行ごと読み書きするだけなのでそんなに難しくはないと思います。サンプルコードを載せておくので実験してみて下さい。
#include <stdio.h>
int main() {
FILE *fp;
fp = fopen("test.txt", "w");
char ch[] = "Hello";
fputs(ch, fp);
fclose(fp);
return 0;
}
実行結果
Hello
#include <stdio.h>
int main() {
FILE *fp;
fp = fopen("test.txt", "r");
char ch[100];
fgets(ch, 100, fp);
fclose(fp);
printf("%s\n", ch);
return 0;
}
実行結果
Hello
fgets関数については少し引数に説明が必要ですね。fgetsの3つの引数を順番に説明します。1つ目はfgets関数が文字列を取得する関数のためreturnで戻すことができず仕方なく付け足された引数になります。文字列ということでchar型の配列を入れておけば大丈夫です。2つ目は最大文字数を指定する引数です。文字列の長さが長すぎてchar型配列に収まりきらずに溢れてエラーが発生しないように文字列長の限界を指定しています。3つ目はfgetc関数の時と同じ引数(FILE型のポインタ変数)ですね。
char型とchar*型を扱う際に生じた差なので当然と言えば当たり前の部分ですが初学者は混乱しやすい場所だと感じたので説明しておきました。
一方でファイルに1行分書き込む際にはfputs関数を用いる。fputs関数は引数にchar*型の値とFILE型のポインタ変数の2つを渡す必要がある。
まとめ
今回の記事は標準関数の中でも厄介なファイル操作に関するものを取り扱いました。次回はよく使う関数をサクサク紹介していくことにします(数が多く記事が長くなりそうなので一つ一つ丁寧に扱いません)。
最後まで記事を見ていただきありがとうございます。また別の記事でお会いできることを祈っております。