ファイルストリーム
ファイル読み書きの新しい方法
C言語ではファイルの読み書きはfopen
関数(fopen_s
関数)でファイルを開き、読み書き用の関数を呼び出し、最後にfclose
関数でファイルを閉じる、という流れで行っていました。
もちろんC++でもこの方法でファイル読み書きが可能です。
C++では、新しいファイルの読み書き方法が提供されています。
ファイルの扱い方に関してはC言語と共通する部分も多いので、もし分からない場合は先にC言語の方のファイル処理に目を通しておくことをおすすめします。
出力ストリーム
ファイルの書き込みにはstd::ofstream
クラスを使用します。
これを使用するには#include <fstream>
が必要です。
#include <iostream>
#include <fstream>
int main()
{
const char* fileName = "C:\\test.txt";
//ファイルを開く
std::ofstream ofs(fileName);
if (!ofs)
{
std::cout << "ファイルが開けませんでした。" << std::endl;
std::cin.get();
return 0;
}
//ファイルに書き込み
ofs << "あいうえお かきくけこ" << std::endl;
std::cout << fileName << "に書き込みました。" << std::endl;
//ファイルを閉じる
ofs.close();
std::cin.get();
}
C:¥test.txtに書き込みました。
このコードを実行すると「C:\test.txt」というファイルが新しく作成されます。
(存在していた場合は上書き)
ファイルを開いて中身を確認してみてください。
アクセス権限の関係でCドライブ直下にファイルが書き込めない場合はパスを適当に変更してください。
パスの指定は相対パスでも可能です。
その場合、実行ファイルの場所が起点となります。
(Visual Studioのプロジェクトフォルダ内)
このファイルの書き込み方法は、std::cout
によるコンソール画面への文字出力とほぼ同じであることに気づいたと思います。
実際のところ、出力先がコンソール画面からファイルに変わっただけです。
ofstream
は出力ファイルストリームの機能を提供するクラスです。
(「o:アウトプット」の「f:ファイル」「stream:ストリーム」)
fopen
関数ではファイル構造体を通してファイルを読み書きしていましたが、ofstream
ではクラスのインスタンスを生成し、これを通してファイルに書き込みます。
ofstream
のコンストラクタにはファイルのパス文字列(ファイルの場所)を指定します。
これはC言語形式の文字列(char型の配列)でもstringクラスでも構いません。
fopen
関数は引数に読み取りモードか書き込みモードかを指定していましたが、ofstream
は出力専用で、ファイルは書き込みモードで開かれます。
open関数
ofstream
のコンストラクタを呼び出す以外にも、インスタンスからopen
関数を実行することでもファイルを開くことができます。
std::ofstream ofs;
ofs.open(fileName);
ofs << "abc" << std::endl;
使い方はコンストラクタの場合と同じです。
オープン成否のチェック
ofstream
のインスタンスを生成したら、ファイルのオープンに成功したかどうかをチェックします。
これはインスタンスをif文などの条件式にセットすることで判別できます。
成功していれば真となり、そのままif文が実行され、失敗していれば偽となりif文は実行されません。
!
演算子(論理否定演算子)で結果を反転させることもできます。
if (ofs)
{
//オープン成功
}
if (!ofs)
{
//オープン失敗
}
ファイルのクローズ
ファイルのクローズはofstream
のclose
メンバ関数で行います。
また、ofstream
クラスのデストラクタではファイルのクローズ処理が行われるため、インスタンスの寿命が尽きると自動的にクローズされます。
fopen
/fclose
関数に比べてファイルの閉じ忘れがなく安全です。
ファイルオープンモード
fopen
関数と同様に、ofstream
にもファイルのオープンモードが存在します。
以下の値の組み合わせをコンストラクタ(またはopen
関数)の第二引数に指定します。
- std::ios::out
- 書き込みモード
(ofstreamのデフォルト)
書き込み時に、以前の内容は破棄される - std::ios::in
- 読み取りモード
(ifstreamのデフォルト) - std::ios::app
- 追記モード
常にファイルの末尾に書き込み
(append) - std::ios::ate
- オープンと同時にファイル末尾に移動
オープン後に任意の位置に移動可能
std::ios::inと同時に指定する(後述)
(at end) - std::ios::trunc
- 上書きモード
ファイルを開いた時点で以前の内容が破棄される
(truncate) - std::ios::binary
- バイナリモード
これらはstd::ios
名前空間に存在します。
ファイルの読み書き動作はfopen関数も参考にしてください。
ofstream
は書き込み専用であるため、どのようなモードを指定しても書き込みが可能です。
app
とate
は似ていますが異なります。
app
はファイル位置を末尾より手前に移動させても書き込みは常に末尾に行われます。
ate
はファイル位置を移動させた位置に書き込みが可能です。
ate
は単独指定では期待通りの動作にならないのでin
と組み合わせて使用します。
これについては後述します。
ファイル位置変更(シーク)
ファイルの現在の位置の変更(シーク)にはseekp
メンバ関数を使用します。
これはC言語でのfseek関数と同様の働きをします。
第一引数はファイル位置のオフセット(第二引数からの相対位置)です。
第二引数は以下の値のいずれかを指定します。
- std::ios::beg
(std::ios_base::beg) - ファイルの先頭
SEEK_SET(fseek関数)と同等 - std::ios::cur
(std::ios_base::cur) - ファイルの現在位置
SEEK_CURと同等 - std::ios::end
(std::ios_base::end) - ファイルの末尾
SEEK_ENDと同等
ファイル位置取得
ファイルの現在位置の取得はtellp
メンバ関数を使用します。
これはC言語のftell
関数に対応します。
この関数に引数はなく、戻り値はstd::streampos
型です。
これはストリーム上の位置を表す値です。
内部的にはクラスで実装されていますが、std::streampos
型同士を+や-で演算したり、std::cout
で数値を出力したりできます。
サンプルコード
ファイルのオープンモードとseekp
関数、tellp
関数を使ったサンプルコードです。
#include <iostream>
#include <fstream>
//現在のファイル位置を表示する関数
std::streampos tellFilePoint(std::ofstream& ofs)
{
std::streampos pos = ofs.tellp();
std::cout << "現在のファイル位置: " << pos << std::endl;
return pos;
}
int main()
{
const char* fileName = "C:\\test.txt";
{
//ファイルを新規に作成
std::ofstream ofs(fileName);
if (!ofs) {
std::cout << "ファイルが開けませんでした。" << std::endl;
std::cin.get();
return 0;
}
//適当に書き込んでファイルを閉じる
ofs << "abc";
//ofsの寿命でファイルはクローズされるので
//↓は無くても同じ
ofs.close();
}
{
//上で作ったファイルを開いて終端に移動
std::ofstream ofs(fileName, std::ios::in | std::ios::ate);
if (!ofs)
{
std::cout << "ファイルが開けませんでした。" << std::endl;
std::cin.get();
return 0;
}
//現在のファイル位置を取得&表示
std::streampos pos = tellFilePoint(ofs);
ofs << "12345";
tellFilePoint(ofs);
//ファイルを開いた当初の位置に戻す
ofs.seekp(pos, std::ios::beg);
tellFilePoint(ofs);
ofs << "678";
tellFilePoint(ofs);
ofs.close();
}
std::cin.get();
}
現在のファイル位置: 3 現在のファイル位置: 8 現在のファイル位置: 3 現在のファイル位置: 6
上記コードを実行すると「C:\test.txt」にファイルが作成され、「abc67845」という文字列が書き込まれます。
ファイルのオープンモードは|
(ビットOR演算子)を使用して複数同時に指定可能です。
ate
はファイルのオープンと同時に末尾に移動するモードですが、ofstream
は書き込み専用のファイルストリームなので、単独で指定してもファイルの末尾が検出できないようです。
そのため、in
と同時に指定することでファイルの末尾まで位置を移動させます。
(app
は単独指定でも動作します)
ただし、ofstream
にはファイル読み取り関数がないので、in
を指定してもファイル内容を読み取って表示させるようなことはできません。
入力ストリーム
ファイルの読み取りにはstd::ifstream
を使用します。
#include <iostream>
#include <fstream>
#include <string>
int main()
{
const char *fileName = "C:\\test.txt";
{//適当なファイルを作っておく
std::ofstream ofs(fileName);
if (!ofs)
{
std::cout << "ファイルが開けませんでした。" << std::endl;
std::cin.get();
return 0;
}
ofs << "This is a pen." << std::endl;
}//ofsの寿命はここまで
//ファイルが閉じられ、外部からも読み取り可能になる
//上で作ったファイルを読み取り専用で開く
std::ifstream ifs(fileName);
if (!ifs)
{
std::cout << "ファイルが開けませんでした。" << std::endl;
std::cin.get();
return 0;
}
std::string data;
//ファイルからstringにデータ格納
ifs >> data;
std::cout << data;
std::cin.get();
}
This
ifstream
も、使い方はstd::cin
と同様に>>
で変数に格納できます。
実行結果を見るとファイルの内容である「This is a pen.」のうち「This」までしか取得できていませんが、これもstd::cin
と同様で空白文字が入力の区切り文字と判断されるからです。
改行までを読み込む
改行文字までを読み込みたい場合はstd::getline
関数を使用します。
この関数は<string>
ヘッダファイルのインクルードが必要です。
- template <class CharType, class Traits, class Allocator>
basic_istream<CharType, Traits>& getline(
basic_istream<CharType, Traits>& in_stream,
basic_string<CharType, Traits, Allocator>& str,
CharType delimiter); - 入力ストリームin_streamから文字delimiterが出現するまでを読み取り、stringインスタンスstrに格納する。
戻り値は入力ストリームへの参照。 - template <class CharType, class Traits, class Allocator>
basic_istream<CharType, Traits>& getline(
basic_istream<CharType, Traits>& in_stream,
basic_string<CharType, Traits, Allocator>& str); - 入力ストリームin_streamから改行が出現するまでを読み取り、stringインスタンスstrに格納する。
戻り値は入力ストリームへの参照。
std::string data;
std::ifstream ifs("a.txt");
std::getline(ifs, data); //一行読み取る
第一引数は読み取りを行うストリームを指定します。
この関数はC言語のfgets関数と同じように、入力ストリームを指定して文字列を取得できます。
ここにstd::cin
を指定すると標準入力から文字列を読み取ります。
第二引数には読み取った文字列を格納するstringクラスのインスタンスを指定します。
第三引数は読み取りの区切りとなる文字を指定します。
この引数は省略可能で、省略した場合は改行文字までを読み取ります。
std::getline
関数はstringクラスの関連関数として定義されています。
(<string>
ヘッダ内に宣言がある)
ifstream
にもgetline
メンバ関数が存在しますが、こちらは格納先にstringクラスを指定できず、char型の配列を指定する必要があります。
C++ではstring型を使った方が便利で安全なので、特別な理由がない限りはstd::getline
関数を使用したほうが良いでしょう。
ファイル終端チェック
eof関数
eof
関数は、ファイルが終端に達した場合に真を返します。
これを利用してファイルの内容をすべて読み込むサンプルです。
#include <iostream>
#include <fstream>
#include <string>
int main()
{
const char* fileName = "C:\\test.txt";
{//適当なファイルを作っておく
std::ofstream ofs(fileName);
if (!ofs)
{
std::cout << "ファイルが開けませんでした。" << std::endl;
std::cin.get();
return 0;
}
ofs << "This is a pen." << std::endl;
}
//上で作ったファイルを読み取り専用で開く
std::ifstream ifs(fileName);
if (!ifs)
{
std::cout << "ファイルが開けませんでした。" << std::endl;
std::cin.get();
return 0;
}
std::string data;
{
std::ifstream ifs(fileName);
if (!ifs)
{
std::cout << "ファイルが開けませんでした。" << std::endl;
std::cin.get();
return 0;
}
std::string buf;
std::getline(ifs, buf);
data += buf;
while (!ifs.eof())
{
data += "\n";
std::getline(ifs, buf);
}
}
std::cout << data;
std::cin.get();
}
eof
関数が偽を返すまで、while文でファイルの読み取りを繰り返します。
getline
関数は改行文字までを読み取りますが、改行文字自体は変数に格納しないので、読み取った文字列の末尾に改行を足しておきます。
ストリームの状態を返す関数は以下があります。
(文字の入出力#入力のエラーチェックで紹介したものと同じです)
- good()
- 通常状態(エラーなし)
- eof()
- ファイルの終端
- fail()
- 読み取りの失敗
- bad()
- その他のエラー
ストリーム自体をチェック
ストリーム自身を条件判定すると、fail
状態またはbad
状態のときに偽を返します。
これを利用してファイル終端を検出することもできます。
(正確には、読み取りエラー発生の判定)
>>
演算子による入力やgetline
関数などは戻り値としてストリーム自身を返すので、以下のような記述が可能です。
std::ifstream ifs(fileName);
std::string buf;
//真を返す場合は読み取り成功
while (std::getline(ifs, buf))
{
data += buf + "\n";
}
状態のクリア
eof
などの状態になったストリームを通常状態(good
)に戻すにはclear
関数を使用します。
std::ifstream ifs(fileName);
std::string buf;
//真を返す場合は読み取り成功
while (std::getline(ifs, buf))
{
data += buf + "\n";
}
//ここに到達した時点でストリームはgood以外の状態
//状態をgoodに戻す
ifs.clear();
//ファイル位置を先頭に戻す
ifs.seekg(0, std::ios::beg);
一度good
以外の状態になったストリームに対してはこの関数でgood
状態に戻しておかないと、シークなども行えなくなるので注意してください。
ファイル位置の設定と取得
入力ストリームでファイル位置を変更する場合はseekg
関数を使用します。
現在のファイル位置はtellg
関数で取得できます。
出力ストリームの場合はseekp、tellp、入力ストリームの場合はseekg、tellgと微妙に名前が異なります。
(put(書き込み)のp、get(読み取り)のgだそうです)
読み取り用も書き込み用も動作は同じです。
入出力ストリーム
fstream
は、入力と出力を同時に行うことができます。
これらはofstream
とifstream
を合体させたようなもので、使い方も同じです。
双方向ストリームとも言います。
#include <iostream>
#include <fstream>
#include <string>
int main()
{
const char* fileName = "R:\\test.txt";
//テスト用のファイル生成
{
std::ofstream ofs(fileName);
if (!ofs)
{
std::cout << "ファイルが開けませんでした。" << std::endl;
std::cin.get();
return 0;
}
ofs << "This is a pen.\n";
ofs << "That is a book.\n";
ofs << "This is a car.\n";
ofs << "That is a pen.\n";
}//この時点でファイル生成完了
std::string data;
{
std::fstream fs(fileName);
if (!fs)
{
std::cout << "ファイルが開けませんでした。" << std::endl;
std::cin.get();
return 0;
}
//ファイル読み込み
std::string buf;
size_t match;
while (std::getline(fs, buf))
{
match = buf.find("pen");
if (match != std::string::npos)
buf.replace(match, 3, "drink"); //3は"pen"の文字数
data += buf + "\n";
}
fs.clear(); //eofフラグをクリア
fs.seekp(std::ios::beg); //ファイル位置を先頭にセット
//ファイル書き込み
fs << data;
}
std::cout << data;
std::cin.get();
}
This is a drink. That is a book. This is a car. That is a drink.
このサンプルコードでは、文字列中に「pen」という文字が登場したら「drink」に置き換える処理をしています。
stringクラスのfind
関数は文字列が登場する位置を返します。
見つからなかった場合はstd::string::npos
を返しるので、それ以外の場合に置き換えを行います。
(C++の文字列2を参照)
一度ファイル終端まで達すると、seekp
などのファイル位置移動関数が効かなくなるのでclear
関数を使用して状態をリセットします。
その上で、ファイル位置を先頭に戻してからファイルを上書きしています。
バイナリデータ
ファイルストリームでバイナリを扱うには、オープンモードにstd::ios::binary
を指定します。
シフト演算子(<<
、>>
)によるデータの流し込みはそれに対応しているデータ型やクラスにしか使えないので、read
関数とwrite
関数を使用します。
- basic_istream<Char_T, Tr>& read(
char_type* str,
streamsize count); - ストリームに文字列strをcount文字分を書き込む。
戻り値はストリームへの参照。
- basic_ostream<Elem, Tr>& write(
const char_type* str,
streamsize count); - ストリームから文字列strにcount文字分を読み取る。
戻り値はストリームへの参照。
1バイトデータはchar型で扱うので「文字」や「文字列」となっていますが、それ以外のデータも扱うことができます。
C言語的な文字列ではないので、読み取ったデータの終端にNULL文字は付けられません。
streamsize
というデータ型はストリームのサイズを表す符号つき整数型です。
(std
名前空間)
#include <iostream>
#include <fstream>
#include <string>
#include <vector>
int main()
{
auto a = sizeof(0);
const char* fileName = "R:\\test.bin";
//書き込むデータのサイズ(要素数)
size_t length;
//ファイルに書き込み
{
//書き込むデータの作成
std::vector<int> vec_out;
for (int i = 0; i < 5; i++)
vec_out.push_back(i * 2);
length = vec_out.size();
//ファイル書き込み
std::ofstream ofs(fileName, std::ios::binary);
if (!ofs)
{
std::cout << "ファイルが開けませんでした。" << std::endl;
std::cin.get();
return 0;
}
//ファイル先頭に要素数を書き込み
ofs.write(reinterpret_cast<char*>(&length), sizeof(size_t));
ofs.write(reinterpret_cast<char*>(&vec_out[0]), sizeof(int) * static_cast<std::streamsize>(length));
}
//データ受け取り用
std::vector<int> vec_in;
//ファイルからデータ復元
{
std::ifstream ifs(fileName, std::ios::binary);
if (!ifs)
{
std::cout << "ファイルが開けませんでした。" << std::endl;
std::cin.get();
return 0;
}
//ファイル先頭から要素数を読み取り容量確保
ifs.read(reinterpret_cast<char*>(&length), sizeof(size_t));
vec_in.resize(length);
ifs.read(reinterpret_cast<char*>(&vec_in[0]), sizeof(int) * static_cast<std::streamsize>(length));
}
for (int i = 0; i < vec_in.size(); i++)
std::cout << vec_in[i] << std::endl;
std::cin.get();
}
0 2 4 6 8
一度データをファイルに保存し、ファイルからデータが復元できていることを確認するサンプルコードです。
read
関数もwrite
関数も、char*型(const char*型)しか扱うことができないので、それ以外のデータ型の場合はキャストが必要です。
int型のvectorの要素をファイルに書き込むのですが、ファイルの読み込み時にも要素数が分かるようにファイルの先頭にsize_t型のサイズで要素数を書き込んでいます。
今回はそういう決まりのファイルを作成していますが、このフォーマットを知らない他のプログラムからはこの書き出したファイルを正確に読み取ることは出来ません。
なお、vectorクラスの各要素は配列のように連続したメモリ領域に確保されることが保証されていますが、このような保証がないクラスではアドレスを指定してデータを一気に読み書きすることはできないので注意してください。
実際のプログラムではこのような読み書きするようなことはほぼなく、専用のクラスを作ることになるでしょう。