C言語のきほん|構造体でデータをまとめる

関連するデータをひとつに整理して、C言語のプログラムをもっと見やすく、もっと扱いやすく。

C言語でプログラムを書いていると、ひとつの値だけを扱う場面よりも、いくつかの関連する情報をまとめて扱いたい場面がたくさん出てきます。
たとえば、学生の情報を管理したいときには、学籍番号だけでは足りません。名前も必要ですし、点数や学年なども一緒に扱いたくなります。

こうした情報を、それぞれ別々の変数として管理することもできますが、項目が増えるほど、どの値がどの人の情報なのか分かりにくくなってしまいます。そこで活躍するのが構造体です。

構造体を使うと、異なる型のデータをひとまとまりとして管理できます。
つまり、int型の数値、char型配列の文字列、double型の小数などを、ひとつのまとまりとして表現できるようになります。

構造体は、C言語の中でもとても大切な考え方です。ここでしっかり理解しておくと、今後学ぶ配列との組み合わせ、関数への受け渡し、ポインタとの連携などもぐっと分かりやすくなります。
今回は、構造体の基本から、宣言、初期化、メンバへのアクセスまで、やさしく丁寧に確認していきましょう。

構造体とは何か

C言語では、データを扱う方法として、代表的に次の3つがあります。

種類役割
変数1つのデータを保持する年齢、点数
配列同じ型のデータをまとめて扱う5人分の点数
構造体異なる型のデータをまとめて扱う1人分の学生情報

たとえば、学生の情報として次のようなデータを扱いたいとします。

  • 学籍番号 → int型
  • 氏名 → char型配列
  • 平均点 → double型

これらを別々の変数で管理すると、次のようになります。

int id;
char name[20];
double score;

これでも動作はしますが、関連する情報なのにバラバラに書かれているため、まとまりが見えにくくなります。
そこで構造体を使うと、これらを「学生情報」というひとつのかたまりとして表現できます。

変数・配列・構造体の違いをイメージする

変数は、1個の箱です。
配列は、同じ種類の箱が横に並んだものです。
構造体は、意味の異なる箱をひとつのセットにまとめたものです。

たとえば次のように考えると分かりやすいです。

データの持ち方イメージ
変数1つの値だけを入れる箱
配列同じ種類の箱が連続して並ぶ
構造体名前の違う箱を1セットにまとめる

学生データなら、

  • 学籍番号の箱
  • 氏名の箱
  • 平均点の箱

を、ひとつのセットとして持つのが構造体です。

この図では、変数は1つの値だけを入れる箱、配列は同じ種類の値を並べて入れる箱、構造体は意味の異なるデータをひとつにまとめる箱として表現されています。
構造体の本質は、異なる型のデータを、ひとつのまとまりとして扱えることにあります。

構造体の型を宣言する

構造体を使うときは、まず「どんな項目を持つか」を決めて、型のひな形を作ります。これを構造体の型の宣言といいます。

基本の書き方は次の通りです。

struct 構造体タグ名 {
    データ型 メンバ名;
    データ型 メンバ名;
    ...
};

たとえば学生情報を表すなら、次のように書けます。

struct student {
    int id;
    char name[20];
    double average;
};

この時点では、まだ実際のデータは入っていません。
ここで作っているのは、「学生情報はこの3つの項目を持ちます」という設計図です。

各部分の意味

書き方意味
struct構造体を定義することを表すキーワード
student構造体タグ名。構造体の名前
int id;整数の学籍番号を表すメンバ
char name[20];文字列として氏名を保存するメンバ
double average;平均点を表すメンバ

ここでのメンバとは、構造体の中に入っている各要素のことです。
配列でいう要素に少し似ていますが、構造体では各メンバがそれぞれ異なる型でもよい、という点が大きな特徴です。

構造体の変数を宣言する

型のひな形を作ったら、その型を使って実際の変数を宣言できます。

書き方は次の通りです。

struct 構造体タグ名 変数名;

学生情報の変数を1つ作るなら、次のように書きます。

struct student s1;

これで、s1という変数の中に、

  • id
  • name
  • average

という3つのメンバを持てるようになります。

ここで大事なのは、構造体の型の宣言構造体の変数の宣言は別であることです。

書き方意味
struct student { ... };型の設計図を作る
struct student s1;実際のデータを入れる変数を作る

この違いはとても重要です。
設計図だけではデータは保存できません。実際に使うには、変数を宣言する必要があります。

構造体の変数を使う流れ

構造体の基本的な使い方は、次の4段階で考えると分かりやすいです。

手順内容
1構造体の型を宣言する
2構造体の変数を宣言する
3メンバに値を代入する
4メンバの値を参照する

この流れを意識しながらサンプルを読むと、構造体の動きが自然に理解できます。

サンプルプログラムで基本を確認する

シンプルな学生情報の例に説明します。

ファイル名:15_1_1.c

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

// 学生情報を表す構造体の型を宣言
struct student {
    int id;             // 学籍番号
    char name[20];      // 氏名
    double average;     // 平均点
};

int main(void)
{
    // 構造体の変数を宣言
    struct student s1;

    // 構造体の変数を初期化
    struct student s2 = {102, "佐藤花子", 88.5};
    struct student s3 = {.id = 103, .name = "鈴木一郎"};

    // s1の各メンバに値を代入
    s1.id = 101;
    strcpy(s1.name, "田中太郎");
    s1.average = 92.0;

    // 各構造体の内容を表示
    printf("学生1: %d %s %.1f点\n", s1.id, s1.name, s1.average);
    printf("学生2: %d %s %.1f点\n", s2.id, s2.name, s2.average);
    printf("学生3: %d %s %.1f点\n", s3.id, s3.name, s3.average);

    return 0;
}

実行結果例

学生1: 101 田中太郎 92.0点
学生2: 102 佐藤花子 88.5点
学生3: 103 鈴木一郎 0.0点

このプログラムでは、3人分の学生情報を構造体で表しています。
それぞれの学生が、学籍番号、氏名、平均点をひとまとまりで持っていることが分かります。

サンプルプログラムを順に読み解く

まず、この部分で構造体の型を宣言しています。

struct student {
    int id;
    char name[20];
    double average;
};

ここでは、「student型は、整数のid、文字列のname、小数のaverageを持つ」と定義しています。
まだこの段階では、s1やs2のような実体はありません。あくまで型の定義です。

次に、実際の変数を宣言しています。

struct student s1;

これで、student型の変数s1が作られました。
ただし、この時点では中身は未設定です。自動で意味のある値が入るわけではないので注意が必要です。

続いて、初期化付きで変数を宣言しています。

struct student s2 = {102, "佐藤花子", 88.5};

これは、先頭から順番に

  • id に 102
  • name に 佐藤花子
  • average に 88.5

を入れています。

さらに、特定のメンバだけを指定して初期化しているのがこちらです。

struct student s3 = {.id = 103, .name = "鈴木一郎"};

これはC99以降で使える書き方で、メンバ名を明示しながら初期化できます。
average は指定していないため、自動的に 0.0 になります。

構造体のメンバに値を代入する

構造体の各メンバには、ドット演算子を使ってアクセスします。

s1.id = 101;
s1.average = 92.0;

このように、変数名.メンバ名 という形で書きます。

ドット演算子とは

ドット演算子は、「この構造体変数の中の、このメンバにアクセスする」という意味を持ちます。

書き方意味
s1.ids1の中のidにアクセスする
s1.names1の中のnameにアクセスする
s1.averages1の中のaverageにアクセスする

とても素直な書き方なので、構造体を理解するうえで最初にしっかり慣れておきたいポイントです。

文字列メンバへの代入で気をつけること

構造体のメンバが char型配列のときは、数値のように = で文字列を代入できません。

たとえば、次のような書き方はできません。

s1.name = "田中太郎";

これは、name が文字列そのものではなく、文字を並べて保存する配列だからです。
そのため、文字列を代入したいときは strcpy を使います。

strcpy(s1.name, "田中太郎");

なぜ strcpy を使うのか

配列は代入演算子で丸ごとコピーできないためです。
文字列は実体としては文字の並びなので、1文字ずつコピーする処理が必要になります。strcpy はそのための関数です。

注意点

strcpy を使うときは、コピー先の配列サイズを超えないように注意が必要です。
たとえば name が 20文字分しかないのに、もっと長い文字列を入れようとすると、メモリ破壊の原因になります。

この段階では、まず次の点を意識しておくと十分です。

  • 文字列メンバは = で代入しない
  • strcpy を使う
  • 配列サイズを超えない文字列を入れる

構造体の初期化を理解する

構造体は、宣言と同時に初期化できます。
初期化には大きく分けて2つの方法があります。

順番どおりに初期化する方法

struct student s2 = {102, "佐藤花子", 88.5};

これはとてもシンプルですが、メンバの順番を正しく覚えていないと、違う値を入れてしまう恐れがあります。

メンバ名を指定して初期化する方法

struct student s3 = {.id = 103, .name = "鈴木一郎"};

こちらは、どのメンバに何を入れるかが見た目ではっきりするので、読みやすくなります。
また、必要なメンバだけを指定できるのも便利です。

初期化されなかったメンバはどうなるか

構造体を初期化子付きで宣言した場合、指定しなかったメンバは自動的に0で初期化されます。

たとえば、

struct student s3 = {.id = 103, .name = "鈴木一郎"};

では average を書いていないので、自動的に 0.0 になります。

また、全メンバを0で初期化したい場合は、次のように書けます。

struct student s4 = {0};

この書き方はとてもよく使われます。
「まず全部0にしておきたい」というときに便利です。

構造体変数の状態を表で整理する

サンプルプログラムの各変数の状態を表にすると、より分かりやすくなります。

変数名idnameaverage
s1代入前は不定代入前は不定代入前は不定
s2102佐藤花子88.5
s3103鈴木一郎0.0

ここで大事なのは、初期化していないローカル変数のメンバは不定値になることです。
つまり、s1は宣言しただけでは安全に使えません。使う前に必ず値を代入する必要があります。

図で宣言と初期化を整理する

  • s1 は宣言だけされていて中身が未設定
  • s2 はすべてのメンバに値が入っている
  • s3 は一部だけ初期化され、残りが0になっている

この図を見ると、宣言だけの構造体変数と、初期化済みの構造体変数の違いがすぐに分かります。
とくに、s3のような一部だけを指定する初期化では、指定しなかったメンバが自動的に0になることが視覚的に理解できます。

構造体を使うメリット

構造体を使う大きなメリットは、関連するデータをひとまとまりで扱えることです。

たとえば、学生1人分の情報を

int id;
char name[20];
double average;

のようにバラバラで持つよりも、

struct student s1;

のようにひとまとめで持てる方が、意味が分かりやすくなります。

構造体を使う利点

利点内容
データの意味がまとまる学生1人分、商品1件分などの単位で扱える
プログラムが読みやすい何の情報がまとまっているか分かりやすい
保守しやすい項目の追加や修正がしやすい
配列や関数と組み合わせやすい複数件管理や受け渡しに発展しやすい

特に、後で複数人分のデータを扱うとき、構造体の配列にすると非常に整理しやすくなります。
この先の学習では、構造体は単体で終わらず、配列やポインタと結びついてさらに重要になっていきます。

構造体の型宣言と変数宣言を混同しない

初心者のうちは、ここで混乱しやすいです。

struct student {
    int id;
    char name[20];
    double average;
};

これは型の宣言です。
一方で、

struct student s1;

これは変数の宣言です。

この2つの違いを、もう一度表にしておきます。

種類書き方役割
型の宣言struct student { ... };設計図を作る
変数の宣言struct student s1;実際に使うデータ領域を作る

この違いが分かると、構造体だけでなく、typedef やポインタを学ぶときにも理解が進みやすくなります。

よくある注意点

構造体を学び始めたときに、つまずきやすいポイントも整理しておきましょう。

文字列を = で代入しようとしてしまう

これはとても多いミスです。

s1.name = "田中太郎";

この書き方はできません。
char型配列には strcpy を使います。

宣言しただけで使ってしまう

struct student s1;
printf("%d\n", s1.id);

このように、値を入れる前に使うと不定値になる可能性があります。
ローカル変数は、初期化しない限り安全な値にはなりません。

メンバの順番を間違えて初期化してしまう

struct student s2 = {"佐藤花子", 102, 88.5};

このように順番を間違えると、正しく初期化できません。
不安なときは、メンバ名を指定する初期化を使うと安心です。

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

ここまでの内容で、最低限しっかり押さえておきたいのは次の点です。

ポイント内容
構造体の役割異なる型のデータをまとめる
構造体の型の宣言struct タグ名 { ... };
構造体変数の宣言struct タグ名 変数名;
メンバへのアクセス変数名.メンバ名
文字列メンバへの代入strcpy を使う
特定メンバの初期化.メンバ名 = 値 の形で書ける
未指定メンバの扱い初期化子付きなら自動的に0になる

構造体は、見た目が少し長く感じるかもしれませんが、考え方そのものはとても自然です。
「関連するデータをひとまとめにする」――まずはこの感覚をしっかり持てれば大丈夫です。

次に構造体の配列へ進むと、「学生を1人扱う」から「学生を何人もまとめて扱う」へと発展していきます。すると、構造体の便利さがさらに実感できるようになります。