C++バイナリデータの扱い方

C++の標準ライブラリには、ファイルを扱うためのクラスstd::fstreamが含まれています。std::fstreamにはテキストファイルを扱うのかバイナリファイルのかを指定する機能があり、このページではバイナリファイルを扱う場合の使い方をまとめてあります。また、最後にテキストファイルとして扱う場合とバイナリファイルとして扱う場合とでは具体的に何が変わるのかを説明します。

std::filestreamを使いバイナリデータとしてファイルを開く

std::fstreamでファイルを開く場合は、コンストラクタにファイルパスを渡すか、fstream::open関数にファイルパスを渡します。コンストラクタとopen関数の第二引数にはファイルの操作モードを指定することができます。
バイナリデータとして開くためには、この第二引数にstd::ios::binaryを指定する必要があります。
また、入力専用の時はstd::ifstream、出力専用の時はstd::ofstreamを用いることができます。


第二引数についてですが、ビット和演算子「|」を使えばオプションを複数指定することも可能です。この時に論理和演算子「||」と間違えないように注意しましょう。
ファイルを閉じるには、close関数を使用すれば良いですが、明示的に閉じなくてもstd::fstreamのインスタンスが破棄されデストラクタが呼ばれると自動的に閉じられます。

バイナリデータをまとめて読み書き

ファイルの内容を読み込むためにはstd::fstreamのreadメンバ関数を使用します。
第一引数には予め確保しておいたバッファ領域の先頭ポインタ、第二引数には読み込むサイズを渡します。この時、第二引数に渡すサイズをバッファより大きい物にしてしまうと、領域外にアクセスしてしまい発見しづらいバグを生む原因になるので気をつけましょう。

次に書き込みです。ファイルにデータを書き込むためにはstd::fstream::writeメンバ関数を用います。
引数はread関数と同じで、ひとつ目にバッファ先頭のポインタをchar*型で、ふたつ目に書き込むサイズを指定します。この時もバッファのサイズより大きなサイズを指定してはいけません。

データのサイズを取得する

ファイルの領域をあらかじめ確保しておきたい場合など、ファイルサイズが必要になる事があります。std::fstreamにはファイルサイズを知るためのメンバ関数は定義されていませんが、次のようにすることでファイルサイズを取得することができます。

まず、seekg(0,std::ios::end)を実行してファイル末尾までシークします。
seekg関数は現在の読み込み位置を移動する関数で、第二引数に移動の開始位置、第一引数に開始位置からの相対的な位置を取ります。今回は開始位置にファイル終端、相対位置に0を指定することで、ファイルの終端に移動しています。

次に現在の読み込み位置をtellg()で取得すれば、それがファイルサイズとなります。
このtellgによる現在位置は、ファイルの先頭を0とした1バイト単位の数値です。そのため現在位置がファイル末尾である状態で使用すれば、そのファイルのサイズがバイト単位で取得できるという仕組みです。

ファイルをバイナリデータとして開くと何が変わるのか

ファイルをテキストデータとして開いた場合と、バイナリデータとして開いた場合とでは何が違うのでしょうか。大きな違いは2つあります。

ファイル終端の判定に関する違い

テキストデータでは0x1aがファイルの終端であるEOFとして扱われます。これに基づき、read関数ではテキストデータとしてファイルを読み込んだ時に、0x1a以降はデータを読み込まないようになっています。
一方、バイナリデータでは0x1aもデータの一部として出現する可能性があります。このため、バイナリデータをテキストデータとして開いてしまうと、ファイル終端ではないのにもかかわらずデータとして出現した0x1aをファイルの終端として判断してしまい、0x1a以降のデータが読み込めなくなってしまう事があります。バイナリデータとして開くことで0x1aによるファイル終端の検出は行われなくなります。

改行文字の扱いに関する違い

Windowsの場合、テキストデータの時はWindowsで改行を表す”\r\n”が”\n”に変換されます。逆にファイルに出力するときは”\n”が”\r\n”に変換されます。
これはWindowsが改行を”\r\n”として扱う一方で、Unix系のOSでは”\n”を改行として扱うという違いがあるためです。Unix系のOS環境では変換されません。
こうすることでWindows上で動くプログラムの場合でも、Unix系OS上で動くプログラムの場合でも、プログラム内では常に改行を”\n”として扱えるため、OS間でのプログラム移植性が高まるという利点があります。
しかし、それをバイナリデータにも適用してしまうと、データ中に”\r\n”の文字コードに相当するデータが現れた場合に”\n”の文字コードに置き換えられてしまうという問題が発生してしまいます。
バイナリデータとして読み込むことで、この改行コードの変換が行われなくなるため、”\r\n”に相当するコードが勝手に置き換えられることは起こらなくなります。

これらの理由から、バイナリデータを扱いたい際は、ファイルを開く際にstd:ios::binaryを指定する必要があるということになります。