C言語のきほん|構造体は代入できるが比較できない

構造体はまとめてコピーできる。でも「同じかどうか」は、ひとつずつ確かめる必要がある。

構造体を学んでいくと、「変数に入れる」「配列でまとめる」といった基本の使い方に少しずつ慣れてきます。
すると次に気になってくるのが、構造体同士をコピーできるのか、そして構造体同士を比べられるのかという点です。

この2つは似ているようで、C言語では扱いがはっきり違います。
同じ型の構造体であれば、代入演算子を使って内容をまとめてコピーできます。
一方で、== 演算子を使って構造体全体をそのまま比較することはできません。

ここは最初のうちは少し不思議に感じるかもしれません。
「代入できるなら比較もできそう」と思いやすいからです。
でもC言語では、構造体のコピーと比較は別のルールで扱われています。

この違いを理解すると、

  • 構造体の値を別の変数へ移す方法
  • 構造体の内容が一致しているか調べる方法
  • 文字列メンバを含む構造体をどう比較するか
  • パディングの影響で単純なメモリ比較が危険な理由

といった大事なポイントが見えてきます。

今回は、構造体の代入はできることと、直接比較はできないことの違いを、やさしく丁寧に整理していきましょう。

構造体の代入と比較は別物

まず最初に、いちばん大事な結論をはっきり押さえておきましょう。

操作できるか説明
構造体同士の代入できる同じ型なら = で丸ごとコピーできる
構造体同士の == 比較できない構造体全体を直接比較することはできない

この違いが、この記事の中心です。

たとえば Person 型の構造体が2つあったとき、

a = b;

はできます。
でも、

if (a == b)

はできません。

この差を、ここから順番に見ていきます。

なぜ「代入できる」のか

C言語では、同じ型の構造体であれば、代入演算子 = を使って内容を丸ごとコピーできます。

たとえば、学生情報や商品情報のような構造体を考えると、1件分のデータを別の変数へそのまま移したい場面はよくあります。
そういうとき、構造体ではメンバを1つずつ書かなくても、まとめて代入できます。

これはとても便利です。
構造体が複数のメンバを持っていても、同じ型なら1回の代入でコピーできるからです。

構造体の代入のイメージ

構造体の代入は、右辺の構造体の内容を左辺の構造体へ丸ごと写すイメージです。

たとえば、

  • 会員番号
  • 氏名

を持つ構造体があるとき、右辺の会員情報が左辺にコピーされます。

つまり、次のようなイメージです。

代入前代入後
record に何らかの値が入っているselected の内容が record にコピーされる

このとき、int 型のメンバも、char 型配列のメンバも、構造体全体としてまとめてコピーされます。

サンプルプログラムで代入を確認する

ここでは、商品情報の例に説明します。

ファイル名:15_6_1.c

#include <stdio.h>

// 商品情報を表す構造体
typedef struct {
    int code;         // 商品番号
    char name[20];    // 商品名
} Item;

int main(void)
{
    // 構造体の変数を宣言
    Item item;

    // 構造体配列を初期化
    Item items[3] = {
        {101, "ノート"},
        {102, "ペン"},
        {103, "消しゴム"}
    };

    // 先頭の商品情報をコピー
    item = items[0];

    // コピーした内容を表示
    printf("選ばれた商品:%d %s\n", item.code, item.name);

    return 0;
}

実行結果例

選ばれた商品:101 ノート

代入のコードを読み解く

このプログラムで大事なのは、次の1行です。

item = items[0];

この1行で、items[0] に入っている

  • code
  • name

の両方が item にコピーされます。

つまり、次のようなことがまとめて行われています。

  • item.code に items[0].code が入る
  • item.name に items[0].name がコピーされる

構造体の代入では、こうしたメンバ単位のコピーを、利用者が1つずつ書かなくても済むのが便利なところです。

代入なら文字列メンバもまとめてコピーされる

ここは少し安心できるポイントです。
構造体のメンバに char 型配列が含まれていても、構造体の代入ならまとめてコピーされます。

たとえば、普段の文字列配列同士では、

name1 = name2;

のような代入はできません。
でも、構造体全体の代入なら話は別です。

item = items[0];

と書けば、その中にある char 型配列の name も含めて構造体全体がコピーされます。

これは、構造体代入の便利さを実感しやすいポイントです。

では、なぜ比較はできないのか

ここで疑問が出てきます。
「代入できるなら、比較もできそうなのに」と感じますよね。

でも、C言語の標準では、構造体同士を == や != で直接比較することはできません。

たとえば、次のようなコードはエラーになります。

if (item == items[0]) {
    printf("同じ商品です。\n");
}

なぜかというと、C言語では構造体全体に対して「等しい」という比較演算を標準で定義していないからです。
つまり、構造体はコピーの対象にはできるが、比較演算子の対象にはできないということです。

比較したいときはメンバごとに調べる

構造体の内容が同じかどうかを確かめたいなら、各メンバを個別に比較する必要があります。

たとえば、商品番号と商品名の2つを持つ構造体なら、

  • 商品番号が同じか
  • 商品名が同じか

を別々に確認し、その両方が一致しているときに「同じ」と判断します。

この考え方はとても大切です。
構造体全体を1回で比べるのではなく、中身を順番に確認するわけです。

サンプルプログラムで比較を確認する

ここでは、会員情報の例に説明します。
文字列比較には strcmp を使います。

ファイル名:15_6_2.c

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

// 会員情報を表す構造体
typedef struct {
    int id;           // 会員番号
    char name[20];    // 氏名
} Member;

int main(void)
{
    // 比較したい会員情報
    Member target = {2, "佐藤花子"};

    // 会員一覧
    Member members[3] = {
        {1, "田中太郎"},
        {2, "佐藤花子"},
        {3, "鈴木一郎"}
    };

    for (int i = 0; i < 3; i++) {
        // メンバごとに比較する
        if ((target.id == members[i].id) &&
            (strcmp(target.name, members[i].name) == 0)) {
            printf("members[%d]が一致しました。\n", i);
        }
    }

    return 0;
}

実行結果例

members[1]が一致しました。

比較のコードを読み解く

大事なのはこの if 文です。

if ((target.id == members[i].id) &&
    (strcmp(target.name, members[i].name) == 0))

ここでは、構造体全体を一気に比べているのではなく、メンバごとに比較しています。

比較している内容

比較対象比較方法
id== で比較
namestrcmp で比較

この2つが両方とも一致したときだけ、「同じ会員情報」と判断しています。

文字列メンバには == を使わない

ここは特に大切です。
構造体の比較では文字列メンバがよく出てきますが、文字列は == で比較しません。

たとえば次のような書き方は適切ではありません。

target.name == members[i].name

char 型配列や文字列は、内容を比べるには strcmp を使います。

strcmp(target.name, members[i].name) == 0

この書き方は、「2つの文字列が同じ内容なら 0 を返す」という strcmp の性質を利用しています。

strcmp の結果

結果意味
0同じ文字列
0以外異なる文字列

そのため、

strcmp(a, b) == 0

で「a と b が同じ」と判断できます。

!strcmp という書き方について

教材やサンプルでは、次のような書き方を見ることがあります。

!strcmp(a, b)

これは、

strcmp(a, b) == 0

とほぼ同じ意味です。

strcmp が 0 を返すとき、! を付けると真になります。
ただ、学習の初期段階では意味が見えやすいので、

strcmp(a, b) == 0

の方が分かりやすいことも多いです。

ここは、図で対比させるとかなり理解しやすいです。

この図では、構造体の代入と比較の違いがひと目で分かります。
左側では、代入によって構造体の内容がそのままコピーされる様子を示しています。
右側では、== を使った直接比較ができないことをバツ印で表しています。
構造体は「まとめて移すことはできるが、まとめて比べることはできない」という性質を、この図で直感的につかめます。

memcmp を使えば比べられそうに見えるが注意が必要

ここで、「じゃあメモリ全体を比べればよいのでは」と考えるかもしれません。
たしかに、memcmp を使えば見た目には構造体全体を比較できそうです。

ですが、これは安全とはいえません。
理由は、構造体にはパディングが入ることがあるからです。

パディングとは何か

パディングとは、構造体のメンバの間や末尾に入るつめもののことです。
これは、CPU が効率よくメモリアクセスできるようにするために、処理系が自動的に挿入することがあります。

たとえば、次のような構造体を考えます。

typedef struct {
    int no;
    char name[14];
} Person;

見た目では、

  • int が 4 バイト
  • char name[14] が 14 バイト

なので、合計 18 バイトに見えます。

でも実際には、処理系によっては末尾に 2 バイトのパディングが入って、20 バイトになることがあります。

なぜパディングが問題になるのか

memcmp は、メモリ領域全体をバイト単位で比較します。
すると、本来のメンバの値が同じでも、パディング部分の内容が異なっていると、「違う」と判断される可能性があります。

つまり、

  • 見た目のデータ内容は同じ
  • でもメモリ全体は一致しない

ということが起こりえます。

そのため、構造体の比較を memcmp に頼るのは危険です。

この図では、構造体のメンバ以外にパディングが入ることがある様子を表しています。
見た目には no と name だけで構成されているように見えても、実際のメモリ上では末尾に余分な領域が追加されることがあります。
そのため、構造体を単純にメモリ全体で比較すると、本来比較したいメンバ以外の部分まで判定に含まれてしまうことがあります。

代入と比較の違いを整理

ここまでの内容を、表で整理しておくと理解が安定します。

項目構造体の代入構造体の比較
できるかできる直接はできない
書き方a = b;a == b; は不可
内容同じ型なら丸ごとコピー各メンバを個別に比較する
文字列メンバ構造体代入ならまとめてコピーされるstrcmp を使う必要がある
注意点型が同じである必要があるmemcmp に安易に頼らない

よくある勘違い

このテーマでは、初心者が引っかかりやすいポイントがいくつかあります。

代入できるなら比較もできると思ってしまう

これはとても自然な発想ですが、C言語ではそうなっていません。
代入は可能でも、== による直接比較はできません。

文字列メンバを == で比べてしまう

文字列は strcmp を使って内容を比べます。
これは構造体比較でも同じです。

memcmp なら簡単だと思ってしまう

パディングの影響があるため、構造体の意味的な比較には向いていません。
安全に比較するには、必要なメンバを個別に比較するのが基本です。

実践的にはどう考えるとよいか

実際のプログラムでは、構造体の比較処理が何度も必要になることがあります。
そんなときは、毎回 if 文に長い比較式を書くより、比較用の関数を作る考え方も有効です。

たとえば Person 型なら、

  • 番号が同じか
  • 氏名が同じか

をチェックする関数を用意しておくと、コードがすっきりします。

この段階ではまず、「構造体そのものは比較できないので、比較したいならメンバごとに確認する」という基本をしっかり押さえておくことが大切です。

この段階で押さえておきたいポイント

ポイント内容
構造体の代入同じ型なら = で丸ごとコピーできる
構造体の比較== や != で直接比較はできない
比較方法メンバごとに個別に比較する
文字列メンバstrcmp を使って比較する
memcmp の注意パディングの影響で正しい比較にならないことがある
パディングメモリアクセス効率のために挿入される余分な領域