バイナリファイルの読み書き 解答ページ | Programming Place Plus C言語編 第42章

トップページC言語編第42章

問題① 🔗

問題① 手元にある適当なファイルをいくつかバイナリエディタに読み込ませて、中身を確認してみてください。


これは本当に何でも構わないので、いろいろなタイプのファイルを読み込ませてみてください。

ビットマップファイル(.bmp) のような画像ファイル、音声ファイル、動画ファイル、プログラムの実行ファイル、 Excel や Word のファイルなどなど、いろいろあります。

こういったさまざまなファイルの中には案外、テキスト形式として読めるようなものもありますし、部分的にだけ読めるものもあるでしょう。 まったく読めないものも当然あります。 しかしどんなファイルであっても、単なるバイトの塊に過ぎないのだという認識を持って欲しいと思います。

問題② 🔗

問題② 次のような構造体型があります。

typedef struct NameList_tag {
    size_t name_length; // name の文字数 (終端文字を除く)
    char* name;        // 名前
    int age;           // 年齢
} NameList;

この型で表現された以下のデータを、バイナリ形式でファイルへ出力するプログラムおよび、 ファイルから入力を受け取るプログラムを作成してください。

static const NameList name_list = {
    4, "John", 29
};


まず、出力するプログラムを作成します。

#include <stdio.h>
#include <stdlib.h>

typedef struct NameList_tag {
    size_t name_length; // name の文字数 (終端文字を除く)
    char* name;        // 名前
    int age;           // 年齢
} NameList;


int main(void)
{
    static const NameList name_list = {
        4, "John", 29,
    };

    FILE* fp = fopen("NameList.bin", "wb");
    if (fp == NULL) {
        fputs("ファイルオープンに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }

    if (fwrite(&name_list.name_length, sizeof(name_list.name_length), 1, fp) < 1) {
        fputs("ファイルへの書き込みに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }
    if (fwrite(name_list.name, sizeof(*name_list.name), name_list.name_length, fp) < name_list.name_length) {
        fputs("ファイルへの書き込みに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }
    if (fwrite(&name_list.age, sizeof(name_list.age), 1, fp) < 1) {
        fputs("ファイルへの書き込みに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }

    if (fclose(fp) == EOF) {
        fputs("ファイルクローズに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }
}

実行結果(標準出力):

実行結果(NameList.bin のテキスト表現):

    John    

文字列を char* で表現していることに注意が必要です。 通常、char[16] のような固定長の方が処理は行いやすいですが、 文字列の長さが大きく増減することがあるようなデータの場合、ポインタで扱うこともあります。

また、名前の長さは専用のメンバに保存されています。 “John” という名前に対して、長さは 4 としているので、ヌル文字を含んでいません。

次に、入力側のプログラムです。

#include <stdio.h>
#include <stdlib.h>

typedef struct NameList_tag {
    size_t name_length; // name の文字数 (終端文字を除く)
    char* name;        // 名前
    int age;           // 年齢
} NameList;


int main(void)
{
    NameList name_list;

    FILE* fp = fopen("NameList.bin", "rb");
    if (fp == NULL) {
        fputs("ファイルオープンに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }

    // 名前の文字数
    if (fread(&name_list.name_length, sizeof(size_t), 1, fp) < 1) {
        fputs("ファイルからの読み込みに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }

    // 領域を動的確保して、そこへ名前を読み込む
    name_list.name = malloc(sizeof(char) * name_list.name_length + 1);
    if (fread(name_list.name, sizeof(*name_list.name), name_list.name_length, fp) < name_list.name_length) {
        fputs("ファイルからの読み込みに失敗しました。\n", stderr);
        goto error;
    }
    name_list.name[name_list.name_length] = '\0';

    // 年齢
    if (fread(&name_list.age, sizeof(int), 1, fp) < 1) {
        fputs("ファイルからの読み込みに失敗しました。\n", stderr);
        goto error;
    }

    printf("%s (%d)\n", name_list.name, name_list.age);

    free(name_list.name);

    if (fclose(fp) == EOF) {
        fputs("ファイルクローズに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }

    return 0;

error:
    fclose(fp);
    free(name_list.name);
    return EXIT_FAILURE;
}

実行結果(標準出力):

John (29)

文字列の長さが一定であるとは限らないため、malloc関数を使って必要な分だけの領域を確保して、そこに読み込むようにします。 バイナリファイルにはヌル文字が書き込まれていないので、自分で付加するようにします。

もし、文字列の長さが一致だと分かっている場合は、char* をやめて char[128] のような固定長にした方が、 簡単で効率的なこともあるでしょう。

問題③ 🔗

問題③ リトルエンディアンとビッグエンディアンを相互に変換するためには、どのようにすれば良いか考えてください。


たとえば、0x1234 という 2バイトのデータは、リトルエンディアンでは「0x34 0x12」の順番で並び、ビッグエンディアンでは「0x12 0x34」の順番で並びます。

0x12345678 という 4バイトのデータなら、リトルエンディアンでは「0x78 0x56 0x34 0x12」と並び、ビッグエンディアンでは「0x12 0x34 0x56 0x78」と並びます。

つまり、逆順になるように、1バイト単位で並び変えれば相互に変換できます。 これは、最上位の 1バイトと最下位の 1バイトとを入れ替え、次にその内側の 1バイト同士を入れ替え…ということを繰り返しても同様の結果になります。

なお、リトルエンディアンからビッグエンディアンであっても、ビッグエンディアンからリトルエンディアンであっても、やりかたはまったく同じで良い訳です。 ですから、同じ操作を2回行えば、また元のエンディアンに戻せます。

これをプログラミングする方法ですが、第49章で説明するビット操作を理解していた方が簡単なので、 ここでは無理に実装する必要はありませんが、現時点の知識でやるとすれば、次のようになります。

#include <stdio.h>

typedef unsigned short u16;
typedef unsigned int u32;

u16 change_engian16(u16 n);
u32 change_engian32(u32 n);

int main(void)
{
    u16 n1 = 0x1234;
    u32 n2 = 0x12345678;

    n1 = change_engian16(n1);
    printf("%x\n", n1);
    n1 = change_engian16(n1);
    printf("%x\n", n1);

    n2 = change_engian32(n2);
    printf("%x\n", n2);
    n2 = change_engian32(n2);
    printf("%x\n", n2);
}

/*
    16bit の整数値のエンディアンを、リトル・ビッグ間で変換する。
*/
u16 change_engian16(u16 n)
{
    u16 r = n / 0x100;
    r += (n % 0x100) * 0x100;
    return r;
}

/*
    32bit の整数値のエンディアンを、リトル・ビッグ間で変換する。
*/
u32 change_engian32(u32 n)
{
    u32 r = ((n / 0x1000000) % 0x100);
    r += ((n / 0x10000) % 0x100) * 0x100;
    r += ((n / 0x100) % 0x100) * 0x10000;
    r += (n % 0x100) * 0x1000000;
    return r;
}

実行結果:

3412
1234
78563412
12345678

要は、変換元の値から1バイト分を抜き出して、変換後の値の適切なバイトへ入れています。 実装内容が異なってしまうので、16ビット版と 32ビット版とで関数を分けています。 short型が 16ビット、int型が 32ビットであるという前提になっているので、そうでない環境では、 typedef のところを変更してください。

問題④ 🔗

問題④ 小さめで単色のビットマップのファイルを用意し、その内容をバイナリエディタで確認してください。 色を変えて再度確認し、内容がどう変化するか調べてください。 その結果から、どんなことが分かりますか?


一般の人が、未知のフォーマットを解読するような場面はあまり無いかもしれませんが、開発の現場では、そういうこともたまにはあるでしょう。未知のフォーマットであっても、実験を繰り返すことで、ある程度は類推できる部分もあるものです。

まず、非常に小さなビットマップを用意します。今回は、4x4 ピクセルで、赤色で塗りつぶした画像を用意してみました。これをバイナリエディタで確認すると、以下のように表示されました。

42 4D 66 00 00 00 00 00 00 00 36 00 00 00 28 00
00 00 04 00 00 00 04 00 00 00 01 00 18 00 00 00
00 00 30 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 24 1C ED 24 1C ED 24 1C ED 24
1C ED 24 1C ED 24 1C ED 24 1C ED 24 1C ED 24 1C
ED 24 1C ED 24 1C ED 24 1C ED 24 1C ED 24 1C ED
24 1C ED 24 1C ED

次に、同じ画像を青色で塗りつぶしてから再確認します。

42 4D 66 00 00 00 00 00 00 00 36 00 00 00 28 00
00 00 04 00 00 00 04 00 00 00 01 00 18 00 00 00
00 00 30 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 CC 48 3F CC 48 3F CC 48 3F CC
48 3F CC 48 3F CC 48 3F CC 48 3F CC 48 3F CC 48
3F CC 48 3F CC 48 3F CC 48 3F CC 48 3F CC 48 3F
CC 48 3F CC 48 3F

この2つの結果を見比べると、「24 1C ED」という連続した並びが「CC 48 3F」という並びに変化しており、 それ以外の箇所に変化は見られません。画像に対して行った変更は、色を変えただけですから、「バイナリデータの変化した内容」=「色の変化の影響」と考えて良さそうです。

また、「24 1C ED」や「CC 48 3F」という連続した並びは、全部で 16組あります。画像が 4x4ピクセルの大きさなので、1ピクセル分の情報が「24 1C ED」や「CC 48 3F」であろうことも類推できます。

バイナリデータ内の変化がない部分には、恐らく、画像のサイズや色数などの情報が含まれていると考えられます。

なお、画像の幅や高さの選び方によっては、1ピクセル分の情報の並びの中に「00」が何個か挿入される可能性があります。これは、パディング(詰め物)と呼ばれる余分なデータです。パディングは、処理の効率化を図るために使われているものですが、今回のケースでは、幅や高さが 4 の倍数になっていれば、パディングの挿入は起こりません。

問題⑤ 🔗

問題⑤ 問題④の結果を踏まえ、赤色の単色画像を読み込んで、青色の単色画像に変換して出力するプログラムを作成してください。


問題⑬で用意した、4x4ピクセルの赤色の画像を、青色画像に変換してみます。

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char* argv[])
{
    const unsigned char pixel[] = {0xCC, 0x48, 0x3F};  // 青色のピクセルデータ?

    FILE* fp = fopen("test.bmp", "rb+");
    if (fp == NULL) {
        fprintf(stderr, "ファイルのオープンに失敗しました。\n");
        exit(EXIT_FAILURE);
    }


    // ピクセルごとの色情報と思われる位置までシーク
    if (fseek(fp, 54, SEEK_SET) != 0) {
        fprintf(stderr, "ファイルポジションの移動に失敗しました。\n");
        exit(EXIT_FAILURE);
    }

    // 青色で 16ピクセル分のデータで上書き
    for (int i = 0; i < 16; ++i) {
        if (fwrite(pixel, sizeof(pixel), 1, fp) < 1) {
            fprintf(stderr, "ファイルへの書き込みに失敗しました。\n");
            exit(EXIT_FAILURE);
        }
    }

    if (fclose(fp) == EOF) {
        fputs("ファイルクローズに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }
}

あくまでも問題④を踏まえてのものなので、いくつかの前提や仮定を元にしていますが、これで確かに、青色の画像に変換できました。

このプログラムが実用的なものかどうかということより、 こういうことが可能であることを理解して欲しいというのが、この問題の目的です。

問題⑥ 🔗

問題⑥ ファイルの内容を、バイナリエディタのように整形して出力するプログラムを作成してください。


たとえば、次のようになります。読み込むファイルは適当に用意して構いません。ここでは、問題⑤ で出力された test.bmp を使ってみます。

#include <stdio.h>
#include <stdlib.h>

#define NUM_OF_VALUE_PER_LINE  (16)   // 1行に出力するデータの個数

int main(void)
{
    FILE* fp = fopen("test.bmp", "rb");
    if (fp == NULL) {
        fputs("ファイルオープンに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }

    unsigned char data[NUM_OF_VALUE_PER_LINE];

    for (;;) {

        // 1行に出力する内容を読み込む
        size_t read_size = fread(data, sizeof(unsigned char), NUM_OF_VALUE_PER_LINE, fp);
        if (read_size < NUM_OF_VALUE_PER_LINE) {
            if (feof(fp)) {
                break;
            }
            else if (ferror(fp)) {
                fputs("読み込み中にエラーが発生しました。\n", stderr);
                exit(EXIT_FAILURE);
            }
        }

        // 16進数2桁で、1バイトずつ出力する
        for (size_t i = 0; i < read_size; ++i) {
            printf("%02X ", data[i]);
        }
        printf("\n");
    }

    if (fclose(fp) == EOF) {
        fputs("ファイルクローズに失敗しました。\n", stderr);
        exit(EXIT_FAILURE);
    }
}

実行結果:

42 4D 66 00 00 00 00 00 00 00 36 00 00 00 28 00
00 00 04 00 00 00 04 00 00 00 01 00 18 00 00 00
00 00 30 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 CC 48 3F CC 48 3F CC 48 3F CC
48 3F CC 48 3F CC 48 3F CC 48 3F CC 48 3F CC 48
3F CC 48 3F CC 48 3F CC 48 3F CC 48 3F CC 48 3F

大抵のバイナリエディタは、1行に 16バイト分のデータを出力するので、それに倣った出力結果にしています。

していることは、16バイト分のデータを読み込んでは、それを printf関数の “%x” や “%X” の変換指定子を使って整形しているだけです。



参考リンク 🔗


更新履歴 🔗

≪さらに古い更新履歴≫

 第48章の練習問題⑬⑭⑮を移動してきて、練習問題④⑤⑥とした。

 全面的に文章を見直し、修正を行った。

 文章中の表記を統一(bit、Byte -> ビット、バイト)

 flose関数の戻り値もチェックするようにした。

 新規作成。



第42章のメインページへ

C言語編のトップページへ

Programming Place Plus のトップページへ



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