この記事は、C言語ドリルの7本目として構造体(struct)を練習する記事です。1人の生徒の名前と3教科の成績、1つのセンサーの値と状態。そんな「関連する複数のデータを1つにまとめて扱う」書き方を身につけます。

構造体の定義・メンバ・typedefまでを7問の練習問題(基礎5問+発展2問)で手を動かして固めるドリルです。解答と解説は各問題の「+」を押すと開きます。まず自分で書いてみて、それから答え合わせをしてください。実務では、通信データ(CANのフレーム)などを構造体でまとめて扱います。その実例まで踏み込みます。

<自己紹介>
筆者は現役の組込みエンジニアです。未経験向けのC言語練習問題を通じて最低限の言語知識を身につけ、最終的にはArduinoでマイコンプログラムを自作できるようになるのをゴールとして、このシリーズを作成しています。

このテーマの要点

構造体でやることはこれだけ

  • 関連するデータを1つの型にまとめるstruct Led { ... };)。違う型を混ぜられるのが配列との違い
  • 中身(メンバ)は ドット . で読み書きする(led.pin
  • まとめたものは、配列にも・関数の引数にもできる。typedef で型名を短くできる

変数がバラバラだと、データが増えるほど管理が大変になります。構造体で「意味のまとまり」ごとに束ねると、コードが整理され、渡すのも1個で済みます。

前提知識:構造体の作り方とtypedef

① 定義(設計図を作る)

struct 構造体名 { ... }; で定義します。中に並べる変数をメンバと呼びます。配列と違い、型の違うデータを混ぜて持てるのが特徴です。

定義

struct Student {
    char name[20];   // 名前(文字の配列)
    int  japanese;   // 国語
    int  math;       // 数学
    int  english;    // 英語
};   // ← 最後のセミコロンを忘れずに
構造体の定義とメモリ上のひとまとまりの対応図
図:定義(設計図)とメモリ上の実体の対応。メンバがひとまとまりで並ぶ

② 使う(変数を作ってメンバにアクセス)

アクセス

struct Student tanaka = {"tanaka", 80, 90, 100};  // 定義順に初期化
printf("%s\n", tanaka.name);   // ドットで読む → tanaka
tanaka.math = 95;              // ドットで書く
メモ:メンバへのアクセスは「ドット」
構造体の中身(メンバ)を読み書きするときは、tanaka.math のようにドット . を使います。(構造体をポインタで扱うときだけ、矢印 -> という別の記号を使います。ポインタは#6で扱います。)

③ typedef(型名を短くする)

変数を作るたびに struct Student と書くのは少し長いですよね。typedef を使うと、型名に別名を付けられます

typedef

typedef struct {
    char name[20];
    int  japanese;
    int  math;
    int  english;
} Student;               // ← これが新しい型名になる

Student tanaka;          // struct を書かずに使える

実務のコードでは、この typedef 形式の方をよく見かけます。ちなみに typedef は構造体専用ではなく、typedef unsigned char uint8_t; のように既存の型への別名にも使えます(#1で触れた uint8_t の正体は、まさにこれです)。

より正確な仕様は、cppreference(C言語リファレンス)で確認できます。

例題(まずは一緒に解いてみる)

練習問題に入る前に、簡単な例題を1つ、答えを見ながら流れを確認しましょう。

例題:xy を持つ構造体 Point を作り、{1, 2} を入れて表示してみましょう。

example.c

#include <stdio.h>

struct Point { int x; int y; };

int main(void) {
    struct Point p = {1, 2};
    printf("x=%d y=%d\n", p.x, p.y);
    return 0;
}

実行結果:x=1 y=2

解説:struct Point { ... }; が設計図、struct Point p = {1, 2}; が実物です。中身は p.xp.y のようにドットで読み書きします。関連する値を1セットで持てるのが構造体です。

流れはつかめましたか? ここからは、自分の手で解いてみましょう。

練習問題(基礎5+発展2)

基礎5問は必ず解いてください。発展2問は余力があればで大丈夫です。まず自分で書いてから、各問題の「+ 解答・解説を見る」を開いて答え合わせをしましょう。

基礎 Q1

xy を持つ構造体 Point を定義し、x=3, y=5 を入れて (3, 5) の形で表示してください。

解答・解説を見る
q1.c

#include <stdio.h>

struct Point { int x; int y; };

int main(void) {
    struct Point p;
    p.x = 3;
    p.y = 5;
    printf("(%d, %d)\n", p.x, p.y);
    return 0;
}

実行結果:(3, 5)

解説:struct Point { ... }; が「設計図」、struct Point p; がそこから作った実物です。中身は p.xp.y のようにドットで読み書きします。関連する2つの値を1セットで扱えるのが構造体の第一歩です。

基礎 Q2

生徒1人の成績をまとめた構造体 Studentnamejapanesemathenglish)を作り、{"tanaka", 80, 90, 100} で初期化して、名前と3教科の点数を表示してください。

解答・解説を見る
q2.c

#include <stdio.h>

struct Student {
    char name[20];
    int  japanese;
    int  math;
    int  english;
};

int main(void) {
    struct Student tanaka = {"tanaka", 80, 90, 100};
    printf("name=%s\n", tanaka.name);
    printf("japanese=%d math=%d english=%d\n",
           tanaka.japanese, tanaka.math, tanaka.english);
    return 0;
}

実行結果:name=tanakajapanese=80 math=90 english=100

解説:この構造体には、文字の配列(name)と整数(成績)という違う型が同居しています。これが配列にはできない、構造体ならではの力です。{"tanaka", 80, 90, 100} は定義した順にまとめて初期化する書き方。名前の表示には文字列用の %s を使います。

基礎 Q3

idvalue を持つ構造体 Sensor3個ぶんの配列にして、for で全部表示してください({1,100},{2,200},{3,300})。

解答・解説を見る
q3.c

#include <stdio.h>

struct Sensor { int id; int value; };

int main(void) {
    struct Sensor sensors[3] = {{1, 100}, {2, 200}, {3, 300}};
    for (int i = 0; i < 3; i++) {
        printf("id=%d value=%d\n", sensors[i].id, sensors[i].value);
    }
    return 0;
}

実行結果:id=1 value=100 / id=2 value=200 / id=3 value=300

解説:構造体も配列にできます。sensors[i].id は「i番目のセンサーのid」という意味です。「複数の同じ種類のもの(センサー)を、それぞれ中身を持たせて並べる」ときに、構造体の配列がぴったりはまります。

基礎 Q4

構造体 Point関数の引数として渡し、その中で (x, y) を表示する関数 printPoint を作ってください。

解答・解説を見る
q4.c

#include <stdio.h>

struct Point { int x; int y; };

void printPoint(struct Point p) {
    printf("(%d, %d)\n", p.x, p.y);
}

int main(void) {
    struct Point p = {7, 9};
    printPoint(p);
    return 0;
}

実行結果:(7, 9)

解説:構造体は、関数にもまるごと1個で渡せます。バラバラの変数を2つ3つと渡す代わりに、1つの構造体で済むのが利点です。(大きな構造体を何度も渡すときは、ポインタで「場所だけ渡す」方法もあります。それは#6で扱います。)

基礎 Q5

Q1の Point を、typedef を使って struct と書かずに使える形に書き直してください。動作はQ1と同じで大丈夫です。

解答・解説を見る
q5.c

#include <stdio.h>

typedef struct {
    int x;
    int y;
} Point;                  // Point が型名になる

int main(void) {
    Point p = {3, 5};     // struct を書かなくてよい
    printf("(%d, %d)\n", p.x, p.y);
    return 0;
}

実行結果:(3, 5)

解説:typedef struct { ... } Point; とすると、以後は Point だけで変数を作れます。intやcharと同じ感覚で自分の型を使えるので、コードがすっきりします。実務のヘッダファイルでは、この形の型定義がずらりと並ぶのが普通です。書き方の違いだけで、中身の使い方(ドットでアクセス)は変わりません。

発展 Q6

構造体 Sensoridvalue)の変数 a = {1, 100} を、そのまま = で別の変数 b に代入してください。その後 b.value だけを 999 に書き換えて、a と b の両方を表示し、aが変わっていないことを確かめます。

解答・解説を見る
q6.c

#include <stdio.h>

struct Sensor { int id; int value; };

int main(void) {
    struct Sensor a = {1, 100};
    struct Sensor b = a;        // まるごとコピーされる
    b.value = 999;              // bだけ書き換える
    printf("a: id=%d value=%d\n", a.id, a.value);
    printf("b: id=%d value=%d\n", b.id, b.value);
    return 0;
}

実行結果:a: id=1 value=100 / b: id=1 value=999

解説:構造体どうしは =まるごとコピーできます(メンバを1つずつ代入する必要はありません)。コピーなので、b を書き換えても a は変わりません。この「実体が2つになる」感覚は、次のポインタ(場所を指すだけで実体は1つ)と対比すると、より深く理解できます。

発展 Q7

車載の通信でおなじみ、CANのフレームを構造体で表してみましょう。iddlc(データ長)・data[8](中身の配列)を持つ構造体を作り、id=0x100, dlc=3, data={0xAA,0xBB,0xCC} を入れて、データ長ぶんだけ中身を表示してください。

解答・解説を見る
q7.c

#include <stdio.h>

struct CanFrame {
    int id;
    int dlc;         // データ長
    int data[8];     // 中身(配列)
};

int main(void) {
    struct CanFrame frame = {0x100, 3, {0xAA, 0xBB, 0xCC}};
    printf("id=0x%X dlc=%d\n", frame.id, frame.dlc);
    for (int i = 0; i < frame.dlc; i++) {
        printf("data[%d]=0x%X\n", i, frame.data[i]);
    }
    return 0;
}

実行結果:id=0x100 dlc=3 のあと data[0]=0xAAdata[2]=0xCC

解説:ここで構造体と配列が合体しています。「1つの通信メッセージ」というまとまりを構造体にし、その中の「中身のバイト列」は配列で持つ、という形です。これは車載のCAN通信そのままの構造で、実務でも同じように扱います。構造体を覚えると、こうした通信データも1個の変数として扱えるようになります。

この発展問題は余力がある人向けです。基礎5問ができていれば、次のテーマ(ビット演算)に進んで大丈夫です。

例:実務で実際に書くコード

構造体は、1つの部品に関する情報をまとめるのに便利です。センサーやLEDごとに、関連する値を1セットで持てます。

例:複数のセンサーを構造体の配列で管理

typedef struct {
    int id;          // センサー番号
    int value;       // 最新の値
    int threshold;   // しきい値
} Sensor;

Sensor sensors[3] = {{1, 0, 300}, {2, 0, 500}, {3, 0, 700}};

// 全センサーを読んで、しきい値を超えたものを表示
for (int i = 0; i < 3; i++) {
    sensors[i].value = read_sensor(sensors[i].id);
    if (sensors[i].value > sensors[i].threshold) {
        printf("sensor %d over!\n", sensors[i].id);
    }
}
実務での構造体の使い方
私が構造体を使うときは、機能のまとまりごとに束ねるのが基本です。「LED」なら pin・状態・明るさ、「モーター」なら pin・速度・回転方向、というふうに、その部品に関する情報を1つにまとめます。車載でおなじみなのは通信データ(CANのフレーム)で、ID・データ長・中身(配列)をひとまとめにして扱います(発展Q7がその形です)。ちなみに「今どの状態か」という状態遷移の管理は、初学者のうちは構造体にせずただの変数1つで十分です。構造体は「関連するデータが増えてきたら束ねる」くらいの気持ちで使い始めれば大丈夫です。

最後につまずきポイントをおさらい

メンバへのアクセスは「.」、ポインタ経由だけ「->」
構造体の中身は、基本は led.pin のようにドットで触ります。ところが「構造体のポインタ」を使うときだけ、ptr->pin という矢印に変わります。この2つの使い分けでよく混乱します。今は「普通はドット、ポインタのときだけ矢印」とだけ覚えておけば十分です(矢印の詳しい話は#6 ポインタで)。

もう一つは 定義の最後のセミコロンです。struct Led { ... }; の閉じカッコの後ろに ; が必要です。ここを忘れるとコンパイルエラーになるので、構造体を定義したら末尾の ; を確認してください。

よくある質問(FAQ)

構造体と配列はどう使い分ける?

同じ種類のものを並べるなら配列(センサー値10個など)、種類の違う値をまとめるなら構造体(ピン・状態・明るさなど)です。両方を組み合わせて「構造体の配列」や「配列を持つ構造体」もよく使います。

typedef は必ず使うべき?

必須ではありません。最初は struct を毎回書く形で十分です。ただ実務のコードは typedef 形式が多いので、Q5の形を「読める」ようにはしておきましょう。自分で書くのは慣れてからで大丈夫です。

メンバはいくつまで持てる?

実用上は気にしなくて大丈夫です。ただしマイコンはメモリが限られるので、本当に必要なメンバだけを持たせるのが、組込みでは良い習慣です。

次に読む

いよいよ最後のテーマ、組込みらしさが一番濃いビット演算です。マイコンのピンやレジスタを、ビット単位で操作します。

次のドリル(#8)C言語 ビット演算の練習問題7問|マスク・シフトを図解で固める
前のドリル(#6)C言語 ポインタの練習問題7問|組込みで避けて通れない理由も解説
目次へ戻るC言語 基礎ドリル|組込みに必要な8テーマの練習問題まとめ