C言語 構造体の練習問題7問|メンバ・typedefまで固める
この記事は、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 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つ、答えを見ながら流れを確認しましょう。
例題:x と y を持つ構造体 Point を作り、{1, 2} を入れて表示してみましょう。
#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.x、p.y のようにドットで読み書きします。関連する値を1セットで持てるのが構造体です。
流れはつかめましたか? ここからは、自分の手で解いてみましょう。
練習問題(基礎5+発展2)
基礎5問は必ず解いてください。発展2問は余力があればで大丈夫です。まず自分で書いてから、各問題の「+ 解答・解説を見る」を開いて答え合わせをしましょう。
基礎 Q1
x と y を持つ構造体 Point を定義し、x=3, y=5 を入れて (3, 5) の形で表示してください。
解答・解説を見る
#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.x、p.y のようにドットで読み書きします。関連する2つの値を1セットで扱えるのが構造体の第一歩です。
基礎 Q2
生徒1人の成績をまとめた構造体 Student(name・japanese・math・english)を作り、{"tanaka", 80, 90, 100} で初期化して、名前と3教科の点数を表示してください。
解答・解説を見る
#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=tanaka と japanese=80 math=90 english=100
解説:この構造体には、文字の配列(name)と整数(成績)という違う型が同居しています。これが配列にはできない、構造体ならではの力です。{"tanaka", 80, 90, 100} は定義した順にまとめて初期化する書き方。名前の表示には文字列用の %s を使います。
基礎 Q3
id と value を持つ構造体 Sensor を3個ぶんの配列にして、for で全部表示してください({1,100},{2,200},{3,300})。
解答・解説を見る
#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 を作ってください。
解答・解説を見る
#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と同じで大丈夫です。
解答・解説を見る
#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
構造体 Sensor(id・value)の変数 a = {1, 100} を、そのまま = で別の変数 b に代入してください。その後 b.value だけを 999 に書き換えて、a と b の両方を表示し、aが変わっていないことを確かめます。
解答・解説を見る
#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のフレームを構造体で表してみましょう。id・dlc(データ長)・data[8](中身の配列)を持つ構造体を作り、id=0x100, dlc=3, data={0xAA,0xBB,0xCC} を入れて、データ長ぶんだけ中身を表示してください。
解答・解説を見る
#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]=0xAA〜data[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の形を「読める」ようにはしておきましょう。自分で書くのは慣れてからで大丈夫です。
メンバはいくつまで持てる?
実用上は気にしなくて大丈夫です。ただしマイコンはメモリが限られるので、本当に必要なメンバだけを持たせるのが、組込みでは良い習慣です。
次に読む
いよいよ最後のテーマ、組込みらしさが一番濃いビット演算です。マイコンのピンやレジスタを、ビット単位で操作します。