C言語を基礎から順に学びたい場合は、C言語 学習ロードマップ を参照してください。
前提として、C言語のポインタ入門を読んでいると理解しやすくなります。
商品、社員、センサー情報のようなデータは、1つの値だけでは表せません。
たとえば商品なら、次のように複数の情報があります。
商品
├ 商品ID
├ 商品名
├ 単価
├ 在庫数
└ 販売状態
これらを別々の変数や別々の配列で持つと、データの対応関係を崩しやすくなります。
C言語では、関連する複数の値を1件のデータとしてまとめるために、構造体を使います。
この記事では、構造体を中心に、次の内容を学びます。
-
structで複数の値を1件にまとめる -
typedefで型名を短く分かりやすくする -
enumで状態や種類を意味のある名前で表す - 構造体の配列で複数件を扱う
-
sizeofで配列の要素数を自動取得する - 構造体がメモリ上に置かれる順番と、パディングの考え方
-
memsetとmemcpyの役割の違い -
constで「変更しない」ことを表す - 構造体を関数へ渡して表示・検索する
前提
この記事は、次の内容を学んだ人を対象にしています。
- 変数、条件分岐、繰り返し、配列、文字列
- 関数
- ポインタの基本
-
NULLの意味
先に次の記事を読んでください。
C言語の基本|初めてのプログラムを動かしながら、書き方と考え方を学ぶ
main 関数の引数や argc・argv が気になる場合は、補足として次の記事を参照してください。
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
↓
必要に応じた末尾のパディング
command、value、status の宣言順は変わりません。
しかし、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 はコピーではない
構造体や受信バッファを扱うと、memset と memcpy が出てきます。
名前が似ていますが、役割は異なります。
| 関数 | 役割 | 主な用途 |
|---|---|---|
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.command や packet.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:memset と memcpy を使い分ける
次の2つの処理について、どちらに memset を使い、どちらに memcpy を使うべきか考えてください。
- 受信前に、256バイトの受信バッファをゼロで埋める
- 受信した電文のバイト列を
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 |
バイト列を別の領域へコピーする |
構造体は、商品一覧のような業務データだけでなく、固定形式のバイト列を扱うときにも利用されます。
ただし、電文を構造体として扱う場合は、パディング、受信サイズ、バイト順、送信側と受信側の仕様が一致しているかを必ず確認してください。
次は、実行時に必要な件数だけデータを用意するために、malloc、calloc、realloc、free、memset を使った動的メモリ管理を学びます。
次の記事
次は、実行中に必要な件数だけデータを扱うための、動的メモリ管理を学びます。
malloc、calloc、realloc、free、memset の役割と、安全に使うための注意点を確認します。