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

トップページ新C++編バイナリ形式での読み書き

このページの概要

このページは、練習問題の解答例や解説のページです。



解答・解説

問題1 (確認★)

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

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


テキスト形式は文字だけでデータを表現するので、整数値を保存する場合、それぞれの桁が数字として保存されることになります。元々文字列である "abcde" はそのまま表現できます(本編解説)。

よって、テキスト形式で保存した結果は、次のようになります。

45 527927948 abcde

1つずつ改行するなら、次のようになるでしょう。

45
527927948
abcde

いずれにせよ、テキスト形式で保存したデータは、文字で構成されているので、基本的には人間がそのまま読める内容になるはずです。保存するだけなら必須ではないですが、データごとに空白文字なり改行文字なりで区切らないと、各データの範囲が判別できず、読み込みが困難になります。

一方、バイナリ形式は、それぞれの値のバイト表現が保存されます(本編解説)。たとえば、変数a の値 45 なら、00000000 00000000 00000000 00101101 という 4つのバイトで表現されます(1バイトごとに区切って記述しています)。変数b も同様に、00000000 00000000 00000000 00000000 00011111 01110111 10001010 10001100 となります。変数c では、それぞれの文字のバイト表現になり、01100001 01100010 01100011 01100100 01100101 00000000 00000000 00000000 ということになります。

これまでの例にならって、各バイトを 16進数で表記すると次のようになります。バイナリ形式で保存した結果として考えられる1つの可能性です。

00 00 00 2D 00 00 00 00 1F 77 8A 8C 61 62 63 64 65 00 00 00

可能性といったのは、実際にはバイトの並び順に関するルール(エンディアン(本編解説))が環境によって異なるためです。上記の出力例はビッグエンディアンの場合で、上位側のバイトから下位側のバイトに並べる方法です。リトルエンディアンの場合は次のようになります。

2D 00 00 00 8C 8A 77 1F 00 00 00 00 61 62 63 64 65 00 00 00

変数a の 4バイト、変数b の 8バイトのそれぞれが、それぞれのバイトの範囲内で逆向きに保存されます。変数c の値は、1バイトずつ書き出しているのなら、エンディアンは無関係なので(1バイトしかないなら、バイトの順序という考え方自体がない)、変化しません。


実際にプログラムを作って確認してみます。

テキスト形式で保存する場合は次のようになります。

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

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

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

    ofs << a << " " << b << " " << c;

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

実行結果(test.txt):

45 527927948 abcde

バイナリ形式で保存する場合は次のようになります。

#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 {45};
    std::int64_t b {527927948LL};
    char c[8] {"abcde"};

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

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

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

2D 00 00 00 8C 8A 77 1F 00 00 00 00 61 62 63 64 65 00 00 00

バイナリ形式でファイルへ書き込むには、std::ofstream の変数を初期化するとき、第2引数に std::ios_base::out | std::ios_base::binary を指定します。std::ios_base::binary があることで、バイナリ形式で書き込むことを指示しています(本編解説)。

実際の書き込みには、std::ofstream の writeメンバ関数を使用しています(本編解説)。

問題2 (基本★)

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


テキスト形式の場合は、次のようになります。

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

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

    std::int32_t a {};
    std::int64_t b {};
    char c[8] {};
    ifs >> a >> b >> c;

    std::cout << a << " " << b << " " << c << "\n";

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

test.txt の内容:

45 527927948 abcde

実行結果:

45 527927948 abcde

次にバイナリ形式の場合です。test.bin のデータフォーマットを知っていれば(先頭から 4バイトの整数、8バイトの整数、8バイト分の文字列がある)、読み込みのプログラムを次のように実装できます(本編解説)。

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

int main()
{
    constexpr auto path = "test.bin";
    std::ifstream ifs(path, std::ios_base::out | 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[8] {};
    ifs.read(reinterpret_cast<char*>(&a), sizeof(a));
    ifs.read(reinterpret_cast<char*>(&b), sizeof(b));
    ifs.read(c, sizeof(c));

    std::cout << a << " " << b << " " << c << "\n";

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

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

2D 00 00 00 8C 8A 77 1F 00 00 00 00 61 62 63 64 65 00 00 00

実行結果:

45 527927948 abcde

ファイル側のエンディアンと、実行環境のエンディアンが異なる場合は、読み込み後の変換(バイトスワッピング)が必要です(本編解説)。

問題3 (応用★★)

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


ファイルのコピーは、ファイルの中身のバイト列をそっくりそのまま、ほかのファイルに書き出せば実現できます。バイナリ形式で取り扱わないと、改行文字の変換が起きたり、ファイル終端で処理が止まってしまったりします。

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

// ファイルの中身をすべて読み込む
//
// path: ファイルパス
// 戻り値: 読み込んだデータが格納された std::vector<char>。
//         エラー発生時は、空のまま返される。
static std::vector<char> read_file(const std::string& path)
{
    std::ifstream ifs(path, std::ios_base::in | std::ios_base::binary);
    if (ifs.fail()) {
        return {};
    }

    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()) {
        return {};
    }

    ifs.close();
    if (ifs.fail()) {
        return {};
    }

    return input_data;
}

// データをファイルに書き込む
//
// path: ファイルパス
// data: 書き込むデータ
// 戻り値: 成功したら true、失敗したら false
static bool write_file(const std::string& path, const std::vector<char>& data)
{
    std::ofstream ofs(path, std::ios_base::out | std::ios_base::binary);
    if (ofs.fail()) {
        return false;
    }

    std::ostreambuf_iterator<char> it_ofs(ofs);
    std::copy(std::cbegin(data), std::cend(data), it_ofs);
    if (ofs.fail()) {
        return false;
    }

    ofs.close();
    if (ofs.fail()) {
        return false;
    }

    return true;
}

int main()
{
    constexpr auto in_path = "test.bin";
    constexpr auto out_path = "test_copy.bin";

    auto input_data = read_file(in_path);
    if (input_data.empty()) {
        std::cerr << "File read error: " << in_path << "\n";
        std::quick_exit(0);
    }

    if (!write_file(out_path, input_data)) {
        std::cerr << "File write error: " << out_path << "\n";
        std::quick_exit(0);
    }
}

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

2D 00 00 00 8C 8A 77 1F 00 00 00 00 61 62 63 64 65 00 00 00

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

2D 00 00 00 8C 8A 77 1F 00 00 00 00 61 62 63 64 65 00 00 00

実行結果(標準出力):

まず、バイナリ形式での読み込み用に test.bin を開き、中身をすべて std::vector に読み込みます。今回のように、ファイルの中身すべてを読み込めばいい場合は、std::istreambuf_iterator が便利です(本編解説)。

書き込みのほうも、std::vector の中身をすべてそのまま書き出せばいいだけです。これには、std::ostreambuf_iterator が使えます(本編解説)。

【C++17】ファイルのコピーは、std::filesystem::copy_file関数で行えるようになっています1

問題4 (応用★★)

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

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

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


構造体の内容をバイナリ形式で読み書きする場合は、データメンバを1つずつ取り扱うようにします。そうしないと、パディングの影響を受けてしまい、正常な結果を得られない可能性があります(本編解説)。

まず、書き込みは次のようにできます。

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

int main()
{
    struct Student {
        char name[32];         // 名前
        std::uint8_t grade;    // 学年 (1~6)
        std::uint8_t score;    // 得点(0~100)
    };
    std::vector<Student> students {
        {"Ken Tanaka", 4, 89},
        {"Ayumi Satou", 5, 70},
        {"Kouji Oota", 3, 72},
    };

    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::uint32_t student_num {students.size()};
    ofs.write(reinterpret_cast<const char*>(&student_num), sizeof(student_num));

    // 各生徒のデータを書き出す
    for (const auto& student : students) {
        ofs.write(student.name, sizeof(Student::name));
        ofs.write(reinterpret_cast<const char*>(&student.grade), sizeof(Student::grade));
        ofs.write(reinterpret_cast<const char*>(&student.score), sizeof(Student::score));
    }

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

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


03 00 00 00 4B 65 6E 20 54 61 6E 61 6B 61 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 04 59 41 79 75 6D 69 20 53 61 74 6F
75 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 05 46 4B 6F 75 6A 69 20 4F 6F
74 61 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 03 48                  

データ件数が一定でない場合は、ファイルの先頭などにデータ件数の情報も書き出しておくと、読み込むときに便利です。

書き出された test.bin を読み込むプログラムは、次のように書けます。

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

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

    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::uint32_t student_num {};
    ifs.read(reinterpret_cast<char*>(&student_num), sizeof(student_num));

    // 各生徒のデータを読み込む
    for (std::uint32_t i {0}; i < student_num; ++i) {
        Student student {};
        ifs.read(student.name, sizeof(Student::name));
        ifs.read(reinterpret_cast<char*>(&student.grade), sizeof(Student::grade));
        ifs.read(reinterpret_cast<char*>(&student.score), sizeof(Student::score));
        students.push_back(student);
    }

    // 確認
    for (const auto& student : students) {
        std::cout << student.name << " " << static_cast<unsigned int>(student.grade) << " " << static_cast<unsigned int>(student.score) << "\n";
    }

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

実行結果:

Ken Tanaka 4 89
Ayumi Satou 5 70
Kouji Oota 3 72


参考リンク



更新履歴




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