構造体

構造体の概念

構造体は、複数の値をまとめて管理することができる機能です。
複数のデータ型を寄せ集めて、新しいデータ型を作る機能ともいえます。

配列も複数の値を一括して扱える機能ですが、配列は同じデータ型の集合なのに対して、構造体は異なるデータ型を一括管理できます。

構造体を使ってみる

構造体の定義

実際に構造体を定義したのが以下のサンプルコードです。


struct Person
{
    char name[50];
    int age;
    char gender;
};

構造体は以下のような形で定義します。


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

structというのが「これから構造体を定義しますよ」というキーワードです。
structに続き構造体名を記述します。
構造体名は好きに決めて構いません。
構造体名は構造体のタグとも言います。

次に、波括弧{}でブロックを作ります。
このブロックの中に、構造体で使用したい変数をひとつ以上定義します。
サンプルコードではchar型配列でname(名前)、int型でage(年齢)、char型でgender(性別)を定義し、Person(人物)の情報を格納する構造体を作っています。
nameのように、配列を含めることもできます。

構造体の中で定義した変数の事をメンバ変数(メンバ)と言います。
メンバ変数はいくつでも増やすことができます。

ブロックの最後にはセミコロン(;)を付けるのを忘れないようにしましょう。
関数の定義ではセミコロンが必要なかったので、構造体の定義では忘れてしまいがちです。

構造体を使用する

構造体を実際に使用してみましょう。


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

struct Person
{
    char name[50];
    int age;
    char gender;
};

int main()
{
    struct Person person;
    strcpy_s(
        person.name,
        sizeof(person.name) - 1,
        "○山×男");
    person.age = 20;
    person.gender = 0;
    
    printf(
        "name: %s\n"
        "age: %d\n"
        "gender: %d\n",
        person.name, person.age, person.gender);
    getchar();
}
name: ○山×男
age: 20
gender: 0

4~9行目で定義した構造体を、13行目で実際に使用しています。
「struct Person」というのが、最初に定義した構造体を使用するためのキーワードです。
もちろん「Person」の部分は自分でつけた構造体名によって変わります。

構造体はデータ型なので、使用する場合は変数を用意します。
サンプルコードでは「person」という名前で構造体変数を定義しています。
(この場合、頭文字が小文字なので、構造体名とは別の名前と認識されます)

構造体変数からメンバ変数にアクセスするにはドット演算子を使用します。
構造体変数に続いて.(ドット、ピリオド)を記述し、さらに使用したいメンバ変数名を記述します。

後は通常の変数と同じように、メンバ変数に値を代入したり取り出したりすることができます。

構造体の基本的な機能はこれだけです。
構造体は書き方にいろいろなルールがあるため最初はややこしく感じるかもしれませんが、「好きなデータ型の変数を、好きな数だけまとめて扱える機能」です。

構造体の初期化

構造体変数は宣言と同時に初期化を行うこともできます。


#include <stdio.h>

struct Person
{
    char name[50];
    int age;
    char gender;
};

int main()
{
    struct Person person = { "○山×男", 20, 0 };

    printf(
        "name: %s\n"
        "age: %d\n"
        "gender: %d\n",
        person.name, person.age, person.gender);
    getchar();
}

メンバ変数に文字列が含まれる場合にstrcpyなどの関数を使用する手間がないので、初期化できるならばなるべく初期化した方が楽です。

初期化子は、構造体でメンバ変数を定義した順に記述することに注意してください。
配列の初期化の時と同じく、メンバ変数に対して初期化子が足りない場合は0で埋められます。
以下のようにすればすべての要素を0で初期化した構造体変数が得られます。


struct Person person = { 0 };

これは「初期化子が足りない場合は残りは0で埋める」ことを利用した初期化です。
ここで指定した値ですべて埋められるわけではないので注意してください。


//先頭要素だけが「1」
//残りは「0」
struct Person person = { 1 };

また、この書き方ができるのは宣言と同時に初期化を行う場合のみです。
宣言後の構造体に初期化子を使用することはできません。


struct Person person;

//ダメ
person = { "○山×男", 20, 0 };

//ダメ
person = { 0 };

ただしキャストすることで代入は可能です。
(後述)

メンバ名を指定して初期化

初期化は.でメンバ名を指定して行うことができます。


struct Person person = {
	.name = "○山×子",
	.gender = 1
};

初期化されなかったメンバは0で初期化されます。
ただし古いコンパイラは対応していない場合もあります。

定義と同時に変数を宣言

以下のようにすると、構造体の定義とその変数を同時に宣言できます。


#include <stdio.h>

struct Person
{
    char name[50];
    int age;
    char gender;
} person; //いま定義した構造体の変数を宣言

int main() {}

このコードでは変数personはグローバル変数となります。

構造体の入れ子

構造体は関数のブロック内や構造体内に定義することもできます。


#include <stdio.h>

int main()
{
    //main関数内に構造体を定義
    struct MyStructA
    {
        int num1;

        //入れ子の構造体
        struct MyStructB
        {
            int num2;
        } msB;
    } msA;

	//msAはローカル変数
	//msBはmsAを通してアクセスできる

    //各メンバへのアクセス
    msA.num1 = 1;
    msA.msB.num2 = 2;

    //新しい構造体変数の宣言
    struct MyStructA msA2;
    struct MyStructB msB2;
}

void f1()
{
    //この関数内からはMyStructAもBも使用できない
}

//引数にも指定できない
//これはエラー
void f2(struct MyStructA a)
{
}

関数ブロック内で定義した構造体は、その関数内でだけ使用が可能です。
(ローカル変数と同じようなもの)

構造体のサイズ

構造体のサイズは、各メンバのサイズの合計以上となります。
実際にいくつになるかは環境により異なります。


#include <stdio.h>

struct MyStruct {
    char c;
    short s;
};

int main()
{
    printf("size: %d\n", sizeof(struct MyStruct));
    getchar();
}
size: 4

上記の構造体はchar型(1バイト)とshort型(2バイト)がひとつずつなので、3バイトあれば足ります。
しかしWindows + Visual Studio環境では上記の構造体のサイズは4バイトとなります。
つまり、メモリ上には4バイト分の領域が確保されていて、1バイト分は未使用になっているということです。
この未使用の領域をパディング(詰め物)といいます。

これはOSやハードウェアが、効率的に処理できるようにメモリ上にデータを並べるために起こります。
言語側でコントロールすることはできません。

無名の構造体

構造体の名前自体は省略することができます。
以下のようにすると、この構造体は変数person以外に新しく変数を作ることができなくなります。
これを無名の構造体といいます。


struct
{
    char name[50];
    int age;
    char gender;
} person;

さらに構造体変数の宣言も省略することができます。


struct
{
    char name[50];
    int age;
    char gender;
};

この構造体は他から使う方法がないため意味がありませんが、構造体や共用体の中で定義することで、各メンバに直接アクセスすることができるようになります。


struct DataA {
    int integer;

    //タグ名のみ省略
    struct {
        double real;
        char str[32];
    } inner;
};

struct DataB {
    int integer;

    //タグ名とメンバ名の省略
    struct {
        double real;
        char str[32];
    };
};

int main()
{
    struct DataA dataA = { 0, 1.0, "ABC" };
    struct DataB dataB = { 0, 1.0, "ABC" };

    double d1 = dataA.inner.real;

    //内側の構造体のメンバに直接アクセス可能
    double d2 = dataB.real;
}

構造体変数の代入

構造体は配列とは違い、同じ構造体型の変数同士をそのまま代入することができます。


#include <stdio.h>

struct Person
{
    char name[50];
    int age;
    char gender;
};

int main()
{
    struct Person person1 = { "○山×男", 20, 0 };
    struct Person person2;

	//構造体変数の代入
	//中身がそのままコピーされる
    person2 = person1;

    printf(
        "name: %s\n"
        "age: %d\n"
        "gender: %d\n",
        person2.name, person2.age, person2.gender);

    getchar();
}

構造体変数に別の構造体変数を代入(15行目)すると、構造体の各メンバ変数がすべてコピーされます。

複合リテラル

初期化子はそのまま構造体変数に代入することはできませんが、キャストによって代入が可能です。


struct Person person;

person = (struct Person){ "○山×男", 20, 0 };
person = (struct Person){ 0 };

代入はすべてのメンバをコピーします。
以下のような指定をしても、「特定のメンバだけに対する代入」は出来ないので注意しましょう。


struct Person person = { "○山×男", 20, 0 };

//代入は全てのメンバを上書きする
//これはnameだけを上書きするのではなく
//ageやgenderは0にセットされる
person = (struct Person){ .name = "ABC" };

この機能は複合リテラルというもので、名前のないデータのかたまりを作ることが出来る機能です。

構造体の配列

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


#include <stdio.h>

struct Person
{
    char name[50];
    int age;
    char gender;
};

int main()
{
    struct Person person[] = {
        { "A山B男", 20, 0 },
        { "C下D太", 18, 0 },
        { "E田F子", 21, 1 },
        { "G山H美", 19, 1 },
    };

    int count = sizeof(person) / sizeof(struct Person)
    for (int i = 0; i &lt; count; i++)
    {
        printf(
            "name: %s\n"
            "age: %d\n"
            "gender: %d\n",
            person[i].name, person[i].age, person[i].gender);
    }
    
    getchar();

このように、複数の情報を構造体にしてまとめて、さらに配列にすることで効率的にデータを管理できます。

19行目では構造体配列personの要素数を取得しています。
「sizeof(構造体配列)」とすることで、その構造体配列全体のサイズ(バイト数)が分かります。
「sizeof(struct 構造体名)」とすることで、その構造体一つで必要になるサイズが分かります。

配列全体のサイズを配列ひとつのサイズで割ることで、配列の要素数を得ることができます。

構造体とtypedef

構造体を定義する場合、typedefを使えばより便利になります。

構造体変数を宣言するとき、構造体名の前にstructというキーワードを付ける必要があります。
構造体を使用する度に毎回structを記述するのはちょっとした手間です。

typedefを使用することでこれを解決できます。


#include <stdio.h>

//※typedefを使わない場合
//struct Person
//{
//	char name[50];
//	int age;
//	char gender;
//};

typedef struct
{
    char name[50];
    int age;
    char gender;
} Person;

int main()
{
    //struct Person person = { "○山×男", 20, 0 };
    //↓これがこのように書ける
    Person person = { "○山×男", 20, 0 };

    printf("name: %s\n"
        "age: %d\n"
        "gender: %d\n",
        person.name, person.age, person.gender);
    getchar();
}

4~9行目の構造体の定義を、typedefを使って書き直したのが11~16行目です。

文法的にはtypedefの対象は「struct { ~ }」で、別名に「Person」を指定していることになります。
こうすることでこの構造体を使用する場合にはPersonと記述するだけで済むようになります。

C++では構造体を使用する場合にstructのキーワードを付ける必要がありません。
そのため、このようなtypedefによる置き換えは必要ありません。

Visual StudioはCとC++を混在できるため、typedefを使用せずともstructを省くことができてしまいます。
しかし純粋なC言語ではtypedefを使用しなければstructを省くことはできません。

Visual Studioであっても、ソースコード名の拡張子を「.cpp」から「.c」に変更することでC++機能を排除し、純粋なC言語として扱うことができます。
この場合はtypedefが必要となります。