0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

C言語でデータをまとめて扱う|構造体・typedef・enum・構造体配列

0
Last updated at Posted at 2026-06-26

C言語を基礎から順に学びたい場合は、C言語 学習ロードマップ を参照してください。

前提として、C言語のポインタ入門を読んでいると理解しやすくなります。

商品、社員、センサー情報のようなデータは、1つの値だけでは表せません。

たとえば商品なら、次のように複数の情報があります。

商品
├ 商品ID
├ 商品名
├ 単価
├ 在庫数
└ 販売状態

これらを別々の変数や別々の配列で持つと、データの対応関係を崩しやすくなります。

C言語では、関連する複数の値を1件のデータとしてまとめるために、構造体を使います。

この記事では、構造体を中心に、次の内容を学びます。

  • struct で複数の値を1件にまとめる
  • typedef で型名を短く分かりやすくする
  • enum で状態や種類を意味のある名前で表す
  • 構造体の配列で複数件を扱う
  • sizeof で配列の要素数を自動取得する
  • 構造体がメモリ上に置かれる順番と、パディングの考え方
  • memsetmemcpy の役割の違い
  • const で「変更しない」ことを表す
  • 構造体を関数へ渡して表示・検索する

前提

この記事は、次の内容を学んだ人を対象にしています。

  • 変数、条件分岐、繰り返し、配列、文字列
  • 関数
  • ポインタの基本
  • NULL の意味

先に次の記事を読んでください。

C言語の基本|初めてのプログラムを動かしながら、書き方と考え方を学ぶ

C言語のポインタ入門|「値」と「場所」を分けて考える

main 関数の引数や argcargv が気になる場合は、補足として次の記事を参照してください。

C言語のmain関数入門|int main(void)・argc・argv・void mainの違い


1. 構造体が必要になる場面

構造体を使わずに、1件の商品を表すとします。

char product_name[] = "りんご";
int unit_price = 120;
int stock_count = 10;
int status = 1;

1件だけなら問題ありません。

しかし、商品が複数になると、次のように対応関係を維持する必要があります。

char product_names[][32] = { "りんご", "みかん", "牛乳" };
int unit_prices[] = { 120, 100, 210 };
int stock_counts[] = { 10, 0, 5 };
int statuses[] = { 1, 0, 1 };

この書き方では、次の関係が常に正しいことを自分で守らなければなりません。

product_names[0]
unit_prices[0]
stock_counts[0]
statuses[0]
↓
すべて「りんご」の情報

項目が増えるほど、ずれや書き間違いが起きやすくなります。

構造体を使うと、商品1件に必要な情報をまとめられます。

Product
├ id
├ name
├ unit_price
├ stock_count
└ status

2. struct で複数の値をまとめる

次のコードは、商品を表す構造体です。

struct Product
{
    int id;
    char name[32];
    int unit_price;
    int stock_count;
};

struct Product は、次の4つの値を持つデータの形を定義しています。

項目 意味
id int 商品ID
name char[32] 商品名
unit_price int 単価
stock_count int 在庫数

構造体の中にある各項目を、メンバーと呼びます。

構造体の変数を作るときは、次のように書きます。

struct Product apple;

メンバーへ値を入れるときは、. を使います。

apple.id = 101;
apple.unit_price = 120;
apple.stock_count = 10;

ただし、毎回 struct Product と書くのは少し長くなります。

そこで、次に typedef を使います。


3. typedef で型名を分かりやすくする

typedef は、型に別名を付ける書き方です。

構造体では、次の形がよく使われます。

typedef struct
{
    int id;
    char name[32];
    int unit_price;
    int stock_count;
} Product;

これで、Product を型名として使えます。

Product apple;

struct Product apple; と書くよりも、読みやすくなります。

以降は、typedef を使ったこの形で説明します。


4. enum で状態を意味のある名前で表す

販売中か、販売停止中かのような状態を、数値だけで管理するとします。

int status = 1;

この 1 が何を意味するのかは、コードだけでは分かりません。

enum を使うと、状態に名前を付けられます。

typedef enum
{
    PRODUCT_STATUS_NOT_FOR_SALE = 0,
    PRODUCT_STATUS_ON_SALE = 1
} ProductStatus;

これで、販売状態は次のように書けます。

ProductStatus status = PRODUCT_STATUS_ON_SALE;

数値ではなく意味のある名前を使うため、コードを読んだときに意図が分かりやすくなります。

product.status = PRODUCT_STATUS_ON_SALE;

5. 商品を表す型を完成させる

ここまでの内容を組み合わせて、商品を表す Product 型を作ります。

product_basic.c というファイルを作成してください。

#include <stdio.h>

// 商品の販売状態を表す型です。
typedef enum
{
    PRODUCT_STATUS_NOT_FOR_SALE = 0,
    PRODUCT_STATUS_ON_SALE = 1
} ProductStatus;

// 商品1件を表す型です。
typedef struct
{
    int id;
    char name[32];
    int unit_price;
    int stock_count;
    ProductStatus status;
} Product;

int main(void)
{
    // Product 型の変数を1つ作ります。
    Product apple = {
        101,
        "りんご",
        120,
        10,
        PRODUCT_STATUS_ON_SALE
    };

    // . を使って、構造体のメンバーを読み取ります。
    printf("商品ID: %d\n", apple.id);
    printf("商品名: %s\n", apple.name);
    printf("単価: %d円\n", apple.unit_price);
    printf("在庫数: %d個\n", apple.stock_count);

    if (apple.status == PRODUCT_STATUS_ON_SALE)
    {
        printf("販売状態: 販売中\n");
    }
    else
    {
        printf("販売状態: 販売停止中\n");
    }

    return 0;
}

コンパイルして実行します。

gcc -std=c17 -Wall -Wextra -Wpedantic product_basic.c -o product_basic
./product_basic

実行結果です。

商品ID: 101
商品名: りんご
単価: 120円
在庫数: 10個
販売状態: 販売中

初期化の対応関係

次の部分は、構造体のメンバー順に初期値を指定しています。

Product apple = {
    101,
    "りんご",
    120,
    10,
    PRODUCT_STATUS_ON_SALE
};

対応は次のとおりです。

順番 メンバー
1 id 101
2 name "りんご"
3 unit_price 120
4 stock_count 10
5 status PRODUCT_STATUS_ON_SALE

メンバーが増えたときに順番を間違えないよう、初期化は見やすく改行して書くと安全です。


6. 構造体の配列で複数の商品を扱う

Product を配列にすると、複数の商品をまとめて扱えます。

Product products[] = {
    { 101, "りんご", 120, 10, PRODUCT_STATUS_ON_SALE },
    { 102, "みかん", 100, 0, PRODUCT_STATUS_ON_SALE },
    { 103, "牛乳", 210, 5, PRODUCT_STATUS_NOT_FOR_SALE }
};

図にすると、次のイメージです。

products[0]
├ id: 101
├ name: りんご
├ unit_price: 120
├ stock_count: 10
└ status: 販売中

products[1]
├ id: 102
├ name: みかん
├ unit_price: 100
├ stock_count: 0
└ status: 販売中

7. sizeof で配列の要素数を取得する

配列の要素数を、次のように手書きすることがあります。

int score_count = 5;

この書き方では、配列の要素を増減したときに、件数の修正を忘れる危険があります。

配列を作った同じ場所なら、sizeof を使って要素数を取得できます。

Product products[] = {
    { 101, "りんご", 120, 10, PRODUCT_STATUS_ON_SALE },
    { 102, "みかん", 100, 0, PRODUCT_STATUS_ON_SALE },
    { 103, "牛乳", 210, 5, PRODUCT_STATUS_NOT_FOR_SALE }
};

size_t product_count = sizeof(products) / sizeof(products[0]);

意味は次のとおりです。

配列全体のサイズ
÷
要素1件分のサイズ
=
要素数

size_t は、サイズや件数を表すための型です。

sizeof の結果も size_t になるため、配列の要素数には size_t を使うと自然です。


8. 構造体はメンバー順にメモリへ置かれる

構造体は、関連する値を1つのデータとして扱うための仕組みです。

メモリの観点では、構造体全体は sizeof(構造体の型) バイトの連続した領域として扱われます。

また、構造体のメンバーは、宣言した順にアドレスが大きくなる位置へ置かれます。

ただし、各型が置かれやすい位置を守るために、メンバーとメンバーの間や構造体の末尾へ、見えない空き領域が入る場合があります。

この空き領域を、パディングと呼びます。

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

#include <stdint.h>

typedef struct
{
    uint8_t command;
    uint16_t value;
    uint8_t status;
} PacketHeader;

メンバーは、次の順で並びます。

command
↓
必要に応じたパディング
↓
value
↓
status
↓
必要に応じた末尾のパディング

commandvaluestatus の宣言順は変わりません。

しかし、uint16_t のような複数バイトの値は、CPUやコンパイラが扱いやすい位置へ置くため、command の後ろへパディングが入る場合があります。

そのため、メンバーのサイズを単純に足した値と、sizeof(PacketHeader) が必ず同じになるとは限りません。

uint8_t  : 1バイト
uint16_t : 2バイト
uint8_t  : 1バイト
----------------
合計      : 4バイト

ただし、パディングが入る環境では
sizeof(PacketHeader) が 4バイトより大きくなる場合がある

実際の配置は、offsetof を使うと確認できます。

#include <stddef.h>
#include <stdint.h>
#include <stdio.h>

typedef struct
{
    uint8_t command;
    uint16_t value;
    uint8_t status;
} PacketHeader;

int main(void)
{
    printf("PacketHeader 全体: %zuバイト\n", sizeof(PacketHeader));
    printf("command の位置: %zuバイト目\n", offsetof(PacketHeader, command));
    printf("value の位置: %zuバイト目\n", offsetof(PacketHeader, value));
    printf("status の位置: %zuバイト目\n", offsetof(PacketHeader, status));

    return 0;
}

offsetof(PacketHeader, value) は、構造体の先頭から value まで何バイト離れているかを表します。

ここで表示される値は、実行環境によって異なる場合があります。

構造体を使うときは、次を覚えておいてください。

構造体全体は、sizeof で示される連続したバイト列として扱われる
メンバーは、宣言した順に並ぶ
ただし、メンバーの間や末尾にパディングが入る場合がある
sizeof(構造体の型) は、パディングを含めた全体のサイズになる

memset はコピーではない

構造体や受信バッファを扱うと、memsetmemcpy が出てきます。

名前が似ていますが、役割は異なります。

関数 役割 主な用途
memset 指定した領域を同じ1バイト値で埋める バッファをゼロで埋める
memcpy ある領域のバイト列を別の領域へコピーする 受信したデータをコピーする

次の memset は、構造体の内容をゼロのバイト列で埋める処理です。

#include <string.h>

PacketHeader header;
memset(&header, 0, sizeof(header));

これは、受信した電文を構造体へ読み込む処理ではありません。

新しく作る構造体の初期値をゼロにしたいだけなら、初学者向けのコードでは、次の書き方を優先してください。

PacketHeader header = { 0 };

{ 0 } は、構造体の各メンバーを型に応じたゼロ値で初期化する意図を表せます。

一方、受信した電文のバイト列を構造体へコピーするのは、memcpy です。


9. 電文を構造体として扱うときの考え方

組み込み機器や通信処理では、受信したバイト列を「電文」として扱うことがあります。

電文の内容が、プログラム側の構造体の配置と完全に一致する場合は、バイト列を構造体へコピーし、メンバー名で取り出せます。

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

#include <stdint.h>

typedef struct
{
    uint8_t command;
    uint8_t status;
    uint16_t value;
} DevicePacket;

受信したデータを DevicePacket へコピーする最小形は、次のようになります。

#include <stddef.h>
#include <stdint.h>
#include <string.h>

int copy_device_packet(
    const uint8_t *received_data,
    size_t received_size,
    DevicePacket *packet)
{
    // 受信バッファと、コピー先の構造体が使えるか確認します。
    if (received_data == NULL || packet == NULL)
    {
        return 0;
    }

    // 構造体1件分に足りない電文はコピーしません。
    if (received_size < sizeof(*packet))
    {
        return 0;
    }

    // 受信したバイト列を構造体へコピーします。
    memcpy(packet, received_data, sizeof(*packet));

    return 1;
}

呼び出し側では、次のように使えます。

DevicePacket packet = { 0 };

if (copy_device_packet(received_data, received_size, &packet))
{
    printf("command: %u\n", packet.command);
    printf("status: %u\n", packet.status);
    printf("value: %u\n", packet.value);
}

この書き方では、packet.commandpacket.value のように、構造体のメンバー名で値を取り出せます。

ただし、この方法を使ってよいのは、次の条件がそろっている場合だけです。

電文のバイト数が sizeof(DevicePacket) と一致している
電文の項目順と、構造体のメンバー順が一致している
パディングを含む構造体の配置が、送信側と受信側で一致している
複数バイト値のバイト順(エンディアン)が、送信側と受信側で一致している
受信したバイト列が、各メンバーの型として有効な値になっている

たとえば、uint16_t value は2バイトです。

送信側が上位バイトから送る仕様(ビッグエンディアン)で、受信側のCPUが下位バイトから値を扱う環境(リトルエンディアン)の場合、memcpy だけでは value を正しく取得できません。

また、構造体へパディングが入る環境では、通信仕様の電文サイズと sizeof(DevicePacket) が一致しない場合があります。

そのため、別の機器、別のCPU、別のコンパイラ、または長期に維持する通信仕様を扱う場合は、電文をそのまま構造体へコピーするより、バイト位置とバイト順を明示して1項目ずつ読み取る方法を基本にします。

たとえば、ビッグエンディアンの2バイト整数を読む処理は、次のように書けます。

#include <stdint.h>

uint16_t read_u16_big_endian(const uint8_t *data)
{
    return (uint16_t)(((uint16_t)data[0] << 8) | data[1]);
}

この方法なら、受信電文の何バイト目を使うか、どちらの順で結合するかをコードで明確にできます。


受信バッファを構造体ポインタへ直接キャストしない

次の書き方は、短く見えます。

const DevicePacket *packet = (const DevicePacket *)received_data;

しかし、初学者向けの標準的な書き方としては推奨しません。

理由は、received_data の先頭位置が DevicePacket に必要な配置条件を満たしていない可能性があるためです。

さらに、受信サイズ、パディング、エンディアンも確認されません。

まずは、次の順番を守ってください。

受信したサイズを確認する
↓
通信仕様上のバイト順を確認する
↓
必要ならバイト位置ごとに値を取り出す
↓
構造体へコピーしてよい条件がそろう場合だけ memcpy を使う

#pragma pack などで強制的に構造体の隙間をなくす方法もあります。

ただし、これはコンパイラ依存の設定であり、配置条件や性能への影響も考える必要があります。

通信仕様とサイズを合わせるための最初の解決策として、安易に使わないでください。


10. 構造体を表示する関数を作る

商品を表示する処理を関数に分けます。

ここでは、構造体の場所を受け取るポインタを使います。

void print_product(const Product *product)

const は、「この関数では、受け取った商品データを変更しない」という意味です。

Product * のときは構造体の場所を受け取りますが、ポインタ経由でメンバーを見るときは . ではなく -> を使います。

書き方 使う場面
product.name 構造体の変数からメンバーを使う
product_pointer->name 構造体へのポインタからメンバーを使う

product_pointer->name は、次と同じ意味です。

(*product_pointer).name

ただし、-> の方が読みやすいため、通常はこちらを使います。


商品一覧を表示するプログラム

product_list.c というファイルを作成してください。

#include <stdio.h>

// 商品の販売状態を表す型です。
typedef enum
{
    PRODUCT_STATUS_NOT_FOR_SALE = 0,
    PRODUCT_STATUS_ON_SALE = 1
} ProductStatus;

// 商品1件を表す型です。
typedef struct
{
    int id;
    char name[32];
    int unit_price;
    int stock_count;
    ProductStatus status;
} Product;

// 商品1件の情報を表示する関数です。
// const を付けることで、この関数では商品情報を変更しないことを表します。
void print_product(const Product *product)
{
    // 万一 NULL が渡された場合は、何もせずに戻ります。
    if (product == NULL)
    {
        return;
    }

    printf("商品ID: %d\n", product->id);
    printf("商品名: %s\n", product->name);
    printf("単価: %d円\n", product->unit_price);
    printf("在庫数: %d個\n", product->stock_count);

    if (product->status == PRODUCT_STATUS_ON_SALE)
    {
        printf("販売状態: 販売中\n");
    }
    else
    {
        printf("販売状態: 販売停止中\n");
    }
}

int main(void)
{
    // 複数の商品を構造体の配列として作ります。
    Product products[] = {
        { 101, "りんご", 120, 10, PRODUCT_STATUS_ON_SALE },
        { 102, "みかん", 100, 0, PRODUCT_STATUS_ON_SALE },
        { 103, "牛乳", 210, 5, PRODUCT_STATUS_NOT_FOR_SALE }
    };

    // 配列の要素数を自動で取得します。
    size_t product_count = sizeof(products) / sizeof(products[0]);

    // 先頭から最後まで、商品を1件ずつ表示します。
    for (size_t index = 0; index < product_count; index++)
    {
        printf("--- %zu件目 ---\n", index + 1);

        // &products[index] は、今見ている商品が置かれている場所です。
        print_product(&products[index]);

        printf("\n");
    }

    return 0;
}

コンパイルして実行します。

gcc -std=c17 -Wall -Wextra -Wpedantic product_list.c -o product_list
./product_list

11. 商品IDで商品を探す

次は、商品IDを受け取り、該当する商品を探す関数を作ります。

見つかった場合は、その商品の場所を返します。

見つからなかった場合は、NULL を返します。

const Product *find_product_by_id(
    const Product products[],
    size_t product_count,
    int target_id)
{
    for (size_t index = 0; index < product_count; index++)
    {
        if (products[index].id == target_id)
        {
            return &products[index];
        }
    }

    return NULL;
}

使い方は次のとおりです。

const Product *found_product = find_product_by_id(products, product_count, 102);

if (found_product != NULL)
{
    print_product(found_product);
}
else
{
    printf("商品が見つかりませんでした。\n");
}

処理の流れです。

配列の先頭から商品を1件ずつ確認する
↓
商品IDが一致するか確認する
↓
一致したら、その商品の場所を返す
↓
最後まで見つからなければ NULL を返す

このように、構造体・配列・関数・ポインタを組み合わせると、実務でよくある「一覧から条件に合う1件を探す」処理を書けます。


12. 確認課題

課題1:商品を1件追加する

次の配列へ、商品ID 104、商品名 パン、単価 180、在庫数 8、販売状態 販売中 の商品を追加してください。

Product products[] = {
    { 101, "りんご", 120, 10, PRODUCT_STATUS_ON_SALE },
    { 102, "みかん", 100, 0, PRODUCT_STATUS_ON_SALE },
    { 103, "牛乳", 210, 5, PRODUCT_STATUS_NOT_FOR_SALE }
};
答えを見る
Product products[] = {
    { 101, "りんご", 120, 10, PRODUCT_STATUS_ON_SALE },
    { 102, "みかん", 100, 0, PRODUCT_STATUS_ON_SALE },
    { 103, "牛乳", 210, 5, PRODUCT_STATUS_NOT_FOR_SALE },
    { 104, "パン", 180, 8, PRODUCT_STATUS_ON_SALE }
};

sizeof(products) / sizeof(products[0]) を使っていれば、商品数を手作業で修正する必要はありません。

課題2:在庫が0の商品を表示する

products の中から、在庫数が 0 の商品名を表示してください。

答えを見る
for (size_t index = 0; index < product_count; index++)
{
    if (products[index].stock_count == 0)
    {
        printf("在庫切れ: %s\n", products[index].name);
    }
}

課題3:販売中で、在庫がある商品だけ表示する

products の中から、販売中で、かつ在庫数が1以上の商品名を表示してください。

期待する表示例です。

販売中: りんご
販売中: パン
答えを見る
for (size_t index = 0; index < product_count; index++)
{
    if (products[index].status == PRODUCT_STATUS_ON_SALE
        && products[index].stock_count > 0)
    {
        printf("販売中: %s\n", products[index].name);
    }
}

課題4:memsetmemcpy を使い分ける

次の2つの処理について、どちらに memset を使い、どちらに memcpy を使うべきか考えてください。

  1. 受信前に、256バイトの受信バッファをゼロで埋める
  2. 受信した電文のバイト列を DevicePacket 型の変数へコピーする
答えを見る

1は、同じ値で領域を埋めるため memset を使います。

uint8_t receive_buffer[256];
memset(receive_buffer, 0, sizeof(receive_buffer));

2は、受信したバイト列を別の領域へコピーするため memcpy を使います。

memcpy(&packet, received_data, sizeof(packet));

ただし、電文を構造体へコピーする前には、受信サイズ、パディング、バイト順、送信側と受信側の仕様が一致しているかを確認します。

課題5:構造体のサイズを確認する

次の PacketHeader について、sizeof(PacketHeader) と各メンバーの位置を実行して確認してください。

typedef struct
{
    uint8_t command;
    uint16_t value;
    uint8_t status;
} PacketHeader;

メンバーのサイズを足した値と sizeof(PacketHeader) が異なる場合、どこにパディングが入っているかを offsetof の結果から考えてください。


まとめ

構造体は、関連する複数の値を1件のデータとしてまとめるための仕組みです。

struct
= 複数の値をまとめる

typedef
= 型名を短く分かりやすくする

enum
= 状態や種類に意味のある名前を付ける

構造体の配列
= 商品一覧や社員一覧のような複数件を扱う

今回の重要な書き方を整理します。

書き方 意味
product.id 構造体のメンバーを使う
product_pointer->id 構造体へのポインタからメンバーを使う
sizeof(array) / sizeof(array[0]) 配列の要素数を求める
sizeof(構造体の型) パディングを含む構造体全体のバイト数を求める
const Product * 受け取った商品を変更しないポインタ
memset 領域を同じ1バイト値で埋める
memcpy バイト列を別の領域へコピーする

構造体は、商品一覧のような業務データだけでなく、固定形式のバイト列を扱うときにも利用されます。

ただし、電文を構造体として扱う場合は、パディング、受信サイズ、バイト順、送信側と受信側の仕様が一致しているかを必ず確認してください。

次は、実行時に必要な件数だけデータを用意するために、malloccallocreallocfreememset を使った動的メモリ管理を学びます。


次の記事

次は、実行中に必要な件数だけデータを扱うための、動的メモリ管理を学びます。

malloccallocreallocfreememset の役割と、安全に使うための注意点を確認します。

C言語の動的メモリ管理入門|malloc・calloc・realloc・free・memset

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?