6日でできる 新C言語入門|構造体でデータをまとめる方法

関連するデータは、ひとまとまりにすると扱いやすい。
構造体を使って、C言語でデータを整理する力を身につけよう。

C言語では、int型、double型、char配列などを使って、さまざまなデータを扱えます。

ただし、実際のプログラムでは、1つのデータだけでなく、複数の情報をセットで扱いたい場面がよくあります。たとえば、商品を管理するなら、商品番号、商品名、価格をひとまとまりにしたくなります。学生を管理するなら、学籍番号、名前、点数をまとめたいこともあります。

このようなときに便利なのが構造体です。

構造体を使うと、異なる型のデータを1つのまとまりとして扱えるようになります。int型の番号、char配列の名前、double型の価格など、種類の違うデータを1つの型としてまとめられるのが大きな特徴です。

構造体を理解すると、データ同士の関係が見えやすくなり、プログラムの設計もしやすくなります。配列やポインタ、typedefと組み合わせることで、複数データの管理や関数への受け渡しも自然に書けるようになります。

この記事では、構造体の基本概念、構造体変数の使い方、構造体の配列、typedefによる型名の整理、構造体ポインタの使い方まで、サンプルプログラムと図を使ってやさしく解説していきます。

構造体とは何か

構造体は、複数の変数を1つにまとめたデータ型です。

普通の変数は、1つの値を保存します。

int id;
double price;

一方、構造体を使うと、関連する複数の値を1つのまとまりとして表現できます。

struct product {
    int id;
    char name[100];
    double price;
};

この例では、productという構造体の中に、id、name、priceという3つの情報をまとめています。

用語意味
構造体複数のメンバをまとめたデータ型
メンバ構造体の中にある個々の変数
構造体変数構造体型を使って作った変数

構造体を使うと、番号、名前、価格のように意味のつながったデータを1つの単位として扱えます。

構造体の定義

構造体は、struct、構造体名、メンバ定義を使って定義します。

基本形は次の通りです。

struct 構造体名 {
    型 メンバ名;
    型 メンバ名;
    型 メンバ名;
};

たとえば、商品データを表す構造体なら、次のように定義できます。

struct product {
    int id;
    char name[100];
    double price;
};

この定義だけでは、まだ具体的な商品データが作られるわけではありません。
構造体の型を作った状態です。

実際にデータを入れるには、構造体変数を宣言します。

struct product item;

このitemが、商品1件分のデータを持つ構造体変数です。

図:構造体は関連するデータを1つにまとめる

この図から分かること

この図から分かるのは、構造体が複数の関連データを1つにまとめるための仕組みだということです。

id、name、priceを別々の変数として管理することもできますが、構造体にまとめると、商品1件分のデータとして扱いやすくなります。データの意味のまとまりがコード上にも表れるため、読みやすく整理されたプログラムになります。

構造体のメンバにアクセスする

構造体変数の中にあるメンバへアクセスするときは、ドット演算子を使います。

item.id = 101;

item.price = 1980.0;

構造体変数itemの中にあるidやpriceへアクセスしている、という意味です。

書き方意味
item.iditemのidメンバ
item.nameitemのnameメンバ
item.priceitemのpriceメンバ

int型やdouble型のメンバには、通常の変数と同じように代入できます。

ただし、char配列のメンバに文字列を代入するときは注意が必要です。
C言語では、配列名に対して文字列を直接代入できません。

そのため、文字列をコピーするときはstrcpyを使います。

strcpy(item.name, "ノート");

strcpyを使うためには、string.hを読み込んでおきます。

構造体の基本的な使い方

次のプログラムでは、商品データを構造体で表し、各メンバに値を代入して表示します。

プロジェクト/ファイル名: Lesson64_1/main.c

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

struct product {
    int id;
    char name[100];
    double price;
};

int main(void) {
    struct product data; // 構造体変数の宣言

    // 各メンバへ値を代入する
    data.id = 101;
    strcpy(data.name, "ノート");
    data.price = 180.5;

    // 構造体のメンバを表示する
    printf("ID:%d 商品名:%s 価格:%.1f円\n", data.id, data.name, data.price);

    return 0;
}

実行結果

ID:101 商品名:ノート 価格:180.5円

このプログラムでは、product構造体を定義しています。

struct product {
    int id;
    char name[100];
    double price;
};

この構造体には、商品番号を表すid、商品名を表すname、価格を表すpriceがあります。

main関数の中では、struct product型のdataを宣言しています。

struct product data;

これにより、dataという変数の中に、id、name、priceの3つのメンバを持てるようになります。

数値のメンバには、次のように代入できます。

data.id = 101;
data.price = 180.5;

文字列のメンバには、strcpyを使って文字列をコピーしています。

strcpy(data.name, "ノート");

構造体を使うと、関連する情報を1つの変数dataにまとめられるため、データの意味が分かりやすくなります。

構造体を使うメリット

構造体を使うメリットは、関連するデータを1つの単位として扱えることです。

たとえば、商品を管理する場合、id、name、priceを別々の配列で管理することもできます。

int ids[3];
char names[3][100];
double prices[3];

しかし、この方法では、ids[0]、names[0]、prices[0]が同じ商品を表すという対応関係を常に意識する必要があります。

構造体を使えば、商品1件分の情報を1つにまとめられます。

struct product item;

このitemの中に、id、name、priceがまとまっています。

管理方法特徴
別々の変数や配列で管理対応関係を自分で意識する必要がある
構造体で管理関連するデータを1つのまとまりにできる

データのまとまりがはっきりすると、プログラムの読みやすさと保守のしやすさが上がります。

構造体の配列で複数データを管理する

構造体は、配列として使うこともできます。

たとえば、商品データを3件管理したい場合、構造体変数を3つ別々に用意するより、構造体配列にしたほうが扱いやすくなります。

struct product list[3];

このように書くと、product構造体を3件分持つ配列が作れます。

要素意味
list[0]1件目の商品データ
list[1]2件目の商品データ
list[2]3件目の商品データ

各要素は構造体なので、メンバにはドット演算子でアクセスします。

list[0].id

list[0].name

list[0].price

配列の要素に対してドット演算子を使う、と考えると分かりやすいです。

構造体配列を使ったプログラム

次のプログラムでは、複数の商品データを構造体配列で管理し、for文で順番に表示します。

プロジェクト/ファイル名: Lesson64_2/main.c

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

struct product {
    int id;
    char name[100];
    double price;
};

int main(void) {
    struct product list[] = {
        { 201, "鉛筆", 80.0 },
        { 202, "消しゴム", 120.0 },
        { 203, "定規", 150.5 }
    };
    int i;

    // 構造体配列の内容を順番に表示する
    for (i = 0; i < 3; i++) {
        printf("ID:%d 商品名:%s 価格:%.1f円\n",
            list[i].id, list[i].name, list[i].price);
    }

    return 0;
}

実行結果

ID:201 商品名:鉛筆 価格:80.0円
ID:202 商品名:消しゴム 価格:120.0円
ID:203 商品名:定規 価格:150.5円

このプログラムでは、listという構造体配列を作っています。

struct product list[] = {
    { 201, "鉛筆", 80.0 },
    { 202, "消しゴム", 120.0 },
    { 203, "定規", 150.5 }
};

各行の波かっこの中に、id、name、priceの値を順番に書いています。

配列要素idnameprice
list[0]201鉛筆80.0
list[1]202消しゴム120.0
list[2]203定規150.5

for文では、iを0から2まで変化させながら、list[i]のメンバを表示しています。

list[i].id
list[i].name
list[i].price

構造体配列を使うと、同じ種類のデータを複数件まとめて管理できます。

図:構造体配列で複数データを管理する

この図から分かること

この図から分かるのは、構造体配列では、1つの要素が複数のメンバを持つデータになっているということです。

list[0]、list[1]、list[2]は、それぞれ商品1件分のデータです。各要素の中にはid、name、priceがまとまっています。for文を使えば、複数の商品データを順番に処理できます。

構造体の初期化

構造体は、宣言と同時に初期化できます。

struct product item = { 101, "ノート", 180.5 };

この場合、構造体のメンバに定義順で値が入ります。

メンバ入る値
id101
nameノート
price180.5

構造体配列でも同じように、波かっこを使って初期化できます。

struct product list[] = {
    { 201, "鉛筆", 80.0 },
    { 202, "消しゴム", 120.0 },
    { 203, "定規", 150.5 }
};

初期化の順番は、構造体のメンバ定義の順番に対応します。

そのため、メンバの並びを変更した場合は、初期化の順番にも注意が必要です。

typedefで構造体の型名を扱いやすくする

構造体を使うとき、毎回struct productと書くのは少し長く感じることがあります。

そこで、typedefを使うと、構造体型に短い名前を付けられます。

たとえば、次のように書けます。

typedef struct product PRODUCT;

これにより、struct productの代わりにPRODUCTという名前を使えるようになります。

PRODUCT item;

また、構造体定義とtypedefをまとめて書くこともできます。

typedef struct {
    int id;
    char name[100];
    double price;
} PRODUCT;

この書き方では、PRODUCTという型名で構造体を使えるようになります。

書き方意味
struct product item;struct product型の変数を宣言する
typedef struct product PRODUCT;struct productにPRODUCTという別名を付ける
PRODUCT item;PRODUCT型の変数を宣言する

typedefを使うと、構造体を通常の型名のように扱いやすくなります。

構造体ポインタとは

構造体も、普通の変数と同じようにポインタで扱えます。

構造体変数のアドレスを関数に渡すと、関数側で構造体の中身を参照したり、変更したりできます。

PRODUCT item;
PRODUCT* p = &item;

このように、構造体を指すポインタを作れます。

構造体変数のメンバにアクセスするときはドット演算子を使います。

item.id

一方、構造体ポインタからメンバにアクセスするときはアロー演算子を使います。

p->id
対象メンバアクセス
構造体変数item.id
構造体ポインタp->id

p->idは、次の書き方と同じ意味です。

(*p).id

ただし、構造体ポインタではp->idのほうが読みやすいため、よく使われます。

構造体ポインタを使ったプログラム

次のプログラムでは、typedefで構造体にBOOKという型名を付け、構造体ポインタを関数に渡して表示します。

プロジェクト/ファイル名: Lesson64_3/main.c

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

typedef struct {
    int id;
    char title[100];
    double price;
} BOOK;

void print_book(BOOK* p) {
    printf("ID:%d 書名:%s 価格:%.1f円\n", p->id, p->title, p->price);
}

int main(void) {
    BOOK book = { 301, "C言語入門", 2200.0 };

    // 構造体変数のアドレスを関数に渡す
    print_book(&book);

    return 0;
}

実行結果

ID:301 書名:C言語入門 価格:2200.0円

このプログラムでは、BOOKという構造体型を定義しています。

typedef struct {
    int id;
    char title[100];
    double price;
} BOOK;

これにより、structを毎回書かずにBOOK型として変数を宣言できます。

BOOK book = { 301, "C言語入門", 2200.0 };

print_book関数は、BOOK型のポインタを受け取ります。

void print_book(BOOK* p)

main関数では、bookのアドレスを渡しています。

print_book(&book);

関数側では、pがbookを指しています。
そのため、p->id、p->title、p->priceのようにアロー演算子を使ってメンバへアクセスできます。

ドット演算子とアロー演算子の使い分け

構造体では、メンバにアクセスする方法が2つあります。

構造体変数そのものからアクセスする場合はドット演算子を使います。

book.id

構造体ポインタからアクセスする場合はアロー演算子を使います。

p->id

この違いはとても重要です。

書き方使う場面
book.id構造体変数bookのメンバにアクセスする
p->id構造体ポインタpが指す先のメンバにアクセスする
(*p).idpが指す構造体を取り出してからメンバにアクセスする

p->idは、(*p).idを読みやすくした書き方です。

ポインタを使って構造体を関数に渡すと、構造体全体をコピーせずに済むため、大きな構造体を扱う場合にも便利です。

図:構造体変数と構造体ポインタのメンバアクセス

この図から分かること

この図から分かるのは、構造体変数と構造体ポインタでは、メンバアクセスの書き方が違うということです。

bookのような構造体変数からメンバを見るときはドット演算子を使います。一方、pのような構造体ポインタからメンバを見るときはアロー演算子を使います。pはbookのアドレスを持っているため、p->idのように書くことで、pが指している構造体のメンバへアクセスできます。

構造体を関数に渡すときの考え方

構造体は、関数に渡すこともできます。

大きく分けると、構造体そのものを渡す方法と、構造体のアドレスを渡す方法があります。

渡し方特徴
構造体そのものを渡す値のコピーが関数に渡る
構造体のアドレスを渡すポインタを通して元の構造体を参照できる

構造体が小さい場合は、値として渡しても分かりやすいです。
ただし、メンバが多い構造体や大きな配列を含む構造体では、コピーの負担が大きくなることがあります。

そのため、構造体を関数に渡すときは、ポインタを使う場面もよくあります。

void print_book(BOOK* p)

このようにすれば、BOOK型のデータ全体をコピーせず、アドレスだけを渡せます。

表示だけを行う関数なら、関数内で値を変更しないように注意して使います。より厳密にしたい場合は、constを組み合わせて、読み取り専用として受け取る設計もあります。

構造体を使うときの注意点

構造体は便利ですが、いくつか注意したい点があります。

文字列メンバにはstrcpyを使う

char配列のメンバに文字列を入れるときは、代入演算子で直接入れることはできません。

strcpy(data.name, "ノート");

文字列を扱うために、string.hを読み込んでおきます。

初期化の順番に注意する

構造体を波かっこで初期化する場合、値はメンバの定義順に入ります。

struct product item = { 101, "ノート", 180.5 };

この場合、id、name、priceの順番で値が入ります。

ドット演算子とアロー演算子を混同しない

構造体変数ならドット演算子、構造体ポインタならアロー演算子です。

対象使う演算子
構造体変数.
構造体ポインタ->

構造体配列では添え字とメンバ名の順番を意識する

構造体配列では、まず配列の要素を選び、その要素のメンバにアクセスします。

list[i].price

これは、listのi番目の構造体のpriceメンバという意味です。

構造体が役立つ場面

構造体は、複数の項目を持つデータを扱う場面で役立ちます。

メンバの例
商品データid、name、price
学生データid、name、score
書籍データid、title、price
座標データx、y
日付データyear、month、day

構造体を使うと、データのまとまりが明確になります。

たとえば、商品番号、商品名、価格がいつも一緒に扱われるなら、それらを1つの構造体にまとめると自然です。

構造体は、プログラムの設計を整理するための大切な道具です。

構造体を理解するとデータ設計がしやすくなる

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

int型、char配列、double型など、異なる型のメンバを1つの構造体に入れられるため、商品、学生、書籍、座標、日付のようなまとまりのあるデータを表現しやすくなります。

構造体変数のメンバにアクセスするときはドット演算子を使います。構造体ポインタからメンバにアクセスするときはアロー演算子を使います。

また、構造体を配列にすると、同じ形式のデータを複数件まとめて管理できます。typedefを使えば、構造体型を短い名前で扱いやすくできます。さらに、構造体ポインタを関数に渡せば、構造体データを効率よく受け渡しできます。

構造体を使いこなせるようになると、C言語で扱えるデータの表現力が大きく広がります。単なる変数の集まりではなく、意味のあるデータのまとまりとして設計する意識を持つと、読みやすく管理しやすいプログラムを書きやすくなります。