C言語のきほん|ポインタで構造体を操作する

構造体をポインタで扱えるようになると、C言語のデータ操作はもっと軽やかで実践的になる。

構造体を使うと、関連するデータをひとまとまりにして整理できます。
ここまでで、構造体の宣言、配列、入れ子、代入などを学んできたので、「複数の情報をまとめて扱う」という感覚はかなりつかめてきたはずです。

ここからさらに一歩進むときに大切になるのが、ポインタを使って構造体を指すという考え方です。
C言語では、構造体の変数そのものを直接扱うだけでなく、その構造体がメモリのどこにあるかを表すアドレスを使って操作することができます。

この方法が重要なのは、単に書き方が増えるからではありません。
ポインタで構造体を扱えるようになると、

  • 関数に構造体を効率よく渡せる
  • 大きな構造体をコピーせずに扱える
  • 構造体の配列を順番に処理しやすくなる
  • 入れ子構造体にも柔軟にアクセスできる

といった実践的なメリットが見えてきます。

最初は、* や & や -> が並んで少し複雑に見えるかもしれません。
でも、考え方の土台はこれまで学んだポインタの基本と同じです。
「構造体の変数を指すポインタ」を作り、そのポインタが指している先のメンバにアクセスする、という流れで理解すれば大丈夫です。

ここでは、まず構造体の変数をポインタで指す基本から始めて、アロー演算子の意味、(*p).member との違い、構造体配列や入れ子構造体への広がりまで、やさしく丁寧に見ていきましょう。

構造体をポインタで指すとは何か

構造体の変数も、普通の変数と同じようにメモリ上に置かれています。
ということは、その構造体が保存されている場所、つまりアドレスを取ることができます。

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

Book book = {101, "C言語入門"};

この book は構造体変数です。
そして、この変数のアドレスは次のように書くと取得できます。

&book

この &book をポインタ変数に入れておけば、そのポインタを通して book の中身を参照したり変更したりできるようになります。

つまり、構造体をポインタで指すというのは、

  • 構造体変数を用意する
  • そのアドレスをポインタに入れる
  • ポインタ経由でメンバにアクセスする

という流れです。

構造体ポインタの基本手順

構造体の変数をポインタで指すときの基本手順は、次の3段階で考えると分かりやすいです。

手順内容
1構造体型のポインタを宣言する
2構造体変数のアドレスをポインタに代入する
3ポインタを使って構造体のメンバにアクセスする

この流れは、普通のポインタ操作と同じです。
違うのは、指している対象が int や double ではなく、構造体そのものである点です。

まずは普通の変数ポインタと比べてみる

たとえば int 型の変数なら、こんな書き方でした。

int x = 10;
int *p = &x;
printf("%d\n", *p);

構造体の場合は、この考え方が次のように広がります。

Book book = {101, "C言語入門"};
Book *p = &book;

ここで p は、Book 型の構造体を指すポインタです。
つまり、*p は「p が指している Book 型の実体」を表します。

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

商品情報を例にして説明します。

ファイル名:15_8_1.c

#include <stdio.h>

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

int main(void)
{
    // Item型の変数を宣言して初期化
    Item item = {101, "ノート"};

    // Item型を指すポインタを宣言し、itemのアドレスを代入
    Item *p = &item;

    // ポインタを使って表示
    printf("%d %s\n", (*p).code, (*p).name);
    printf("%d %s\n", p->code, p->name);

    return 0;
}

実行結果例

101 ノート
101 ノート

このプログラムでは、item という構造体変数を、ポインタ p で指しています。
そして、2通りの書き方で同じメンバの値を表示しています。

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

まず、この部分で構造体を定義しています。

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

この Item 型は、商品番号と商品名をまとめた構造体です。

次に、構造体変数 item を宣言して初期化しています。

Item item = {101, "ノート"};

ここで、item の中には

  • code に 101
  • name に ノート

が入っています。

続いて、この構造体を指すポインタを宣言しています。

Item *p = &item;

この1行はとても大事です。
意味を分解すると、次のようになります。

部分意味
Item *pItem 型の構造体を指すポインタ p を宣言
&itemitem のアドレスを取得
=そのアドレスを p に代入

つまり、p は item の場所を覚えているポインタになった、ということです。

(*p).member という書き方

ポインタ p が構造体 item を指しているとき、p が指している先の構造体のメンバにアクセスするには、まず次のように書けます。

(*p).code
(*p).name

この書き方は、意味としてはとても素直です。

  • *p で「p が指している構造体本体」を取り出す
  • .code や .name で、その構造体のメンバにアクセスする

という流れになっています。

なぜ (*p).code のように括弧が必要なのか

ここはとても大切なポイントです。
もし次のように書いてしまうと、

*p.code

これは「p の code というメンバ」を先に見ようとしてしまいます。
なぜなら、ドット演算子の方が * より優先順位が高いからです。

そのため、「まず p を間接参照してから、その結果のメンバにアクセスする」ことをはっきり示すために、次のように括弧で囲みます。

(*p).code

この括弧は省略できない、と覚えておくのが大切です。

-> 演算子とは何か

構造体ポインタでは、(*p).member という書き方をもっと簡潔に書けるように、-> 演算子が用意されています。

たとえば、

(*p).code

は、次のように書けます。

p->code

同じように、

(*p).name

は、

p->name

と書けます。

つまり、-> は

  • ポインタが指す構造体
  • その中のメンバ

へアクセスするための専用の書き方です。

(*p).member と p->member の違い

意味は同じですが、書きやすさに違いがあります。

書き方意味特徴
(*p).codep が指す構造体の code仕組みが見えやすい
p->code上と同じ短くて実用的

実際のコードでは、ほとんどの場合 p->code のような書き方が使われます。
見やすく、間違いも起こりにくいからです。

この図では、ポインタ p が item を指している様子と、そこからメンバへアクセスする2つの書き方を並べて示しています。
(*p).code と p->code は、どちらも item の code を表しています。
つまり、-> 演算子は特別な別物ではなく、ポインタ経由で構造体メンバへアクセスするための便利な省略記法だと理解できます。

構造体ポインタを使うメリット

構造体をポインタで扱うと、いくつかの大きな利点があります。

利点内容
コピーを避けられる大きな構造体でも丸ごと渡さなくてよい
関数との相性がよい呼び出し先で元の構造体を変更できる
配列処理がしやすい先頭要素をポインタで順にたどれる
動的メモリと相性がよいmalloc で確保した構造体を扱いやすい

特に、構造体が大きくなると、関数に値渡しするたびにコピーが発生すると効率が落ちます。
そのため、ポインタで渡す考え方がとても重要になります。

構造体の変数をポインタで変更する例

ポインタは表示だけでなく、メンバの値を書き換えることもできます。

ファイル名:15_8_2.c

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

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

int main(void)
{
    Item item = {101, "ノート"};
    Item *p = &item;

    p->code = 202;
    strcpy(p->name, "ペン");

    printf("%d %s\n", item.code, item.name);

    return 0;
}

実行結果例

202 ペン

ここでは、ポインタ p を通して item の内容を書き換えています。
p は item を指しているので、p->code を変更すると、元の item.code も変わります。

文字列メンバを変更するときの注意

構造体のメンバが char 型配列の場合、文字列は = で代入できません。
これは通常の構造体でも同じでしたが、ポインタ経由でも同じです。

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

p->name = "ペン";

そのため、文字列を変更したいときは strcpy を使います。

strcpy(p->name, "ペン");

この点は、ポインタを使っていても変わりません。

構造体配列をポインタで見るとどうなるか

構造体配列でも、先頭要素を指すポインタを使うことができます。

たとえば次のような配列があるとします。

Item items[3] = {
    {101, "ノート"},
    {102, "ペン"},
    {103, "消しゴム"}
};

このとき、配列名 items は先頭要素 items[0] のアドレスとして扱われるので、

Item *p = items;

と書けます。

これは、次の意味と同じです。

Item *p = &items[0];

構造体配列をポインタでたどる例

ファイル名:15_8_3.c

#include <stdio.h>

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

int main(void)
{
    Item items[3] = {
        {101, "ノート"},
        {102, "ペン"},
        {103, "消しゴム"}
    };

    Item *p = items;

    for (int i = 0; i < 3; i++) {
        printf("%d %s\n", (p + i)->code, (p + i)->name);
    }

    return 0;
}

実行結果例

101 ノート
102 ペン
103 消しゴム

ここでは、p + i で i 番目の要素を指し、その先のメンバへ -> でアクセスしています。

配列の添字とポインタ表現の対応

構造体配列では、次のような対応があります。

配列での書き方ポインタでの書き方意味
items[i].code(p + i)->codei 番目の要素の code
items[i].name(p + i)->namei 番目の要素の name

最初は配列の添字で書く方が分かりやすいですが、ポインタ表現に慣れておくと、関数や動的メモリの学習につながりやすいです。

入れ子の構造体をポインタで扱う

構造体が入れ子になっていても、考え方は同じです。
たとえば、日付を含む書籍情報を考えてみましょう。

typedef struct {
    int year;
    int month;
    int day;
} Date;

typedef struct {
    char title[30];
    Date publish;
} Book;

この Book をポインタで指すなら、

Book *p = &book;

のように書けます。

そのうえで、入れ子の中のメンバへアクセスするときは、

p->publish.year
p->publish.month
p->publish.day

のように書きます。

入れ子構造体のポインタ例

ファイル名:15_8_4.c

#include <stdio.h>

typedef struct {
    int year;
    int month;
    int day;
} Date;

typedef struct {
    char title[30];
    Date publish;
} Book;

int main(void)
{
    Book book = {"C言語しっかり入門", {2024, 4, 12}};
    Book *p = &book;

    printf("書名:%s\n", p->title);
    printf("発売日:%d年%d月%d日\n",
           p->publish.year,
           p->publish.month,
           p->publish.day);

    return 0;
}

実行結果例

書名:C言語しっかり入門
発売日:2024年4月12日

ここでは、p が指しているのは Book 全体です。
そして、その中の publish は Date 型の構造体なので、さらに .year のようにたどっています。

図で入れ子構造体のポインタアクセスを確認する

この図では、ポインタ p が book を指しており、その book の中に入れ子になった publish 構造体があることを表しています。
最初に p->publish で Book の中の Date 型メンバへアクセスし、そのあと .year で Date の中の年を取り出します。
階層を順番にたどる感覚が分かると、入れ子構造体も落ち着いて読めるようになります。

よくある間違い

構造体ポインタでは、初心者がつまずきやすいポイントがあります。

*p.member と書いてしまう

これは優先順位の問題で意図通りに動きません。
正しくは、

(*p).member

です。

-> と . を混同する

  • 普通の構造体変数なら .
  • 構造体ポインタなら ->

という整理が大切です。

たとえば、

item.code
p->code

は正しいですが、

p.code

は誤りです。

文字列メンバに = で代入しようとする

ポインタ経由でも、char 型配列には直接代入できません。
必要なら strcpy を使います。

実践的にはどんな場面で使うか

構造体ポインタは、実際にはかなり多くの場面で使われます。

場面使い方の例
関数で構造体を更新したいポインタ引数で渡して中身を書き換える
大きなデータを効率よく扱いたい値渡しではなくアドレスを渡す
構造体配列をまとめて処理したい先頭ポインタで順にたどる
動的確保した構造体を使いたいmalloc の結果を構造体ポインタで受ける

つまり、構造体ポインタは単なる書き方の一種ではなく、C言語で本格的にデータを扱っていくための重要な道具です。

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

ポイント内容
構造体ポインタの宣言構造体型 *ポインタ名;
アドレスの代入&構造体変数 をポインタに入れる
メンバアクセス1(*p).member
メンバアクセス2p->member
よく使う書き方p->member
構造体配列との関係配列名は先頭要素を指す
入れ子構造体p->outer.inner のようにたどる

構造体をポインタで扱えるようになると、C言語のコードの読み方も書き方も一段深く理解できるようになります。
ここは少し記号が増えるところですが、考え方そのものはとても素直です。
「どこを指しているのか」を意識しながら読めるようになると、構造体とポインタの関係がすっきり見えてきます。