C++編【言語解説】 第6章 ファイルストリームの基礎

この章の概要

この章の概要です。

ファイルストリーム

前章で標準入出力ストリームを、第4章では文字列ストリームに触れました。 この章では、ファイルの入出力を行うファイルストリームを取り上げます。

C言語では fopen関数(⇒リファレンス)でファイルを開き、 何種類かある読み書き用の関数を使い、 最後に fclose関数(⇒リファレンス)で閉じるという処理を行いました。
この一連の処理について、C言語編では、第39章以降の数章を割いて解説しています。 C++編でも、できるだけC言語編と近い例を使って解説していきます。

出力ファイルストリーム

ファイルへデータを書き出す場合には、ofstream を使用します。 ofstream を使用するには、fstream というヘッダをインクルードする必要があります。

まず試しに、"Hello, World" という文字列をファイルへ書き出してみます。

#include <iostream>
#include <fstream>
#include <cstdlib>

int main()
{
	std::ofstream ofs("hello.txt");
	if (!ofs) {
		std::cerr << "ファイルオープンに失敗" << std::endl;
		std::exit(1);
	}

	ofs << "Hello, World" << std::endl;
}

実行結果:


実行結果(hello.txt):

Hello, World

ofstream型の変数を定義する際にファイルの名前を指定します。 すると、この名前のファイルがオープンされます(無ければ作成されます)。 fopen関数で言うところの、

foepn("hello.txt", "w");

に当たりますが、fopen関数の第2引数のように出力用であることを明示する必要がありません。 なぜなら、ofstream である時点で、出力用であることが決まっているからです(頭の o が output のことで、次の f が file のことです)。

実際には、ofstream の定義時に2つ目の引数を与えることが可能で、そこに fopen関数の第2引数のようなオープンモードを指定できます。 しかし多くの場合、省略した場合のデフォルトの挙動で十分です。 詳細は、【標準ライブラリ】第28章を参照して下さい。

ファイルオープンが成功したかどうかは、ofstream型の変数を直接 if文で調べれば分かります。 抜き出すと、以下の部分です。

if (!ofs) {
}

ファイルオープンのタイミングだけでなく、ストリームの操作中に何らかのエラーが起きていないかどうかを、 !演算子で問い合わせることができます。 エラーが起きていたら、この結果が真になります。
逆に、!演算子を使わずに問い合わせることで、エラーが起きていないことを確認できます。

if (ofs) {
	// エラーは起きていない
}

ファイルへ文字列を書き出す方法は、標準入出力ストリームや文字列ストリームと同じで、<<演算子が使えます。 この辺りの統一感は素晴らしいですね。

最後に、ファイルクローズしている箇所が見当たりませんが、 ofstream型の変数の生存期間が終われば、自動的にクローズされます。 つまり、変数ofs は main関数のローカル変数として宣言されているので、main関数を抜け出すときに生存期間を終えて、 自動的にファイルクローズが行われます。
このような仕組みになっているため、ファイルクローズを忘れることは、ほとんどあり得ません。 この仕組みは、C++ では非常に一般的であると同時に、非常に重要な考え方になっています。

なお、何らかの事情で明示的にファイルをクローズしたければ、close関数を使用します。

ofs.close();

C言語に慣れていると、open と close が対応付いていないことに違和感を感じるかも知れませんが、 C++ の世界では、自動的に行われるのならば、余計な手を出さずに任せてしまうべきです。

入力ファイルストリーム

ファイルからデータを読み取る場合には、ifstream を使用します。 ifstream を使用するには、fstream というヘッダをインクルードする必要があります。

先ほどの「出力ファイルストリーム」のところで書き出したファイルを読み込んでみましょう。

#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib>

int main()
{
	std::ifstream ifs("hello.txt");
	if (!ifs) {
		std::cerr << "ファイルオープンに失敗" << std::endl;
		std::exit(1);
	}

	std::string buf;
	ifs >> buf;

	std::cout << buf << std::endl;
}

実行結果:

Hello,

ifstream の使い方は、ofstream とほとんど変わりありません。 変数定義の際にファイル名を指定すれば、その名前でファイルがオープンできます。 ifstream は入力用に開くので、fopen関数の第2引数に "r" を指定した場合と同じで、 指定したファイルが存在しなければエラーになります。

実際の読み込みの際には、>>演算子が使えます。 これは、標準入力ストリーム(cin) や文字列ストリーム(istringstream) と同じです。
しかし、今回のサンプルの場合、実行結果にあるように、想定した「Hello, World」ではなく、 「Hello,」となってしまいます。

>>演算子による読み込みは、空白文字が現れるまでを1かたまりとして扱います。 ですから "Hello, World" を読み取ろうとすると、1度目に "Hello," までが得られ、2度目で "World" を得ることになります。

この挙動は std::noskipwsマニピュレータによって変更でき、これを使うと、空白文字も読み取るようになります。

今回のようなテキストファイルの読み込みの場合、1行を1つの単位として読み込むのが自然で簡単です。 この目的のために、getline関数が使えます。

#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib>

int main()
{
	std::ifstream ifs("hello.txt");
	if (!ifs) {
		std::cerr << "ファイルオープンに失敗" << std::endl;
		std::exit(1);
	}

	std::string buf;
	getline(ifs, buf);

	std::cout << buf << std::endl;
}

実行結果:

Hello, World

getline関数は、第1引数に入力ストリームを指定し、第2引数に受取り用の std::string型変数を指定します。 ファイルストリームに限定されている訳ではないので、標準入力や文字列の入力ストリームにも使用できます。
受け取った1行分の文字列が、第2引数に指定した std::string に格納されます。 第4章でも取り上げたように、std::string は自動的に必要なメモリを割り当てるので、バッファオーバーフローは起きません。
また、getline関数は改行文字を行の区切りとみなしますが、読み取りはしません。 C言語の fgets関数(⇒リファレンス)は改行文字まで受け取ってしまうので、 挙動が違っていますが、普通、getline関数の動作の方が自然だと言えます。

getline関数は、改行文字を区切りとみなしますが、これはデフォルトの挙動であり、 別の区切り文字を指定することが可能です。その場合、第3引数に char型で文字を指定します。

ところで、getline関数は正確には std::getline なのですが、std:: を付けなくてもエラーになりません。 高度な内容になるので詳細はコラムに譲りますが、std:: を付けたとしても問題はありません。

これは Koenig Lookup というルールによって、自動的に、関係する名前空間を参照してくれるからです。 引数に std名前空間内の型が指定されているため、std名前空間が「関係する名前空間」であるとみなされます。

また、ifs.getline(buf); のように書きたくなるかも知れません。 実は、ifs.getline() は存在しますが、この場合、引数の型は char*型になってしまい、std::string型を受け付けてくれません。 char型の配列を扱うよりも、std::string型を使う方が安全なので、 よほど高い効率を求めるのでなければ、std::getline() の方を使った方が良いと言えます。

入出力両用のファイルストリーム

入出力の両用に使えるファイルストリームが必要ならば、fstream を使用します。 fstream を使用するには、(同名ですが)fstream というヘッダをインクルードする必要があります。

ifstream や ofstream の合わせ技のようなものなので、使い方も同じと考えて良いです。

また、fopen関数の場合、第2引数に "r+" を指定した場合と "w+" を指定した場合とで違いがありました。 C言語編第40章で取り上げていますが、引用すると次のようになります。

ファイルが存在しない場合、fstream ではエラーになります。 また、ファイルが存在する場合に、中身は残されます。 従って、"r+" の挙動になっています。

バイナリモード

バイナリファイルを扱いたいときは、ofstream、ifstream、fstream の変数定義時に、 第2引数に std::ios_base::binary という指定を与えます。

std::ofstream ofs("test.bin", std::ios_base::out | std::ios_base::binary);
std::ifstream ofs("test.bin", std::ios_base::in | std::ios_base::binary);
std::fstream ofs("test.bin", std::ios_base::in | std::ios_base::out | std::ios_base::binary);

std::ios_base::in や std::ios_base::out も登場していますが、これらはデフォルトで指定されているものです。 デフォルトの定義をそのまま活かすため、|演算子で結合して渡しています。

実際に、バイナリデータの入出力を試してみましょう。

#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib>

int main()
{
	std::ofstream ofs("test.bin", std::ios_base::out | std::ios_base::binary);
	if (!ofs) {
		std::cerr << "ファイルオープンに失敗" << std::endl;
		std::exit(1);
	}

	int num = 900;
	double d = 7.85;
	std::string str("xyzxyz");
	std::string::size_type strLen = str.size();

	ofs.write((const char*)&num, sizeof(num));
	ofs.write((const char*)&d, sizeof(d));
	ofs.write((const char*)&strLen, sizeof(strLen));
	ofs.write(str.c_str(), strLen);
	ofs.close();


	std::ifstream ifs("test.bin", std::ios_base::in | std::ios_base::binary);
	if (!ifs) {
		std::cerr << "ファイルオープンに失敗" << std::endl;
		std::exit(1);
	}


	char* s = (char*)std::malloc(strLen + 1);
	ifs.read((char*)&num, sizeof(num));
	ifs.read((char*)&d, sizeof(d));
	ifs.read((char*)&strLen, sizeof(strLen));
	ifs.read(s, strLen);
	s[strLen] = '\0';

	std::cout << num << "\n"
	          << d << "\n"
	          << s << std::endl;

	std::free(s);
}

実行結果:

900
7.85
xyzxyz

ちょっと複雑ですが、ofstream を使ってバイナリデータを書き出し、 書き出されたファイルを ifstream で読み取って、標準出力に出力することで確認しています。

バイナリデータを書き出すには、write関数を使います。 この関数の第1引数が書き出すデータのアドレスで、第2引数がサイズになっています。
第1引数の型は、const char*型なので、すごく鬱陶しいのですが、他の型を書き出すにはキャストを必要とします。

ここではC言語のキャスト構文を使っていますが、C++ では static_cast を使うようにしましょう(第7章

一方、バイナリデータを読み込む際には、read関数を使います。 こちらは、第1引数が受け取り先のアドレス、第2引数がサイズになります。
第1引数の型は、char*型になるので、やはり他の型を読み込む際にはキャストが必要です。

さて、このサンプルでは文字列を std::string型で扱っています。 write関数に渡す際には、c_str関数を使って const char*型を受け取ることで対応できますが、 read関数に渡す際には、「const」が邪魔になります。
この場合、char型の配列で読み取らなければいけません。 すると今度は、この配列の大きさがどれだけあればいいのか分からないという問題に行き当ってしまいます。 今回は、バイナリデータを書き出す際に、文字列の長さもデータに含めておくことで解決しています。
バイナリデータ内に可変長のデータがあると、このような面倒な処理が必要になってきます。

ファイルの終わりの検出

仮に、テキストファイルの中身を1文字ずつ読み込んでいき、 それらを標準出力へ書き出していくようなプログラムを書きたいとします。 そのためには、ファイルの終端を検出できないといけません。

どのタイプのストリームであっても、 ファイルの終端に達したかどうかは eof関数によって調べられます。

次のようなテキストファイル (test.txt) があるとします。

Hello, World
Hello, C++

このファイルの内容を標準出力へ書き出してみます。

#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib>

int main()
{
	std::ifstream ifs("test.txt");
	if (!ifs) {
		std::cerr << "ファイルオープンに失敗" << std::endl;
		std::exit(1);
	}

	while (!ifs.eof()) {
		char c;
		ifs.get(c);
		std::cout << c;
	}
	std::cout << std::flush;
}

実行結果:

Hello, World
Hello, C++

eof関数は、ファイルの終端に達していたら真になります。 また、get関数は1文字を読み込んで、引数に渡した変数に格納します。

実行結果を見ると、最後に余計な改行が入ってしまっています。 これは、eof関数を呼び出すタイミングが悪いため、whileループ内の処理が1回多く回ってしまっているためです。
C言語の feof関数(⇒リファレンス)でもそうですが、 入出力の関数を呼び出した「後で」、終端判定をしないといけません。 (C言語編第40章参照)。
これを踏まえて、while文のところを書き直してみます。

#include <iostream>
#include <fstream>
#include <string>
#include <cstdlib>

int main()
{
	std::ifstream ifs("test.txt");
	if (!ifs) {
		std::cerr << "ファイルオープンに失敗" << std::endl;
		std::exit(1);
	}

	while (1) {
		char c;
		ifs.get(c);
		if (ifs.eof()) {
			break;
		}
		std::cout << c;
	}
	std::cout << std::flush;
}

実行結果:

Hello, World
Hello, C++

正しい結果を得られました。 しかし、これはちょっと不恰好です。 実は次のように書くことができます。

char c;
while (ifs.get(c)) {
	std::cout << c;
}

get関数の戻り値は、ストリーム自身です。 従って、while(ifs) {} と書いたような状態になるので、これはエラーチェックで使った if (!ifs) {} のような形になります。 終端に達した場合も、if (!ifs) は真になるので、これでも動作します。
ただし、この方法の場合、本当にエラーが起きていた場合との区別が付きません。 区別を付ける必要があるのなら、eof関数を使うしかありません。

正確には、get関数の戻り値は、ストリーム自身の参照になります。


練習問題

問題@ 標準入力から受け取った内容を、テキストファイルへ書き出すプログラムを作成して下さい。細かい仕様はお任せします。

問題A 既存のテキストファイルのコピーを作り出すプログラムを作成して下さい。


解答ページはこちら

参考リンク

更新履歴

'2013/12/23 新規作成。



前の章へ

次の章へ

C++編のトップページへ

Programming Place Plus のトップページへ