C言語で構造体を比較する【memcmpの危険性】

384, 2022-01-06

目次

C言語で構造体を比較する

structure

C言語では構造体を使うことができます。
構造体を使っていると、構造体同士を比較したい時があります
この記事では構造体同士の比較方法について解説します

結論から言うと「memcmpは使うな!」ということになります。

構造体の比較関数を作る

twins

構造体同士の比較を行う場合は、構造体用の比較関数を自前で作ります。

#include <stdio.h>
#include <stdbool.h>
#include <string.h>

// 動物を表す構造体
struct Animal {
    int age;  // 年齢
    char name[40];  // 名前
};

// Animal用の比較関数
// lhs ... 比較するAnimalのポインタ
// rhs ... 比較するAnimalのポインタ
// 返り値 ... lhsとrhsが一致していればtrue, でなければfalse
bool animal_equal(const struct Animal *lhs, const struct Animal *rhs) {
    return lhs->age == rhs->age &&
           strcmp(lhs->name, rhs->name) == 0;
}

int main(void) {
    struct Animal cat = { 20, "Tama" };  // cat変数の定義
    struct Animal dog = cat;  // dogにcatをコピー

    // catとdogを比較
    if (animal_equal(&cat, &dog)) {
        puts("catとdogは等しいです。");
    }

    dog.age = 30;  // dogのデータを変更

    // catとdogを比較
    if (!animal_equal(&cat, &dog)) {
        puts("catとdogは等しくないです。");
    }

    return 0;
}

上記のコードを実行すると↓のような結果になります。

catとdogは等しいです。
catとdogは等しくないです。

上記のコードにあるanimal_equal()struct Animal構造体のポインタ同士を比較します。
引数のポインタのデータが一致していればtrueを返し、不一致であればfalseを返します。

この方法はけっこうめんどくさい方法ではあります。
構造体のメンバが増減したら比較関数も更新しないといけません
ですがこれが構造体の比較の唯一の方法です。

memcmpを使った構造体の比較

memory

string.hをインクルードすると使えるmemcmp()関数は、構造体などのデータをバイト列として比較することができます。
これを使えば構造体の比較ができそうですが、この関数を使うのは誤りです

なぜかというと、構造体にはパディングという詰め物が入る場合があります。
これはコンパイラが調整して勝手に入れるものです。
パディングが入った構造体の比較をmemcmp()で行うと、パディングが原因で比較が正常に行われない場合があります。
これはパディング部分の値が未規定なためです。
そのためmemcmp()を構造体の比較に使うのは非常にデンジャーです。

memcmp()を構造体の比較に使うのはデンジャー

やめておいた方が無難

構造体のパディングを見てみる

padding

構造体のパディングを見る方法もあります。
これを見るとmemcmp()が意図しない動作になる理由がわかるかと思います。

#include <stdio.h>

struct Animal {
    int age;  // 4バイト
    char name[1];  // 1バイト
    // 4 + 1 = 5バイトでパディングが入って8バイトになる
};

// バイト列を出力する関数
void dump(void *p, size_t size) {
    void *end = p + size;
    for (int i = 1; p < end; i += 1, p += 1) {
        unsigned char byte = * (unsigned char *) p;
        printf("%dバイト目: %d\n", i, byte);
    }
}

int main(void) {
    // intのサイズ
    printf("sizeof int = %d\n", sizeof(int));

    // struct Animalのサイズ
    printf("sizeof Animal = %d\n", sizeof(struct Animal));

    // catのバイト列を見る
    struct Animal cat = { 20, "" };
    dump(&cat, sizeof cat);

    return 0;
}

上記のコードをコンパイルして実行すると↓のような結果になります。

sizeof int = 4
sizeof Animal = 8
1バイト目: 20
2バイト目: 0
3バイト目: 0
4バイト目: 0
5バイト目: 0
6バイト目: 0
7バイト目: 0
8バイト目: 0

上記のコードのdump()関数はオブジェクトをバイト列で出力する関数です。
上の出力結果を見ると、構造体Animalにしっかりとパディングが入ってるのがわかります。
memcmp()は比較を行う時にこのパディングの部分も比較しますので、パディングの値が未規定と言うことは期待した動作をしないことがあるということになります。

目に見えない働き者

パディングに光を与えたまえ

おわりに

end

今回はC言語の構造体の比較方法について解説しました。
C言語の構造体の比較は自前で比較用の関数を用意するのがセオリーです。
memcmp()を使うのはやめておきましょう。

比較されることに疲れました

比較されない世界に生きたい