C言語のきほん|構造体のアドレスとポインタの使い方

構造体のアドレスとポインタを理解すると、入力も参照もぐっと自然に書けるようになる。

構造体を使えるようになると、関連するデータをひとまとめにして扱えるようになります。
ただ、構造体を本当に実用的に使っていくには、もう一歩進んでアドレスポインタの考え方に慣れておくことが大切です。

C言語では、構造体そのもののアドレスも取れますし、その中にある各メンバのアドレスも取れます。
このアドレスは、ポインタとして使えるだけでなく、scanf でキーボード入力を受け取るときにも欠かせません。

ここで少し注意したいのが、すべてに同じように & を付けるわけではないという点です。
構造体変数そのものには & を付けますが、構造体の配列では配列名がすでに先頭要素を指す形で扱われます。
また、メンバが int のような普通の変数なら & が必要ですが、メンバが char 型配列なら配列名そのものを渡します。

このあたりは、最初のうちは少しややこしく見えるかもしれません。
でも、変数と配列の違いを落ち着いて整理すると、実はこれまで学んできた配列やポインタの知識がそのままつながっているだけだと分かります。

今回は、構造体のアドレス取得、構造体配列との違い、scanf での使い方、そしてポインタとつながる考え方まで、順番にやさしく見ていきましょう。

構造体のアドレスを取得するとはどういうことか

C言語では、構造体の変数もメモリ上に置かれています。
ということは、普通の int 型変数と同じように、その構造体がメモリのどこにあるのかを表すアドレスを取ることができます。

たとえば、次のような構造体変数があるとします。

Student st;

この st という変数全体のアドレスは、次のように書くと取得できます。

&st

これは「構造体 Student 型の変数 st が保存されている場所」を表します。

さらに、構造体の各メンバもメモリ上に存在しているので、たとえば整数メンバ id のアドレスなら、

&st.id

のように書けます。

つまり、構造体では

  • 構造体全体のアドレス
  • 構造体の中の各メンバのアドレス

の両方を扱えるわけです。

なぜアドレスを取得する必要があるのか

構造体のアドレスを取る場面はいくつかありますが、特に大事なのは次の2つです。

用途内容
ポインタとして使う構造体を関数に渡したり、参照したりできる
scanf で値を入力する入力先の場所を指定するためにアドレスが必要

scanf は、キーボードから読み取った値を変数へ書き込む関数です。
そのため、「どこへ書き込むのか」という保存先のアドレスを渡さなければなりません。

たとえば整数メンバ id に値を入れたいなら、

scanf("%d", &st.id);

のように書きます。

構造体変数と構造体配列ではアドレスの考え方が少し違う

ここがとても大切なポイントです。

構造体の変数の場合

構造体の変数は、普通の変数と同じように扱います。
そのため、アドレスを取りたいときは & を付けます。

&st

構造体の配列の場合

一方、構造体の配列は配列です。
配列名は式の中で先頭要素へのポインタのように扱われるので、先頭要素のアドレスを使いたいときは & を付けなくてもよいです。

students

これは、次のものと同じアドレスを表します。

&students[0]

つまり、構造体配列でも基本は普通の配列と同じです。

まずは表で整理する

対象書き方意味
構造体変数全体&stst 全体のアドレス
構造体配列の先頭studentsstudents[0] の先頭アドレス
構造体配列の先頭要素&students[0]students[0] のアドレス
構造体の整数メンバ&st.idid メンバのアドレス
構造体の文字配列メンバst.name文字配列の先頭アドレス

この表の最後の2行もとても重要です。
メンバが普通の変数なのか、配列なのかで書き方が変わります。

メンバが変数のときは & を付ける

たとえば、構造体のメンバが int や double のような普通の変数なら、scanf に渡すときには & が必要です。

scanf("%d", &st.id);
scanf("%lf", &st.score);

これは、これまでの普通の変数入力と同じ考え方です。

  • id は int 型の変数
  • score は double 型の変数

なので、入力先のアドレスを渡します。

メンバが配列のときは & を付けない

一方で、メンバが char 型配列のときは少し違います。
たとえば、名前を表す name が char name[20]; なら、scanf に渡すときは & を付けません。

scanf("%19s", st.name);

これは、name が配列だからです。
配列名は先頭要素のアドレスとして扱われるため、すでにポインタのような役割を持っています。

ここを間違えて

scanf("%19s", &st.name);

と書かないように注意しましょう。
学習の初期段階では、文字配列には & を付けないと覚えておくと整理しやすいです。

この図では、構造体全体のアドレス、メンバのアドレス、配列名の扱いの違いをまとめて確認できます。
id は普通の変数なので &st.id と書きますが、name は配列なので st.name と書くだけで先頭アドレスを表します。
また、students という配列名は &students[0] と同じ位置を指すことも、視覚的に理解しやすくなります。

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

ここでは、商品情報を例にして解説します。

ファイル名:15_7_1.c

#include <stdio.h>

// 商品情報を表す構造体
typedef struct {
    int code;         // 商品番号
    char name[20];    // 商品名
} Item;

int main(void)
{
    // 構造体変数を宣言
    Item item;

    // 構造体配列を宣言
    Item items[3];

    // 構造体変数と配列先頭のアドレスを表示
    printf("&item      : %p\n", (void *)&item);
    printf("&items[0]  : %p\n", (void *)&items[0]);
    printf("items      : %p\n", (void *)items);

    // 構造体変数へ入力
    printf("\n商品番号を入力> ");
    scanf("%d", &item.code);
    printf("商品名を入力> ");
    scanf("%19s", item.name);

    // 構造体配列へ入力
    for (int i = 0; i < 3; i++) {
        printf("\n商品%dの番号を入力> ", i + 1);
        scanf("%d", &items[i].code);
        printf("商品%dの名前を入力> ", i + 1);
        scanf("%19s", items[i].name);
    }

    // 構造体変数の内容を表示
    printf("\n【単独の商品情報】\n");
    printf("%d %s\n", item.code, item.name);

    // 構造体配列の内容を表示
    printf("\n【商品一覧】\n");
    for (int i = 0; i < 3; i++) {
        printf("items[%d]: %d %s\n", i, items[i].code, items[i].name);
    }

    return 0;
}

このサンプルの大事な見どころ

まず、次の2行です。

printf("&items[0]  : %p\n", (void *)&items[0]);
printf("items      : %p\n", (void *)items);

この2つは同じアドレスになるはずです。
なぜなら、配列名 items は先頭要素 items[0] の先頭アドレスとして扱われるからです。

次に、scanf の部分もとても重要です。

scanf("%d", &item.code);
scanf("%19s", item.name);

ここでは、

  • code は int 型の変数なので & を付ける
  • name は char 型配列なので & を付けない

という違いがはっきり出ています。

構造体配列の入力でも同じ考え方です。

scanf("%d", &items[i].code);
scanf("%19s", items[i].name);
scanf("%d", &items[i].code);
scanf("%19s", items[i].name);

%p でアドレスを表示するときの注意

アドレスを表示するときは %p を使います。
このとき、引数は一般に void * にして渡す形が分かりやすく安全です。

printf("%p\n", (void *)&item);

構造体のアドレスはポインタとして使える

ここまでで、構造体のアドレスが取れることは分かりました。
では、そのアドレスは何に使えるのでしょうか。
答えは、構造体へのポインタとして使える、ということです。

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

Item item = {100, "Mouse"};
Item *p = &item;

この p は、item を指すポインタです。
つまり、p を通して item の中身にアクセスできるようになります。

構造体ポインタの基本の書き方

構造体ポインタを宣言するには、次のように書きます。

Item *p;

これは、「Item 型の構造体を指すポインタ p」を意味します。

そこへ構造体変数のアドレスを入れるには、

p = &item;

と書きます。

これで p は item を指すようになります。

ポインタを通してメンバへアクセスする方法

ポインタを通して構造体のメンバにアクセスするには、2つの書き方があります。

1つ目の書き方

(*p).code

2つ目の書き方

p->code

こちらの方が短く、実際によく使われます。

書き方意味
(*p).codep が指す構造体の code
p->code上と同じ意味

ポインタを使った簡単な例

ファイル名:15_7_2.c

#include <stdio.h>

typedef struct {
    int code;
    char name[20];
} Item;

int main(void)
{
    Item item = {200, "Keyboard"};
    Item *p = &item;

    printf("商品番号: %d\n", p->code);
    printf("商品名: %s\n", p->name);

    return 0;
}

なぜ構造体ポインタが大事なのか

構造体ポインタは、今後とても重要になります。

場面理由
関数に構造体を渡すコピーを避けて効率よく扱える
動的メモリ確保と組み合わせるmalloc などで扱いやすい
構造体配列を順に処理するポインタ演算と相性がよい

この図では、ポインタ p が構造体 item の場所を指していることを表しています。
p 自体は構造体の中身を持っているのではなく、item がどこにあるかという場所の情報を持っています。
そのため、p->code や p->name のように書くことで、p が指している先の構造体のメンバにアクセスできます。

scanf と構造体のアドレスの関係を整理

入力先書き方
構造体の int メンバscanf("%d", &st.id);
構造体の double メンバscanf("%lf", &st.value);
構造体の char 配列メンバscanf("%19s", st.name);
構造体配列の int メンバscanf("%d", &students[i].id);
構造体配列の char 配列メンバscanf("%19s", students[i].name);

よくある間違い

文字列メンバに & を付けてしまう

scanf("%19s", &st.name);

と書いてしまうケースです。
学習中は、文字配列には & を付けないと整理しておくのがよいです。

構造体配列に毎回 & を付けたくなる

配列名そのものは先頭要素のアドレスとして扱われるので、先頭を表したいなら配列名だけで十分です。

students

&students[0]

と同じ場所を指します。

実践問題1

次の要件を満たすプログラムを作成してください。

① 次の表の社員の情報を管理する構造体を定義する。ただし、連絡情報と評価情報はそれぞれ構造体で定義し、それをメンバとして含む入れ子構造にする。

ID氏名連絡情報(内線 / 部署コード)評価情報(実務 / 協調性 / 技術)
1松田 恒一101 / 2078 / 82 / 75
2木村 拓真102 / 3085 / 88 / 91
3西田 恒一103 / 1074 / 79 / 80
4藤井 恒一104 / 4090 / 84 / 86
5森本 颯真105 / 2069 / 77 / 73
6安藤 悠真106 / 5088 / 90 / 87

② 表の情報を表示する。
③ 内線、部署コード、実務、協調性、技術について平均を求めて表示する。

解答例

ファイル名:15_7_3.c

#include <stdio.h>

typedef struct {
    int extension;   // 内線番号
    int dept_code;   // 部署コード
} Contact;

typedef struct {
    int practical;   // 実務
    int teamwork;    // 協調性
    int skill;       // 技術
} Review;

typedef struct {
    int id;              // 社員番号
    char name[20];       // 氏名
    Contact contact;     // 連絡情報
    Review review;       // 評価情報
} Employee;

int main(void)
{
    Employee employees[6] = {
        {1, "松田 恒一", {101, 20}, {78, 82, 75}},
        {2, "木村 拓真", {102, 30}, {85, 88, 91}},
        {3, "西田 恒一", {103, 10}, {74, 79, 80}},
        {4, "藤井 恒一", {104, 40}, {90, 84, 86}},
        {5, "森本 颯真", {105, 20}, {69, 77, 73}},
        {6, "安藤 悠真", {106, 50}, {88, 90, 87}}
    };

    double sum_ext = 0.0, sum_dept = 0.0;
    double sum_practical = 0.0, sum_teamwork = 0.0, sum_skill = 0.0;

    printf("【社員の一覧】\n");
    printf("ID  名前         内線  部署  実務  協調性  技術\n");

    for (int i = 0; i < 6; i++) {
        printf("%d  %-10s  %3d  %3d   %3d    %3d   %3d\n",
               employees[i].id,
               employees[i].name,
               employees[i].contact.extension,
               employees[i].contact.dept_code,
               employees[i].review.practical,
               employees[i].review.teamwork,
               employees[i].review.skill);

        sum_ext += employees[i].contact.extension;
        sum_dept += employees[i].contact.dept_code;
        sum_practical += employees[i].review.practical;
        sum_teamwork += employees[i].review.teamwork;
        sum_skill += employees[i].review.skill;
    }

    printf("平均                %.1f  %.1f  %.1f   %.1f  %.1f\n",
           sum_ext / 6,
           sum_dept / 6,
           sum_practical / 6,
           sum_teamwork / 6,
           sum_skill / 6);

    return 0;
}

解説

この問題では、連絡情報と評価情報を別構造体に分け、それを Employee の中に入れています。
そのため、employees[i].contact.extension や employees[i].review.skill のように、意味のまとまりを保ちながらアクセスできます。

実践問題2

実践問題1で作成した構造体配列の中から、各社員の評価点の合計が最も高い社員を見つけ、その情報を構造体変数 best_employee に格納してください。

解答例

ファイル名:15_7_4.c

#include <stdio.h>

typedef struct {
    int extension;
    int dept_code;
} Contact;

typedef struct {
    int practical;
    int teamwork;
    int skill;
} Review;

typedef struct {
    int id;
    char name[20];
    Contact contact;
    Review review;
} Employee;

int main(void)
{
    Employee employees[6] = {
        {1, "松田 恒一", {101, 20}, {78, 82, 75}},
        {2, "木村 拓真", {102, 30}, {85, 88, 91}},
        {3, "西田 恒一", {103, 10}, {74, 79, 80}},
        {4, "藤井 恒一", {104, 40}, {90, 84, 86}},
        {5, "森本 颯真", {105, 20}, {69, 77, 73}},
        {6, "安藤 悠真", {106, 50}, {88, 90, 87}}
    };

    int best_index = 0;
    int best_total = employees[0].review.practical
                   + employees[0].review.teamwork
                   + employees[0].review.skill;

    for (int i = 1; i < 6; i++) {
        int total = employees[i].review.practical
                  + employees[i].review.teamwork
                  + employees[i].review.skill;

        if (total > best_total) {
            best_total = total;
            best_index = i;
        }
    }

    Employee best_employee = employees[best_index];

    printf("【最高評価の社員】\n");
    printf("社員番号:%d\n", best_employee.id);
    printf("氏名:%s\n", best_employee.name);
    printf("実務:%d  協調性:%d  技術:%d\n",
           best_employee.review.practical,
           best_employee.review.teamwork,
           best_employee.review.skill);
    printf("合計点:%d\n", best_total);

    return 0;
}

実践問題3

次の要件を満たすプログラムを作成してください。

① x 座標と y 座標からなる点の構造体を定義する。
② 点の構造体を入れ子にして、開始座標と終了座標からなる線分の構造体を定義する。
③ ②の構造体で要素数3の配列を宣言し、scanf で値を入力する。
④ 長さが最も長い線分の情報を表示する。
⑤ 同じ長さの場合には添字の小さい方を優先とする。

解答例

ファイル名:15_7_5.c

#include <stdio.h>
#include <math.h>

typedef struct {
    int x;
    int y;
} Point;

typedef struct {
    Point start;
    Point end;
} Segment;

int main(void)
{
    Segment segments[3];

    for (int i = 0; i < 3; i++) {
        printf("線分%dの開始座標(x y)を入力> ", i + 1);
        scanf("%d %d", &segments[i].start.x, &segments[i].start.y);

        printf("線分%dの終了座標(x y)を入力> ", i + 1);
        scanf("%d %d", &segments[i].end.x, &segments[i].end.y);
    }

    int max_index = 0;
    double dx = segments[0].end.x - segments[0].start.x;
    double dy = segments[0].end.y - segments[0].start.y;
    double max_length = sqrt(dx * dx + dy * dy);

    for (int i = 1; i < 3; i++) {
        dx = segments[i].end.x - segments[i].start.x;
        dy = segments[i].end.y - segments[i].start.y;
        double length = sqrt(dx * dx + dy * dy);

        if (length > max_length) {
            max_length = length;
            max_index = i;
        }
    }

    printf("\n最も長い線分\n");
    printf("開始座標:(%d, %d)\n",
           segments[max_index].start.x,
           segments[max_index].start.y);
    printf("終了座標:(%d, %d)\n",
           segments[max_index].end.x,
           segments[max_index].end.y);
    printf("長さ:%.2f\n", max_length);

    return 0;
}

実践問題4

次の要件を満たすプログラムを作成してください。

① 学生の情報を管理する構造体に double 型の average を追加する。

typedef struct {
    int id;
    char name[20];
    int math;
    int english;
    int science;
    double average;
} Student;

② 学生の3教科の点数から average を求めて構造体配列に格納する。
③ 学生の情報を average の降順に並べ替える。
④ 平均点は、3教科の合計を 3.0 で割って求める。
⑤ 並べ替えには基本選択法を使用する。

解答例

ファイル名:15_7_6.c

#include <stdio.h>

typedef struct {
    int id;
    char name[20];
    int math;
    int english;
    int science;
    double average;
} Student;

int main(void)
{
    Student students[6] = {
        {1, "石田 恒一", 78, 65, 71, 0.0},
        {2, "上村 拓海", 88, 87, 92, 0.0},
        {3, "大西 翔太", 67, 94, 70, 0.0},
        {4, "川本 恒一", 85, 81, 78, 0.0},
        {5, "坂井 颯太", 65, 94, 83, 0.0},
        {6, "中原 恒一", 76, 82, 88, 0.0}
    };

    for (int i = 0; i < 6; i++) {
        students[i].average =
            (students[i].math + students[i].english + students[i].science) / 3.0;
    }

    printf("=== 並べ替え前のデータ ===\n");
    printf("番号 | 氏名       | 数学 | 英語 | 理科 | 平均点\n");
    printf("-----------------------------------------------\n");
    for (int i = 0; i < 6; i++) {
        printf("%d    | %-10s | %3d  | %3d  | %3d  | %.2f\n",
               students[i].id,
               students[i].name,
               students[i].math,
               students[i].english,
               students[i].science,
               students[i].average);
    }

    for (int i = 0; i < 5; i++) {
        int max_index = i;
        for (int j = i + 1; j < 6; j++) {
            if (students[j].average > students[max_index].average) {
                max_index = j;
            }
        }

        if (max_index != i) {
            Student temp = students[i];
            students[i] = students[max_index];
            students[max_index] = temp;
        }
    }

    printf("\n=== 平均点の降順に並べ替えたデータ ===\n");
    for (int i = 0; i < 6; i++) {
        printf("%d    | %-10s | %3d  | %3d  | %3d  | %.2f\n",
               students[i].id,
               students[i].name,
               students[i].math,
               students[i].english,
               students[i].science,
               students[i].average);
    }

    return 0;
}

実行結果例

=== 並べ替え前のデータ ===
番号 | 氏名       | 数学 | 英語 | 理科 | 平均点
-----------------------------------------------
1    | 石田 恒一 |  78  |  65  |  71  | 71.33
2    | 上村 拓海  |  88  |  87  |  92  | 89.00
3    | 大西 翔太  |  67  |  94  |  70  | 77.00
4    | 川本 恒一 |  85  |  81  |  78  | 81.33
5    | 坂井 颯太  |  65  |  94  |  83  | 80.67
6    | 中原 恒一 |  76  |  82  |  88  | 82.00

=== 平均点の降順に並べ替えたデータ ===
2    | 上村 拓海  |  88  |  87  |  92  | 89.00
6    | 中原 恒一 |  76  |  82  |  88  | 82.00
4    | 川本 恒一 |  85  |  81  |  78  | 81.33
5    | 坂井 颯太  |  65  |  94  |  83  | 80.67
3    | 大西 翔太  |  67  |  94  |  70  | 77.00
1    | 石田 恒一 |  78  |  65  |  71  | 71.33

解説

この問題では、平均点を計算して average メンバへ保存し、その後に選択ソートで並べ替えています。
入れ替えでは、

Student temp = students[i];
students[i] = students[max_index];
students[max_index] = temp;

のように、構造体代入を使って要素全体をまとめて交換しています。
構造体の代入ができることを理解していると、並べ替え処理がかなりすっきり書けます。

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

ポイント内容
構造体全体のアドレス&構造体変数 で取得できる
構造体配列の先頭配列名 は &配列[0] と同じ場所を示す
メンバが変数scanf では & を付ける
メンバが配列配列名をそのまま渡す
構造体ポインタ構造体のアドレスを保持できる
ポインタ経由のアクセスp->member の形で書ける