バイナリ形式での読み書き | Programming Place Plus 新C++編

トップページ新C++編

このページの概要 🔗

このページでは、ファイルの内容をバイナリ形式で読み書きする方法を取り上げます。これまでのページでも、ファイルを読み書きする処理は登場していますが、これまでの方法はテキスト形式で読み書きするものでした。テキスト形式は、データがテキスト(文字)だけで構成されていることを想定したものであり、どんな種類のファイルでも取り扱えなければならないバイナリエディタの実装には不向きです。

このページの解説は C++14 をベースとしています

以下は目次です。要点だけをさっと確認したい方は、「まとめ」をご覧ください。



テキスト形式とバイナリ形式 🔗

一般的にバイナリエディタは、ファイルを1つ選んで読み込み、その中身をバイト単位で表示するという作りになっています。ファイルの内容を読み込むプログラムはこれまでにも登場しているので(「ファイル処理」のページを参照)、同じ方法で実現できそうに思えます。

std::ifstream ifs("test.txt");
std::string s {};
ifs >> s;

しかしこの方法は、バイナリエディタの実装に使うには問題があります。

データの形式には、テキスト形式 (text format) とバイナリ形式 (binary format) の2つがあります。

テキスト形式では、データを構成しているバイトはどれも文字を表現しています。画像や音声のように、文字ではないデータを含んでいる場合はバイナリ形式です。

テキスト形式で画像や音声などを表現するファイルフォーマットも存在しますが、ここでは無視しています。

たとえば「12345」を文字列として記録しているならテキスト形式ですが、数値として記録しているならバイナリ形式です。前者なら、半角の数字を 1バイトで表現するエンコーディング形式(「マルチバイト文字」のページを参照)であれば、全部で 5バイトです。後者の場合、12345 という整数を表現するために最低限必要なバイト数は 2バイトです。


文字でないデータを含んでいるときに、テキスト形式のつもりで取り扱うとどんな問題が起こり得るでしょうか。

1つに、改行文字の問題があります。テキスト形式では、改行を特別な文字として表現します。改行文字の具体的な表現は環境(OS)によって異なり、Windows では「0x0D 0x0A」という 2バイトで、macOS や Linux では「0x0A」という 1バイトです。これまでに登場したプログラムがそうであったように、C++ のソースコード上では、'\n' と書くことによって、環境ごとの違いが吸収されています。テキスト形式としてファイルを読み込むとき、Windows では「0x0D 0x0A」が出現したら、1つの改行であると判断します。反対に、テキスト形式としてファイルに書き出すときには、'\n' を「0x0D 0x0A」に変換して書き込みます。バイナリ形式として取り扱うと、こうした変換が行われません。

もう1つには、ファイルの終端の問題があります。ファイルの終端には、EOF (End Of File) と呼ばれる1バイトのマーク(「0x1A」で表現)が置かれていることがあり、テキスト形式での読み込みではこれより後ろにあるデータを読み込みません。

画像や音声などのデータには、こうした特別な意味をもつ文字と同じバイトが偶然含まれている可能性があります。そういうファイルをテキスト形式として取り扱うと、改行やファイル終端であると誤認識して、おかしな結果を生んでしまいます。こうした問題を回避するため、文字データのみが書かれたファイルではない場合、バイナリ形式として取り扱わなければなりません。

バイナリエディタは、ファイルを構成するバイト列をありのまま確認することが目的のツールなので、余計な変換が行われてはいけません。そのため、バイナリ形式で読み込む必要があります。

オープンモード 🔗

ファイルをテキスト形式で扱うのか、バイナリ形式で扱うのかは、std::ofstream や std::ifstream に渡すオープンモードによって指定します。

std::ofstream ofs(パス, オープンモード);
std::ifstream ifs(パス, オープンモード);

オープンモードには、以下の表に示す6種類があって、これらを組み合わせて指定します。

指定 意味
std::ios_base::in 入力のために開く
std::ios_base::out 出力のために開く
std::ios_base::binary バイナリ形式として扱う
std::ios_base::trunc 開いたとき、元あった内容を破棄する
std::ios_base::app 書き込もうとするときに、ストリームの末尾に移動する (append の略)
std::ios_base::ate 開いた直後、ストリームの末尾に移動する (at end の略)

オープンモードの指定を省略した場合、デフォルトのオープンモードが指定されたことになります。std::ofstream のデフォルトのオープンモードは std::ios_base::out、std::ifstream なら std::ios_base::in です。いずれも std::ios_base::binary を含んでおらず、テキスト形式で取り扱うことになります。

このページではファイルを対象とした解説に限定しますが、std::istringstream や std::ostringstream でも(「stringstream」のページを参照)、初期化時の第2引数にオープンモードを渡せるようになっています。

なお、std::ios_base::app、std::ios_base::ate については、このページでは取り上げません。

ランダムアクセス」のページで取り上げます。

上記6つの定義は std::ios_base::openmode型ですが、これは型の別名であり、具体的にどのような型なのかは処理系定義となっています。いずれも、 <ios> で定義されていますが、<iostream> をインクルードすると同時にインクルードされるため[1]、多くの場合、特に気にせず使用できます。

【C言語プログラマー】3つ目までは、fopen関数の第2引数の各種指定(“r”、“w”、“b”)と、std::ios_base::app は “a” と一致しています。std::ios_base::trunc は “w” のときの挙動と一致します[2]。std::ios_base::ate は、開いた直後に fseek関数で末尾に移動することと同義です。

std::ofstream に std::ios_base::in を指定したり、std::ifstream に std::ios_base::out を指定したりしたらどうなるのだろうかと思うかもしれません。指定自体はできますが、std::ofstream には読み込みの関数がありませんし、std::ifstream には書き込みの関数がありませんから、結局、こうした反対方向の指定には意味がありません。もし入出力の両方が必要である場合には、std::fstream を使います。

std::ios_base::trunc を指定すると、ファイルに元々書き込まれていた内容が消えます。それならば、指定しなければ元の内容を残してくれそうですが、実際には std::ios_base::out を指定していると、std::ios_base::trunc を指定しなくても消されます

【上級】元の内容を消さずに書き込みを行うには、std::ios_base::app を使うか(この場合は、末尾に書き足すことしかできない)、std::fstream を使って、読み書き両用でオープンする必要があります。

オープンモードは、ビット単位のフラグになっています。そのため、入力用に開きつつ、バイナリ形式として扱いたいと思ったら、std::ios_base::in | std::ios_base::binary のように、ビット単位OR(「ビット単位の処理」のページを参照)を使って結合します。

std::ofstream ofs(パス, std::ios_base::out | std::ios_base::binary);
std::ifstream ifs(パス, std::ios_base::in | std::ios_base::binary);

バイナリ形式での読み書き 🔗

では実際にバイナリ形式でファイルを読み書きしてみます。

これまでのページで使ってきた、<<>>、std::getline関数を使う方法は、バイナリ形式の読み書きには不向きです。これらの方法は、データが空白文字や改行で区切られたフォーマット(書式)になっているときに使うものです。バイナリ形式の読み書きは、こうしたフォーマットで表現されていることを前提にできません(普通、区切りや改行の概念がなく、データが詰め込まれた状態になっています)。

C++ の標準ライブラリでは、書式が決まっている場合にうまく利用できる関数や演算子は、書式付き入力 (formatted input)、書式付き出力 (formatted output) と呼ばれ、書式を想定しないものは、書式なし入力 (unformatted input)、書式なし出力 (unformatted output) と呼ばれます。

大きさを指定して書く (writeメンバ関数) 🔗

書き込みからやってみます。

std::ofstream の writeメンバ関数[3]を使うと、大きさを指定しながら書き込みを行えます。

【上級】正確には、std::ofstream の基底クラスにあたる std::basic_ostreamクラステンプレートで宣言されているメンバ関数です。

writeメンバ関数には2つの引数があって、1つ目が書き込みたいデータ(のメモリアドレス)、2つ目がその大きさ(バイト数)です。

writeメンバ関数は書式なし出力なので、どんな種類のデータであっても単なるバイト列として扱う作りになっており、第1引数の型は const char* です。そのため、int型の変数 value の値を書き出したいという場合は、reinterpret_cast<const char*>(&value) のように、&演算子でメモリアドレスを得て、const char* にキャストすることでバイト列として扱わなければなりません。

【上級】関数テンプレートを使ってうまく処理をまとめれば多少は楽になります[4]

【上級】第1引数の型は正確には const char_type* です。char_type は、std::basic_ostreamクラステンプレートのテンプレート実引数によって異なりますが、std::ofstream を使っているのならそれは char であるため、結果的に const char* ということになります。

第2引数は std::streamsize という型です。これは符号付き整数型の別名ですが、具体的なことは処理系定義になっています。

書き込みのエラーチェックはいつも通り、std::ofstream の failメンバ関数を使うことで行います。ただし、バッファリングの仕組みのせいで、write関数を呼んだ直後でエラーチェックを行っても意味がない可能性もあります(「ファイルとエラー」のページを参照)。


では実際に、適当なデータをバイナリ形式で書き出してみます。

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

int main()
{
    // バイナリ形式でオープン
    constexpr auto path = "test.bin";
    std::ofstream ofs(path, std::ios_base::out | std::ios_base::binary);
    if (ofs.fail()) {
        std::cerr << "File open error: " << path << "\n";
        std::quick_exit(0);
    }

    // 適当なデータを書き込む
    std::int32_t a {0x123456};
    std::int64_t b {0xabcd0123LL};
    char c[] {"xyz"};
    ofs.write(reinterpret_cast<const char*>(&a), sizeof(a));
    ofs.write(reinterpret_cast<const char*>(&b), sizeof(b));
    ofs.write(c, sizeof(c));
    if (ofs.fail()) {
        std::cerr << "File write error: " << path << "\n";
        std::quick_exit(0);
    }

    ofs.close();
    if (ofs.fail()) {
        std::cerr << "File close error: " << path << "\n";
        std::quick_exit(0);
    }
}

実行結果(test.bin をバイナリで表示):

56 34 12 00 23 01 CD AB 00 00 00 00 78 79 7A 00

ソースコード上での整数値と、実行結果の並びが一致していないのは、この実行環境がリトルエンディアンだからです(「配列とポインタ」のページを参照)。

writeメンバ関数の第1引数がメモリアドレスを渡す仕様であるため、書き込みたいデータはまず変数に入れることになります(リテラルにメモリアドレスは存在しない)。前述のとおり const char* へのキャストが必要です。


テキスト形式で書き込む場合なら、各データを空白文字や改行文字で区切るのが一般的です。そうしないと、データの切れ目が分からなくなってしまい、読み込み側のプログラムを書くことができません。一方、バイナリ形式の場合は、こうした区切りを入れないほうが一般的です。ファイル内のデータの並び方のルール(データフォーマット)が分かっていれば、読み込み側のプログラムは書けます。今回の例でいえば、先頭から順に、4バイトの整数、8バイトの整数、4バイト分の文字列が並んでいますが、これを知っていれば、読み込み側は 4バイト、8バイト、4バイトの順で読み取っていけばいいわけです。逆にいえば、データフォーマットを知らないバイナリ形式のファイルを正しく読み込むことは非常に困難(あるいは不可能)です。

そのため、バイナリ形式では各データの大きさはとても重要なので、int型や long long int型といった、処理系ごとに大きさが異なる可能性がある型ではなく、std::int16_t や std::int32_t のような、大きさが一定の型を使ったほうが安全です(「整数型」のページを参照)。

大きさを指定して読む (readメンバ関数) 🔗

次に読み込みをやってみます。大きさを指定した読み込みには、std::ifstream の readメンバ関数[5]を使います。

【上級】正確には、std::ifstream の基底クラスにあたる std::basic_istreamクラステンプレートで宣言されているメンバ関数です。

さきほど書き込みのプログラムで書き出されたファイルを読み込んでみます。

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

int main()
{
    constexpr auto path = "test.bin";
    std::ifstream ifs(path, std::ios_base::in | std::ios_base::binary);
    if (ifs.fail()) {
        std::cerr << "File open error: " << path << "\n";
        std::quick_exit(0);
    }

    // データを読み込む
    std::int32_t a {};
    std::int64_t b {};
    char c[4] {};
    ifs.read(reinterpret_cast<char*>(&a), sizeof(a));
    ifs.read(reinterpret_cast<char*>(&b), sizeof(b));
    ifs.read(c, sizeof(c));
    if (ifs.fail()) {
        std::cerr << "File read error: " << path << "\n";
        std::quick_exit(0);
    }

    // 確認
    std::cout << std::hex << a << "\n"
              << std::hex << b << "\n"
              << std::dec << c << "\n";

    ifs.close();
    if (ifs.fail()) {
        std::cerr << "File close error: " << path << "\n";
        std::quick_exit(0);
    }
}

test.bin(バイナリで表示):

56 34 12 00 23 01 CD AB 00 00 00 00 78 79 7A 00

実行結果:

123456
abcd0123
xyz

readメンバ関数の第1引数は、ファイルから読み込んだ値を書き込む場所(メモリアドレス)です。第2引数は読み込むデータの大きさを指定します。第1引数の型は char* です。指定したメモリアドレスへ値を書くので、constポインタではありません。第2引数の型は std::streamsize です。

読み込みのエラーチェックはいつも通り、std::ifstream の failメンバ関数を使うことで行います。

ここでは "xyz\0" の部分を char型の配列で受け取っていますが、std::string で扱いたいと思うかもしれません。その場合は、先頭要素のメモリアドレスを指定すればいいですが、std::string に十分なサイズがなければならないことと、readメンバ関数の第2引数を sizeof(c) とはできないことに注意してください。

std::string c(4, ' ');  // 十分なサイズが必要
ifs.read(&c[0], 4);

【C++17】std::string の dataメンバ関数に、const でないポインタを返すオーバーロードが追加されました[7]。そのため、ifs.read(c.data(), 4); とすることが可能になっています。

std::vector に受け取りたいときも同様ですが、こちらは dataメンバ関数[6]を使ってもいいです。

std::vector<char> c(4);  // 十分なサイズが必要

ifs.read(&c[0], 4);
ifs.read(c.data(), 4);  // こちらでもいい

パディング 🔗

構造体の内容を書き出す場合、構造体全体を指定すると、パディング(「メモリとオブジェクト」のページを参照)の部分まで書き出されることに注意が必要です。パディングの入り方は処理系によって異なるため、この方法で書き出したファイルが、ほかの処理系で正常に読めない可能性があります。

struct S {
    std::int64_t a;
    char b;
    std::int16_t c;
};
S s {};

ofs.write(&s, sizeof(s));  // 可能ではあるが、読み込み側もパディングを意識して正しく実装しないと、正常に読めない

面倒でも、構造体の各メンバを1つ1つ書き出す・読み込むようにコードを書き、ファイルの側にはパディングの部分が含まれないようにするのが安全です。

ofs.write(reinterpret_cast<const char*>(&s.a), sizeof(s.a));
ofs.write(reinterpret_cast<const char*>(&s.b), sizeof(s.b));
ofs.write(reinterpret_cast<const char*>(&s.c), sizeof(s.c));

ifs.read(reinterpret_cast<char*>(&s.a), sizeof(s.a));
ifs.read(reinterpret_cast<char*>(&s.b), sizeof(s.b));
ifs.read(reinterpret_cast<char*>(&s.c), sizeof(s.c));

エンディアン 🔗

もう1つ問題になるのはエンディアン(「配列とポインタ」のページを参照)です。たとえば、リトルエンディアンの処理系でバイナリ形式で書き出したファイルを、ビッグエンディアンの処理系で読み込むと、バイトの並び順が入れ替わった状態になり、正常な値を読めません。

プログラムを実行している環境のエンディアンと、ファイルのエンディアンを比べて、両者が同じなら何もする必要はありませんが、違っているのなら、読み込みのあとでバイトの順序を入れ替える処理(バイトスワッピング (byte swapping))が必要です。

ここではリトルエンディアンとビッグエンディアンだけを想定しています。非常にレアですが、ほかにもエンディアンの種類はあるため、そうした環境では、ここで取り上げるものとは異なる実装が必要になります。

実行環境のエンディアンは、環境が同じであるかぎりは変化するものではないので、特定のエンディアンに決め打ちしてしまうことも1つの方法ではあります。複数の環境で使うプログラムの場合は、#if でコードを切り分けて、環境ごとにコンパイルしなおす方法があります。

もしくは、以下のようなコードで、実行時にエンディアンを判定することもできます。

【C++20】実行環境のエンディアンを std::endian::native[8] で調べられるようになったので、ここで紹介するような技巧的な手段は不要になっています。

#include <cstdint>
#include <iostream>

int main()
{
    std::uint16_t n {1};
    if (*reinterpret_cast<unsigned char*>(&n) == 1) {
        std::cout << "Little endian.\n";
    }
    else {
        std::cout << "Big endian.\n";
    }
}

実行結果:

Little endian.

まず、2バイト以上の大きさの変数に 1 を入れておきます(1バイトの変数ではバイトの順序という考え方がないため、2バイト以上必要)。2バイトの変数n に格納した 1 は、実行環境がリトルエンディアンなら 01 00、ビッグエンディアンなら 00 01 という 2バイトの並びになります。

次に &nunsigned char* にキャストしています。unsigned char は 1バイトなので、このキャストによって、さきほどの 2バイトの並びの上位側のバイトのメモリアドレスが得られます。そのバイトの値を * による間接参照で取り出します。この結果は、リトルエンディアンなら 01、ビッグエンディアンなら 00 になります。したがって、== 1 という比較が true になるならリトルエンディアン、false になるならビッグエンディアンであることが分かります。


読み込んだファイルのエンディアンと、実行環境のエンディアンが異なっている場合は、読み込み後にバイトスワッピングを行います。基本的にはバイトの並び順を逆転させればいいので、以下の流れがシンプルに実装できます。

  1. バイト列として読み込む
  2. バイトスワッピングを行う
  3. 本来の型にキャストする
#include <iostream>
#include <utility>

void byte_swap16(unsigned char* bytes)
{
    std::swap(bytes[0], bytes[1]);
}

void byte_swap32(unsigned char* bytes)
{
    std::swap(bytes[0], bytes[3]);
    std::swap(bytes[1], bytes[2]);
}

void byte_swap64(unsigned char* bytes)
{
    std::swap(bytes[0], bytes[7]);
    std::swap(bytes[1], bytes[6]);
    std::swap(bytes[2], bytes[5]);
    std::swap(bytes[3], bytes[4]);
}

int main()
{
    unsigned char b1[] {0x12,0x34};  // 読み込まれたデータとする
    std::cout << std::hex << *reinterpret_cast<std::uint16_t*>(b1) << "\n";
    byte_swap16(b1);
    std::cout << std::hex << *reinterpret_cast<std::uint16_t*>(b1) << "\n";

    unsigned char b2[] {0x12,0x34,0x56,0x78};  // 読み込まれたデータとする
    std::cout << std::hex << *reinterpret_cast<std::uint32_t*>(b2) << "\n";
    byte_swap32(b2);
    std::cout << std::hex << *reinterpret_cast<std::uint32_t*>(b2) << "\n";

    unsigned char b3[] {0x12,0x34,0x56,0x78,0x90,0xab,0xcd,0xef};  // 読み込まれたデータとする
    std::cout << std::hex << *reinterpret_cast<std::uint64_t*>(b3) << "\n";
    byte_swap64(b3);
    std::cout << std::hex << *reinterpret_cast<std::uint64_t*>(b3) << "\n";
}

実行結果(リトルエンディアン環境):

3412
1234
78563412
12345678
efcdab9078563412
1234567890abcdef

実行結果(ビッグエンディアン環境):

1234
3412
12345678
78563412
1234567890abcdef
efcdab9078563412

もう1つのパターンとして、すでに整数型の値になっている場合は、以下のようにビット演算を駆使してバイトスワッピングを実現できます。

【C++23】std::byteswap関数[9]が追加されたため、自作する必要はなくなりました。

#include <cstdint>
#include <iostream>

std::uint16_t byte_swap16(std::uint16_t value)
{
    return (value << 8)
         | (value >> 8)
         ;
}

std::uint32_t byte_swap32(std::uint32_t value)
{
    return (value << 24)
         | (value & 0x0000'ff00) << 8
         | (value & 0x00ff'0000) >> 8
         | (value >> 24)
         ;
}

std::uint64_t byte_swap64(std::uint64_t value)
{
    return (value << 56)
         | (value & 0x0000'0000'0000'ff00) << 40
         | (value & 0x0000'0000'00ff'0000) << 24
         | (value & 0x0000'0000'ff00'0000) << 8
         | (value & 0x0000'00ff'0000'0000) >> 8
         | (value & 0x0000'ff00'0000'0000) >> 24
         | (value & 0x00ff'0000'0000'0000) >> 40
         | (value >> 56)
         ;
}

int main()
{
    std::uint16_t v16 {0x1234};
    std::cout << std::hex << v16 << "\n";
    v16 = byte_swap16(v16);
    std::cout << std::hex << v16 << "\n";

    std::uint32_t v32 {0x12345678};
    std::cout << std::hex << v32 << "\n";
    v32 = byte_swap32(v32);
    std::cout << std::hex << v32 << "\n";

    std::uint64_t v64 {0x1234567890abcdef};
    std::cout << std::hex << v64 << "\n";
    v64 = byte_swap64(v64);
    std::cout << std::hex << v64 << "\n";
}

実行結果(リトルエンディアン環境):

1234
3412
12345678
78563412
1234567890abcdef
efcdab9078563412

実行結果(ビッグエンディアン環境):

3412
1234
78563412
12345678
efcdab9078563412
1234567890abcdef

すべて読む (std::istreambuf_iterator)

ファイルの中身を全部一気に読み込みたいときは、std::istreambuf_iterator[10] を使う方法が簡単です。

std::istreambuf_iterator は、名前のとおり、ストリームのバッファ(「ファイルとエラー」のページを参照)に対して機能するイテレータであり、std::ifstream が内部にもっているバッファを指し示しています。std::istreambuf_iterator は、<iterator> で定義されています。

以下のプログラムは、test.txt の中身全部をそのままのかたちで std::vector に読み込みます。

#include <cstdlib>
#include <fstream>
#include <iomanip>
#include <iostream>
#include <iterator>
#include <vector>

int main()
{
    constexpr auto path = "test.txt";

    std::ifstream ifs(path, std::ios_base::in | std::ios_base::binary);
    if (ifs.fail()) {
        std::cerr << "File open error: " << path << "\n";
        std::quick_exit(0);
    }

    // ファイルの中身すべてを std::vector<char> に読み込む
    std::istreambuf_iterator<char> it_ifs_begin(ifs);
    std::istreambuf_iterator<char> it_ifs_end {};
    std::vector<char> input_data(it_ifs_begin, it_ifs_end);
    if (ifs.fail()) {
        std::cerr << "File read error: " << path << "\n";
        std::quick_exit(0);
    }

    // 読み込んだ内容をバイト列として標準出力に書き出す
    for (std::size_t i {0}; i < input_data.size(); ++i) {
        std::cout << std::setw(2) << std::setfill('0') << std::hex << std::uppercase
            << static_cast<unsigned int>(input_data[i]) << " ";
    }
    std::cout << "\n";

    ifs.close();
    if (ifs.fail()) {
        std::cerr << "File close error: " << path << "\n";
        std::quick_exit(0);
    }
}

test.txt の内容:

1行目
2行目
3行目

実行結果:

EF BC 91 E8 A1 8C E7 9B AE 0D 0A EF BC 92 E8 A1 8C E7 9B AE 0D 0A EF BC 93 E8 A1 8C E7 9B AE

std::istreambuf_iterator を使うときの型名は、取り扱うデータの大きさが char型の大きさ(つまり 1バイト単位)であることを明確にするため、std::istreambuf_iterator<char> とします。std::ifstream型の変数を渡してやることで、その std::ifstream が持っているバッファをターゲットにしたイテレータとして初期化されます。何も渡さずに定義した場合は、ファイルの終端を指すイテレータであるとみなされます。

std::vector の変数を定義するとき、2つのイテレータで範囲を渡すことで、その範囲内の要素の値による初期化が行えました(「配列」のページを参照)。そのため、std::vector<char> input_data(it_ifs_begin, it_ifs_end); というコードで、ターゲットのバッファと、ファイルの終端を指示したことになり、そのあいだにある要素、つまりファイルの中身全部による初期化が行われます。

読み込みのエラーチェックはいつも通り、std::ifstream の failメンバ関数を使うことで行います。

まとめて書く (std::ostreambuf_iterator)

さきほどの反対で、std::vector に入っているすべての値をバイナリ形式として一気に書き込みます。std::ifstream に代わって std::ofstream を使い、std::istreambuf_iterator に代わって std::ostreambuf_iterator[11] を使います。

#include <algorithm>
#include <cstdlib>
#include <fstream>
#include <iostream>
#include <iterator>
#include <vector>

int main()
{
    constexpr auto path = "test.bin";
    std::ofstream ofs(path, std::ios_base::out | std::ios_base::binary);
    if (ofs.fail()) {
        std::cerr << "File open error: " << path << "\n";
        std::quick_exit(0);
    }

    // 適当なデータを作る
    std::vector<unsigned char> data {};
    data.push_back(0);
    data.push_back(100);
    data.push_back(200);
    data.push_back('\n');
    data.push_back('a');
    data.push_back('b');
    data.push_back('c');

    // std::vector の中身すべてをファイルへ書き出す
    std::ostreambuf_iterator<char> it_ofs(ofs);
    std::copy(std::cbegin(data), std::cend(data), it_ofs);
    if (ofs.fail()) {
        std::cerr << "File write error: " << path << "\n";
        std::quick_exit(0);
    }

    ofs.close();
    if (ofs.fail()) {
        std::cerr << "File close error: " << path << "\n";
        std::quick_exit(0);
    }
}

実行結果(test.bin をバイナリで表示):

00 64 C8 61 62 63

適当に作ったデータを std::ostreambuf_iterator を使って書き込みます。std::copy関数(「配列」のページを参照)を使って、std::copy(std::cbegin(data), std::cend(data), it_ofs); とすることで、data の先頭から末尾の範囲にあるものを、it_ofs が指し示すバッファ(std::ofstream が持っているバッファ)へコピーします。書き込みのためのバッファに蓄えられたデータは、バッファリングがどのような実装になっているか次第ですが、どこかのタイミングで実際にファイルへ書き込まれることになります。

書き込みのエラーチェックはいつも通り、std::ofstream の failメンバ関数を使うことで行います。ただし、バッファリングの仕組みのせいで、書き込みのコードの直後でエラーチェックを行っても意味がない可能性もあります(「ファイルとエラー」のページを参照)。

まとめ 🔗


新C++編の【本編】の各ページには、末尾に練習問題があります。ページ内で学んだ知識を確認する簡単な問題から、これまでに学んだ知識を組み合わせなければならない問題、あるいは更なる自力での調査や模索が必要になるような高難易度な問題をいくつか掲載しています。


参考リンク 🔗


練習問題 🔗

問題の難易度について。

★は、すべての方が取り組める入門レベルの問題です。
★★は、自力でプログラミングができるようなるために、入門者の方であっても取り組んでほしい問題です。
★★★は、本格的にプログラマーを目指す人のための問題です。

問題1 (確認★)

以下の変数の値をファイルに保存するとします。テキスト形式として保存した場合、バイナリ形式として保存した場合、それぞれどのようなファイルになりますか? また、実際にそれぞれのプログラムを作ってみてください。

std::int32_t a {45};
std::int64_t b {527927948LL};
char c[8] {"abcde"};

解答・解説

問題2 (基本★)

問題1で作られたテキスト形式とバイナリ形式のファイルについて、ファイル内のデータを、人が読みやすいかたちで標準出力に出力するプログラムを作成してください。

解答・解説

問題3 (応用★★)

適当に test.bin を用意し、その内容と同じ test_copy.bin を作る、ファイルコピーのプログラムを作成してください。

解答・解説

問題4 (応用★★)

以下の構造体で表現されるデータ(students の内容)をファイルに書き出すコードと、書き出されたデータを読み込むコードをそれぞれ作成してください。

struct Student {
    char name[32];         // 名前
    std::uint8_t grade;    // 学年 (1~6)
    std::uint8_t score;    // 得点(0~100)
};

std::vector<Student> students {};  // データ件数は不明

解答・解説


解答・解説ページの先頭



更新履歴 🔗




はてなブックマーク に保存 Pocket に保存 Facebook でシェア
X で ポストフォロー LINE で送る noteで書く
rss1.0 取得ボタン RSS 管理者情報 プライバシーポリシー
先頭へ戻る