アプリケーションでは、様々な外部ファイルを利用します。
ゲームでも、
・テクスチャ
・モデル
・アニメーション
・サウンド
・セーブデータ
…他にも数えきれないほど
保存されているデータをアプリケーションで利用する為には、
まずアプリケーションでファイルを読み込む(ロードする)事が必要です。
ここではC言語におけるアプリケーションでの読み込み方、
そして外部ファイルに保存する方法を紹介します。
ファイルを扱う為に事前に必要な知識
C言語のコーディングでファイル操作を行う為には、
事前に把握しておいた方が良い内容が幾つかあります。
これらを整理した上でコーディングに進みましょう。
ファイル操作の手順
①開く → ②読み取るor書き込む → ③閉じる
どの様なファイルでもこの手順を踏みます。
ファイルの種類
ファイル操作で扱うファイルには二種類あります。
・テキストファイル
・バイナリファイル
テキストファイル
テキストファイルは「文字のみを扱うファイル」です。
正確には「保存されている数値を必ず文字を表す番号として扱うファイル」となります。
単純な構造であるため、人の目で見て把握しやすく、
どのような環境でも利用しやすいです。
ただし「文字を表す番号」には色々種類があります。
・UTF-8
・Shift_JIS
など
これらは文字コードと呼ばれます。
バイナリファイル
バイナリファイルは「コンピュータが扱いやすい形になっているファイル」です。
バイナリファイルは人の目から見てぱっと見でどんなデータか判断が付きません。
2進数の塊で出来ているデータとなります。
単なる数値でしかない為、その数値をどう扱うかはプログラム次第になります。
「41」という数字が入っていても、
それが「文字」なのか「数値」なのかは、
ファイルのデータのみでは判断できません。
FILE構造体
下記の図の様に、ハードディスクからファイルをメモリ側に持ってくる際、
ストリームと呼ばれる部分を経由し、FILE構造体を通じて初めて利用できるようになります。
モード
ファイルの扱い方を指定する為の文字列です。
ファイルを
・読み込むのか
・上書きするのか
・最後尾から追加するのか
等を指定する為に必要になります。
文字列と対応する扱い方は下記の通りです。
文字列 | 内容 | ファイルが既にある場合 | ファイルがまだない場合 |
---|---|---|---|
r | 読み込む | 読み込み成功する | 読み込み失敗する |
w | 書き込む | 内容が全削除される | 新規作成される |
a | 追加で書き込む | 末尾に追加される | 新規作成される |
r+ | 読み込む/書き込む | 読み込み成功する | 読み込み失敗する |
w+ | 読み込む/書き込む | 内容が全削除される | 新規作成される |
a+ | 読み込む/追加で書き込む | 末尾に追加される | 新規作成される |
また、扱いたいファイルの種類によっても文字列が変化します。
・テキストファイル:上記の文字列で問題ありません。
・バイナリファイル:文字「b」を追加し、「rb」「wb」の様に指定します。
EOF
ファイルの終端を表すキーワードです。
ファイル操作を行う一部の関数では、
ファイルの末端まで読み込んだ or 書き込んだ際に、
戻り値でEOFというキーワードを返します。
このキーワードが返されれば、そのファイルはそれ以上操作出来ない
という意味を指す為、ファイル操作の終了させる条件に利用されます。
バイトオーダー
メモリには「どの順番で情報が入るか」というルールがあり、
それをバイトオーダーと呼びます。
「リトルエンディアン」「ビックエンディアン」の二つを覚えておくといいでしょう。
例として「0xAABBCCDD」の数値を格納した4バイト変数があるとします
その数値の1バイト毎のメモリ格納結果は、バイトオーダーによって変化します。
ビッグエンディアンの場合の内容
・・・| 0xAA | 0xBB | 0xCC | 0xDD |・・・
リトルエンディアンの場合の内容
・・・| 0xDD | 0xCC | 0xBB | 0xAA |・・・
バイトオーダーはプログラムを動かすハードウェア(CPU)によって決まります。
環境によってデータの格納のされ方が変わり、
同じファイル読み込んでるのにデータが違う、という事もしばしば起こりますのでご注意ください。
アラインメントとパディング
突然ですが、下記構造体のサイズはいくらでしょうか?
// 構造体(サイズは10?)
struct Str
{
char a;
char* b;
int c;
char d;
};
10と答えたいところですが、実際は10ではありません。
それぞれの変数型には、アラインメントと呼ばれる数値があり、
メモリ確保時には、その倍数のアドレスに配置されるルールがあります。
下記は 32bit環境 での一例です。
型 | サイズ(バイト) | アラインメント |
---|---|---|
char | 1 | 1 |
short | 2 | 2 |
int | 4 | 4 |
ポインタ | 4 | 4 |
各型の場合は上記の様にわかりやすいですが、
構造体の場合は少し特殊です。
構造体が持つ内容の中で一番大きいアラインメント値が構造体自体のアラインメント
になります。
アラインメントを考慮して配置された際に生まれる「空」の領域をパディングと呼びます。
実際は下記の様にパディングが追加され、最終的なサイズは16になります。
// 構造体(32bit環境でパディング込みの場合)
// 64bit環境では結果が変わる
struct Str32
{
char a;
// ---- ポインタ型は4バイトアラインメント、その為直前にパディングが追加される
char padding1[3]; // ← コード上には存在しない
char* b;
// ---- int型 は4バイトアラインメント、元々4バイト境界に位置する為パディングはなし
int c;
// ---- char型 は1バイトアラインメントは、元々1バイト境界に位置する為パディングはなし
char d;
// ---- 構造体の終わりだが、構造体自体のアラインメントは、一番大きい値に合わせて4バイトとなる
// ---- Str32型 の4バイトアラインメント、その為最後のパディングが追加される
char padding2[3]; // ← コード上には存在しない
};
構造体毎にファイルの内容を書き込んでいく際、
構造体のサイズが思ってたのと違う?といった事も出てくるかもしれません。
そんな時は、まずアラインメントやパティングを思い出してもらえればと思います。
では具体的なコードを見ていきましょう。
まずはテキストファイルの書き込み操作から進めます。
書き込み処理の例題1
この例題では
asset/sample.txt に「text write」という文字列を書き込んで保存する
という事を行います。
大まかな手順は下記のとおりです。
- fopen_s 関数で指定したファイルを指定したモードで開き、変数fpで操作できるようにする
- fputs 関数で指定した文字列を、変数fpを利用してファイルに書き込む
- fclose 関数でファイルを閉じる
#include
// エントリー関数
int main(void)
{
// ファイル構造体を扱うポインタ変数宣言
FILE* fp = NULL;
// プロジェクトからの相対パスで asset/sample.txt を書込モードで開く
// asset フォルダが無いと失敗する事に注意する
if(0 == fopen_s(&fp, "asset/sample.txt", "w"))
{
// 文字列を書き込む
fputs("text write", fp);
// 閉じる
fclose(fp);
}
else {
printf("ファイルを開けませんでした");
}
return 0;
}
順にみていきましょう。
①fopen_s 関数で指定したファイルを指定したモードで開き、変数fpで操作できるようにする
下記コードがその個所となります。
// ファイル構造体を扱うポインタ変数宣言
FILE* fp = NULL;
// プロジェクトからの相対パスで asset/sample.txt を書込モードで開く
// asset フォルダが無いと失敗する事に注意する
if(0 == fopen_s(&fp, "asset/sample.txt", "w"))
fopen_s 関数でファイルを開く事に成功した場合、戻り値にゼロが戻ってくる事に注意してください。
サンプルでは失敗した場合の対応も行っています。
②fputs 関数で指定した文字列をファイルに書き込む
下記コードがその個所となります。
// 文字列を書き込む
fputs("text write", fp);
fputs 関数で指定した文字列をファイルに書き込みます。
書き込みの開始位置は、変数fpで制御されており、
書き込みが行われると、書き込んだ分だけ開始位置を進めてくれます。
戻り値は成否(成功ならゼロ以上、失敗ならEOF)となります。
③ fclose 関数でファイルを閉じる
下記コードがその個所となります。
// 閉じる
fclose(fp);
ファイルは使い終わると必ず fclose で閉じるようにしてください。
書き込み処理の例題2
この例題では
asset/sample.txt に任意の型の変数の内容を文字列を書き込んで保存する
という事を行います。
大まかな手順は下記のとおりです。
- fopen_s 関数で指定したファイルを指定したモードで開き、変数fpで操作できるようにする
- fprintf 関数で指定したフォーマットで各変数の内容をファイルに書き込む
- fclose 関数でファイルを閉じる
#include
// エントリー関数
int main(void)
{
FILE* fp = NULL;
// プロジェクトからの相対パスで asset/sample.txt を書込モードで開く
if (0 == fopen_s(&fp, "asset/sample_write.txt", "w"))
{
// 文字を入力する
// ※スキャン集合を利用して改行だけ読み取らない、つまり、スキャンを終えるのは改行が来た時だけ
char buffer[100] = {};
scanf_s("%[^\n]", buffer, 100);
// 数字を入力する
int no = 0;
scanf_s("%d", &no);
// 数字を入力する
float value = 0;
scanf_s("%f", &value);
// フォーマットを指定して書き込む
fprintf(fp, "%s, %d, %f", buffer, no, value);
// 閉じる
fclose(fp);
}
else
{
printf("ファイルを開けませんでした");
}
return 0;
}
①と③は同じである為割愛します。
② fprintf 関数で指定したフォーマットで各変数の内容をファイルに書き込む
下記コードがその個所となります。
// フォーマットを指定して書き込む
fprintf(fp, "%s, %d, %f", buffer, no, value);
それぞれ型の違う変数を、フォーマットを指定してファイルに書き込んでいきます。
書き込みの開始位置は、変数fpで制御されており、
書き込みが行われると、書き込んだ分だけ開始位置を進めてくれます。
戻り値には書き込んだ文字数が返ってきます。
次にテキストファイルの読み込み操作を見ていきます。
読み込み処理の例題1
この例題では
asset/sample.txt の内容を一行ずつ読み込んで文字列に詰め、詰め込んだ内容を表示する
という事を行います。
sample.txt
abcdef
大まかな手順は下記のとおりです。
- fopen_s 関数で指定したファイルを指定したモードで開き、変数fpで操作できるようにする
- fgets 関数で内容を一行ずつ読み取っていき、buffer に詰めてコンソールに表示する
- fclose 関数でファイルを閉じる
#include
// エントリー関数
int main(void)
{
// ファイル構造体を扱うポインタ変数宣言
FILE* fp = NULL;
// プロジェクトからの相対パスで asset/sample.txt を読込モードで開く
// 開くことに失敗した場合は、エラーを表示する
if (0 == fopen_s(&fp, "asset/sample.txt", "r"))
{
// 読込先の文字配列
char buffer[256] = {};
// fgets で一行毎に文字を配列に詰め込む
while (NULL != fgets(buffer, 256, fp))
{
// 詰め込んだ内容を表示する
printf("%s", buffer);
}
// 閉じる
fclose(fp);
}
else
{
printf("ファイルがありません\n");
}
return 0;
}
順にみていきましょう。
①fopen_s 関数で指定したファイルを指定したモードで開き、変数fpで操作できるようにする
下記コードがその個所となります。
// ファイル構造体を扱うポインタ変数宣言
FILE* fp = NULL;
// プロジェクトからの相対パスで asset/sample.txt を読込モードで開く
// 開くことに失敗した場合は、エラーを表示する
if (0 == fopen_s(&fp, "asset/sample.txt", "r"))
読み込みなので、モードは「r」を指定しています。
サンプルでは失敗した場合の対応も行っています。
② fgets 関数で内容を一行ずつ読み取っていき、buffer に詰めてコンソールに表示する
下記コードがその個所となります。
// 読込先の文字配列
char buffer[256] = {};
// fgets で一行毎に文字を配列に詰め込む
while (NULL != fgets(buffer, 256, fp))
{
// 詰め込んだ内容を表示する
printf("%s", buffer);
}
読み取り開始の現在位置はfpが持ち、fgets 関数が成功すると、
読み取った分だけ、変数fpが持つ読み取り開始の位置を進めてくれます。
その為、戻り値が NULL (EOFに到達 or 読み取りに失敗)になるまで、
while で読み取りを繰り返すといった事が可能になります。
③ fclose 関数でファイルを閉じる
下記コードがその個所となります。
// 閉じる
fclose(fp);
ファイルは使い終わると必ず fclose で閉じるようにしてください。
読み込み処理の例題2
この例題では
asset/sample.txt の内容を指定のフォーマットに合わせて読み取り、変数に詰め込んだ内容を表示する
という事を行います。
sample.txt
1 aaaa 1.7 3 bbb 3.4
大まかな手順は下記のとおりです。
- fopen_s 関数で指定したファイルを指定したモードで読み込み、変数fpで操作できるようにする
- fscanf_s 関数で指定したフォーマットで内容を読み取り、各変数に詰めてコンソールに表示する
- fclose 関数でファイルを閉じる
#include
// エントリー関数
int main(void)
{
FILE* fp = NULL;
// プロジェクトからの相対パスで asset/sample.txt を読込モードで開く
// 失敗した場合はエラー表示
if (0 == fopen_s(&fp, "asset/sample.txt", "r"))
{
// 読込先の整数変数
int no = 0;
// 読込先の文字配列
char string[256] = {};
// 読込先の浮動小数変数
float value = 0;
// fscanf_s でフォーマットを指定して読み取る
while (EOF != fscanf_s(fp, "%d %s %f ", &no, string, 256, &value))
{
// 読み込んだ内容を表示する
printf("%d,%s,%1.2f\n", no, string, value);
}
// 閉じる
fclose(fp);
}
else
{
printf("ファイルがありません");
}
return 0;
}
①と③は同じである為割愛します。
②fscanf_s 関数で指定したフォーマットで内容を読み取り、各変数に詰めてコンソールに表示する
下記コードがその個所となります。
// 読込先の整数変数
int no = 0;
// 読込先の文字配列
char string[256] = {};
// 読込先の浮動小数変数
float value = 0;
// fscanf_s でフォーマットを指定して読み取る
while (EOF != fscanf_s(fp, "%d %s %f ", &no, string, 256, &value))
{
// 読み込んだ内容を表示する
printf("%d,%s,%1.2f\n", no, string, value);
}
fscanf_s 関数で注目するべきは第二引数以降です。
第二引数で、読み取るテキストのフォーマットをそれぞれ指定します。
第三引数以降で、フォーマットに対応した型の変数のアドレスを指定します。
文字列として変数に受け取る際は、受け取る文字列の最大数も指定する必要があります。
どのフォーマットがどんな構成でテキストファイルに入っているのかを、
正しく把握しておかなければならない点に注意してください。
読み取った分だけ、fpが持つ読み取り開始の位置を進めてくれます。
戻り値はファイルの終端であれば EOF それ以外は読み取った個数になります。
続いてバイナリファイルの書き込み操作を見ていきます。
書き込み処理の例題
この例題では
asset/data.bin に10個の整数値を書き込んでいく
という事を行います。
大まかな手順は下記のとおりです。
- fopen_s 関数で指定したファイルを指定したモードで開き、変数fpで操作できるようにする
- fwrite 関数で指定した整数値を指定した個数分書き込む
- fclose 関数でファイルを閉じる
#include
// エントリー関数
int main(void)
{
// ファイル構造体を扱うポインタ変数宣言
FILE* fp = NULL;
// バイナリデータを書き込む為、 モードに「b」を付け加える
if (0 == fopen_s(&fp, "asset/data.bin", "wb"))
{
// int 型の整数値を10個書き込む
int data[10] = { 1,2,3,4,5,6,7,8,9,10 };
fwrite(data, sizeof(int), 10, fp);
fclose(fp);
}
else
{
printf("ファイルを開けませんでした");
}
return 0;
}
順にみていきましょう。
①fopen_s 関数で指定したファイルを指定したモードで開き、変数fpで操作できるようにする
下記コードがその個所となります。
// ファイル構造体を扱うポインタ変数宣言
FILE* fp = NULL;
// バイナリデータを書き込む為、 モードに「b」を付け加える
if (0 == fopen_s(&fp, "asset/data.bin", "wb"))
バイナリデータを書き込む為、モードが「wb」になっている事に注目してください。
②fwrite 関数で指定した整数値を指定した個数分書き込む
下記コードがその個所となります。
// int 型の整数値を10個書き込む
int data[10] = { 1,2,3,4,5,6,7,8,9,10 };
fwrite(data, sizeof(int), 10, fp);
fwrite 関数は書き込みたいデータの単体のデータサイズと個数を指定してファイルに書き込みます。
書き込みの開始位置は、変数fpで制御されており、
書き込みが行われると、書き込んだ分だけ開始位置を進めてくれます。
戻り値は書き込んだデータの個数になっています。
③ fclose 関数でファイルを閉じる
下記コードがその個所となります。
fclose(fp);
ファイルは使い終わると必ず fclose で閉じるようにしてください。
最後にバイナリファイルの読み込み操作を見ていきます。
読み込み処理の例題
この例題では
asset/data.bin を開き10個の整数値を読み込んでいく
という事を行います。
大まかな手順は下記のとおりです。
- fopen_s 関数で指定したファイルを指定したモードで開き、変数fpで操作できるようにする
- fread 関数で指定した型のサイズを指定した個数分読み込んで、コンソールに表示する
- fclose 関数でファイルを閉じる
#include
// エントリー関数
int main(void)
{
// ファイル構造体を扱うポインタ変数宣言
FILE* fp = NULL;
// バイナリデータを読み込む為、 モードに「b」を付け加える
if (0 == fopen_s(&fp, "asset/data.bin", "rb"))
{
// int 型の整数値を10個読み込む
int data[10] = { };
fread(data, sizeof(int), 10, fp);
// 読み込んだ内容をコンソールに表示する
for (int i = 0; i < 10; ++i)
{
printf("%d ", data[i]);
}
fclose(fp);
}
else
{
printf("ファイルがありません");
}
return 0;
}
順にみていきましょう。
①fopen_s 関数で指定したファイルを指定したモードで開き、変数fpで操作できるようにする
下記コードがその個所となります。
// ファイル構造体を扱うポインタ変数宣言
FILE* fp = NULL;
// バイナリデータを読み込む為、 モードに「b」を付け加える
if (0 == fopen_s(&fp, "asset/data.bin", "rb"))
バイナリデータを読み込む為、モードが「rb」になっている事に注目してください。
②fread 関数で指定した型のサイズを指定した個数分読み込んで、コンソールに表示する
下記コードがその個所となります。
// int 型の整数値を10個読み込む
int data[10] = { };
fread(data, sizeof(int), 10, fp);
// 読み込んだ内容をコンソールに表示する
for (int i = 0; i < 10; ++i)
{
printf("%d ", data[i]);
}
fread 関数で読み込みたいデータの単体のデータサイズと個数を指定して、格納先の変数に設定していきます。
読み込みの開始位置は、変数fpで制御されており、
読み取りが行われると、読み取った分だけ開始位置を進めてくれます。
バイナリデータである為、読み込まれたデータが何を意味するのかは、
そのバイナリデータを作成した(書き込んだ)人にしか理解できない内容になっています。
テキストファイルのフォーマット指定読み込みと同じく、
データフォーマット(「何バイト目から何バイト分が何の情報を指すのか」等)を
理解して利用しなければならない事に注意してください。
戻り値は読み込んだデータの個数となっています。
③ fclose 関数でファイルを閉じる
下記コードがその個所となります。
fclose(fp);
ファイルは使い終わると必ず fclose で閉じるようにしてください。