【C言語入門】構造体の使い方(struct、ポインタ、アロー演算子)

構造体って使ってますか? C言語では構造体を使って、ある対象に関連する項目をひとまとまりに扱うことができます。

たとえば個人情報データならば、氏名、性別、年齢、住所、職業などが項目になり、それぞれのデータは個人によって変わってきます。構造体を使うとこれらをひとつにまとめて宣言しておいて、それをもとに各個別ごとで入れ物(実体)を作り、その入れ物(実体)に項目(メンバ)ごとのデータを入力していくことができます。

したがって、各個別(実体)ごとにそれぞれの項目(メンバ)ごとの変数を用意する必要がなく手間が省けて便利です。この記事では「構造体とは「構造体の使い方」「ポインタ、アロー演算子の使い方」という基本的な内容から、

  • 実体をコピーする方法について
  • 実体を比較演算する方法について
  • 構造体を配列で扱う方法


など応用的な使い方まで、わかりやすく解説していきます!

目次

構造体とは

構造体とは、ある対象に関連する項目をまとめて1つのかたまりにしたものです。

関連する項目はメンバと呼ばれ、変数や文字列などをメンバとすることができます。ただし関数はメンバとすることはできません。

構造体は実体と呼ばれるオブジェクトを生成して使用します。実体からメンバを呼び出し、値を代入して使用します。

なお、同じように項目をまとめて1つのかたまりにしたものに配列があります。ただし、配列の場合は同じ型のモノしか1つのかたまりにできません。

構造体はint型や文字列など型の違うモノでも1つのかたまりにできます。

構造体の使い方について

構造体の使い方について宣言の方法、初期化の方法の順に説明していきます。

structで宣言する方法

構造体の宣言の方法について説明します。構造体は下記のように宣言します。

構造体名の前に「struct」句を記述し、「;」で区切られたメンバを「{};」ブロックで囲みます。

struct 構造体名 { 
    メンバ1; 
    メンバ2; 
    ・・・
};

この場合「struct 構造体名」というデータ型として定義されます。これに加えて「typedef」句を使った宣言の方法も使われます。

typedef struct 構造体名 { 
    メンバ1; 
    メンバ2; 
    ・・・
}; 構造体名2;

この場合「struct 構造体名」というデータ型を「構造体名2」というデータ型に型を付け替えるということになります。

// 構造体
struct person1 {
    char *name;
    char sex;
    int age;
    char *add;
    char *job;
};
 
// 構造体
typedef struct person {
    char *name;
    char sex;
    int age;
    char *add;
    char *job;
} person2;

このサンプルコードでは前述の2つの宣言方法で記述しています。「struct person1」型と「person2」型を宣言しています。

なお「typedef」句を使った宣言の場合「person」句を省略することもできます。

// 構造体
typedef struct {
    char *name;
    char sex;
    int age;
    char *add;
    char *job;
} person2;

初期化の方法について

それでは構造体の実体を定義し初期化する方法について説明します。まず前述の宣言の仕方の違いによって、構造体の実体を生成する記述が違ってきます。

「struct 構造体名」型の場合は、次のように定義します。

struct 構造体名 実体名;

「typedef」句を使った「構造体名」型の場合は、次のように定義します。

構造体名 実体名;

構造体の初期化について全メンバへの一括代入は実体の宣言時のみ可能です。それ以外は一括代入できませんのでご注意ください。

また構造体のメンバは、実体からドット演算子(.)を使って呼び出します。それではサンプルコードを確認していきます。

#include <stdio.h>
 
// 構造体
struct person1 {
    char *name;
    char sex;
    int age;
    char *add;
    char *job;
};
 
// 構造体
typedef struct person {
    char *name;
    char sex;
    int age;
    char *add;
    char *job;
} person2;
 
// 一括代入用の関数
person2 init(char *name, char sex, int age, char *add, char* job) {
    person2 p2;
    p2.name = name;
    p2.sex = sex;
    p2.age = age;
    p2.add = add;
    p2.job = job;
    return p2;
}
 
int main(void) {
    // 構造体の実体を生成
    struct person1 tanaka;
    tanaka.name = "T.Tanaka";
    tanaka.sex = 'm';
    tanaka.age = 30;
    tanaka.add = "Tokyo";
    tanaka.job = "teacher";
    printf("%sは%d歳で、%sで%sをしています\n", tanaka.name, tanaka.age, tanaka.add, tanaka.job);
 
    // 構造体の実体の生成と一括初期化
    person2 sato = {"S.Sato", 'f', 25, "Osaka", "nurse"};
    /* error: expected expression before '{' token
    sato = {"S.Sato", 'f', 25, "Osaka", "nurse"};
    */
    printf("%sは%d歳で、%sで%sをしています\n", sato.name, sato.age, sato.add, sato.job);
 
    // 関数で一括代入
    sato = init("K.Sato", 'm', 35, "Nagoya", "doctor");
    printf("%sは%d歳で、%sで%sをしています\n", sato.name, sato.age, sato.add, sato.job);
 
    return 0;
}

実行結果:

T.Tanakaは30歳で、Tokyoでteacherをしています
S.Satoは25歳で、Osakaでnurseをしています
K.Satoは35歳で、Nagoyaでdoctorをしています

このサンプルコードでは構造体を「struct person1」型と「person2」型で宣言しています。「struct person1」型を実体化する際には「struct person1 tanaka」と定義しています。

実体「tanaka」のメンバの初期化は各メンバをひとつずつ初期化しています。「person2」型を実体化する際には「person2 sato」と定義しています。

実体「sato」のメンバの初期化は定義と一緒に全メンバ一括で行っています。定義とは別に実体を一括代入しようとしてもコンパイルエラーとなりますのでご注意ください。

実体の各メンバの代入を一つずつ行うと面倒ですので、一括代入用の関数「init」を記述し使用しています。

ポインタ、アロー演算子の使い方

構造体の実体はポインタを使って扱うと全メンバを含む実体全体を扱うことができて便利です。ポインタを使って実体のアドレスを扱う方法について説明します。

実体のポインタからメンバを呼び出すには、アロー演算子を使用します。「->」記号を使います。

下記のように記述して使用します。

構造体の実体のポインタ変数名->メンバ名

サンプルコードで確認していきましょう。

#include <stdio.h>
 
// 構造体
typedef struct person {
    char *name;
    char sex;
    int age;
    char *add;
    char *job;
} person2;
 
int main(void) {
    person2 sato, *p1;
    p1 = &sato; // 実体のアドレス
    p1->age = 20; // ポインタを使ってメンバの初期化
    printf("%d\n", sato.age);
 
    person2 kato, *p2;
    p2 = &kato; // 実体のアドレス
    *p2 = *p1; // アドレス先の値を共有
    printf("%d\n", kato.age);
 
    return 0;
}

実行結果:

20
20

このサンプルコードでは、構造体の実体「sato」のアドレス先にポインタ「p1」を使用しています。

ポインタ「p1」のメンバ「age」のアドレスにアロー演算子「->」を使ってアクセスし、アドレス先の値を初期化しています。また実体「kato」のアドレス先の値に実体「sato」のアドレス先の値を代入しています。

したがって、実体「kato」のメンバ「age」の値も実体「sato」のメンバ「age」と同じ値となっています。

実体をコピーする方法について

同じ実体を複製コピーして使用したい場合があります。そんな場合はポインタを使ってひとつの実体のアドレス先の値をコピーするのが便利です。

それではサンプルコードで確認していきましょう。

#include <stdio.h>
 
typedef struct person {
    char *name;
    char sex;
    int age;
    char *add;
    char *job;
} person2;
 
int main(void) {
    person2 sato = {"S.Sato", 'f', 25, "Osaka", "nurse"};
    printf("%sは%d歳で、%sで%sをしています\n", sato.name, sato.age, sato.add, sato.job);
 
    person2 *p1;
    p1 = &sato; // 実体のアドレス
    person2 sato2, *p2;
    p2 = &sato2; // 実体のアドレス
 
    *p2 = *p1; // アドレス先の値を共有
    printf("%sは%d歳で、%sで%sをしています\n", sato2.name, sato2.age, sato2.add, sato2.job);
 
    return 0;
}

実行結果:

S.Satoは25歳で、Osakaでnurseをしています
S.Satoは25歳で、Osakaでnurseをしています

このサンプルコードでは、構造体の実体「sato」のアドレス先の値を実体「sato2」のアドレス先に代入しています。アドレス先の値を代入するだけで、メンバ全体の値がコピーされています。

実体を比較演算する方法について

実体がメンバのバイト単位で同じか否かを比較評価する方法について説明します。サンプルコードで確認していきましょう。

#include <stdio.h>
#include <string.h>
 
typedef struct person {
    char *name;
    char sex;
    int age;
    char *add;
    char *job;
} person2;
 
// 構造体比較用関数
int buf_equal(person2 *p1, person2 *p2) {
    if(p1->sex != p2->sex || p1->age != p2->age) {
        return 1;
    }
    if(strcmp(p1->name, p2->name) != 0) {
        return 1;
    }
    if(strcmp(p1->add, p2->add) != 0) {
        return 1;
    }
    if(strcmp(p1->job, p2->job) != 0) {
        return 1;
    }
    return 0;
}
 
int main(void) {
    person2 sato = {"S.Sato", 'f', 25, "Osaka", "nurse"};
    printf("%sは%d歳で、%sで%sをしています\n", sato.name, sato.age, sato.add, sato.job);
 
    person2 *p1;
    p1 = &sato; // 実体のアドレス
    person2 sato2, *p2;
    p2 = &sato2; // 実体のアドレス
 
    *p2 = *p1; // アドレス先の値を共有
    printf("%sは%d歳で、%sで%sをしています\n", sato2.name, sato2.age, sato2.add, sato2.job);
 
    // 自作の比較用関数を使用
    if(buf_equal(&sato, &sato2) == 0) {
        printf("構造体の実体%sと%sは同じです", "sato", "sato2");
    } else {
        printf("構造体の実体%sと%sは別です", "sato", "sato2");
    }
 
    /* パディングにより比較結果が正しくない可能性あり
    if(memcmp(&sato, &sato2, sizeof(person2)) == 0) {
        printf("構造体の実体%sと%sは同じです", "sato", "sato2");
    } else {
        printf("構造体の実体%sと%sは別です", "sato", "sato2");
    }
    */
 
    return 0;
}

実行結果:

S.Satoは25歳で、Osakaでnurseをしています
S.Satoは25歳で、Osakaでnurseをしています
構造体の実体satoとsato2は同じです

このサンプルコードでは、先ほどアドレス先の値をコピーした例を使って、実体が同じかどうか評価しています。比較評価のために自作の関数「buf_equal」を記述して使用しています。メンバを一つずつバイト単位で比較評価しています。

C言語にはヘッダーファイル「string.h」をインクルードするとmemcmp関数でバイト単位の評価ができますが、構造体の実体を比較する場合不具合が発生する可能性があります。これはパディングが起こる可能性があるからです。

パンディングとは奇数個の要素をもつchar型配列をメンバとしてもつ場合に、処理環境によっては1バイト空領域を追加して偶数個に揃える処理のことです。これによりあるメンバのアドレス先からズレが生じることもありますので、memcmp関数を使って評価する場合は注意が必要です。

メンバの値を代入してコピーをするとこのパンディングによるアドレス先のズレが生じることもあります。実体をコピーする場合には先の例のように実体全体のアドレス先の値をコピーし、かつ比較評価する場合はメンバ一つずつバイト単位で評価することをおススメします。

構造体を配列で扱う方法

構造体を配列で扱う方法は、変数を配列で扱う場合と変わりません。変数の配列のように以下のように宣言します。

構造体の型名 配列名[要素数];

サンプルコードで確認しましょう。

#include <stdio.h>
 
// 構造体
typedef struct {
    char *name;
    char sex;
    int age;
    char *add;
    char *job;
} person;
 
// 一括代入用の関数
person init(char *name, char sex, int age, char *add, char* job) {
    person p;
    p.name = name;
    p.sex = sex;
    p.age = age;
    p.add = add;
    p.job = job;
    return p;
}
 
int main(void) {
    // 構造体の実体を生成
    person p[3];
    
    // 関数で一括代入
    p[0] = init("T.Tanaka", 'm', 30, "Tokyo", "teacher");
    p[1] = init("S.Sato", 'f', 25, "Osaka", "nurse");
    p[2] = init("K.Sato", 'm', 35, "Nagoya", "doctor");
    
    for(int i = 0; i < 3; i++) {
        printf("%sは%d歳で、%sで%sをしています\n", p[i].name, p[i].age, p[i].add, p[i].job);
    }
    return 0;
}

実行結果:

T.Tanakaは30歳で、Tokyoでteacherをしています
S.Satoは25歳で、Osakaでnurseをしています
K.Satoは35歳で、Nagoyaでdoctorをしています

配列の使い方については、こちらで詳しく解説していますので、ぜひ参考にしてください。

構造体を関数の引数で使用する方法について

構造体の実体を関数の引数で使用する方法について説明します。サンプルコードで確認していきましょう。

#include <stdio.h>
 
// 構造体
typedef struct person {
    char *name;
    char sex;
    int age;
    char *add;
    char *job;
} person2;
  
// person2型の構造体を引数にもつ関数
void subst(person2 *new_p2, person2 p2) {
    *new_p2 = p2;
}
 
int main(void) {
    // 構造体の実体の生成と初期化
    person2 sato = {"S.Sato", 'f', 25, "Osaka", "nurse"};
    printf("%sは%d歳で、%sで%sをしています\n", sato.name, sato.age, sato.add, sato.job);
 
    person2 p2 = {"K.Sato", 'm', 35, "Nagoya", "doctor"};
    subst(&sato, p2); // 構造体の実体を引数で使用
    printf("%sは%d歳で、%sで%sをしています\n", sato.name, sato.age, sato.add, sato.job);
 
    return 0;
}

実行結果:

S.Satoは25歳で、Osakaでnurseをしています
K.Satoは35歳で、Nagoyaでdoctorをしています

このサンプルコードではすでに初期化された実体をコピーするための関数「subst」を使用しています。

「subst」関数ではすでに初期化された実体のアドレス先の値を引数で参照渡ししています。main関数ではすでに初期化された実体「sato」を別の実体「p2」のメンバの値で書き換えています。

構造体を関数の引数で使用する方法については、こちらのサイトでも解説していますので、ぜひ参考にしてください。

ポインタについて詳しく知りたい方へ

これまでお伝えしてきたように、構造体の実体はポインタを使って扱うと便利な場合が多いです。ポインタの使い方については、こちらで詳しく解説していますので、ぜひ参考にしてください。

まとめ

ここでは、構造体の使い方について説明しました。構造体を使うと関連する項目をひとまとまりに扱うことができて便利です。

また構造体のメンバ全体をまとめて扱う場合はポインタを使うと、実体のアドレス先の情報ひとつで操作することができて簡単です。

使いこなすことができるように、この記事を何度も参考にして下さいね!

この記事を書いた人

【プロフィール】
DX認定取得事業者に選定されている株式会社SAMURAIのマーケティング・コミュニケーション部が運営。「質の高いIT教育を、すべての人に」をミッションに、IT・プログラミングを学び始めた初学者の方に向け記事を執筆。
累計指導者数4万5,000名以上のプログラミングスクール「侍エンジニア」、累計登録者数1万8,000人以上のオンライン学習サービス「侍テラコヤ」で扱う教材開発のノウハウ、2013年の創業から運営で得た知見に基づき、記事の執筆だけでなく編集・監修も担当しています。
【専門分野】
IT/Web開発/AI・ロボット開発/インフラ開発/ゲーム開発/AI/Webデザイン

目次