C言語基礎|構造体とアロー演算子

「ポインタ先の構造体に、迷わず一発アクセス!ドットとアローで“読みやすいC”にしよう。」

構造体に慣れてくると、次に必ず出会うのが「関数に構造体を渡して、中身を書き換えたい」という場面です。
でも、構造体をそのまま渡すと“コピー”が作られてしまい、関数の中で変更しても元の値は変わりません。

そこで使うのが 構造体へのポインタ
ポインタで渡せば、関数の中から元の構造体を直接更新できます。

そして、そのとき一緒に覚えておくと超ラクになるのが アロー演算子(->)です。
(*p).member のような書き方は、括弧が必要でミスしがち。
p->member を使うと、同じ意味を スッキリ安全に書けます。

この記事で扱うポイント(全体像)

テーマ何が分かる?
構造体ポインタ関数で“元の構造体”を更新できる理由
(*p).memポインタが指す構造体のメンバへアクセスする基本形
演算子の優先順位なぜ *p.mem がダメなのか
p->mem(*p).mem を短く・読みやすく書く方法
メンバアクセス演算子. と -> の役割の使い分け

まずは基本:構造体のメンバアクセス(.)

書式

obj.mem

意味

  • 構造体変数 obj の中のメンバ mem を表します。

item.price

ポインタが指す構造体のメンバへアクセスするには?

構造体へのポインタ p があるとき、p 自体は「住所」なので、そのままではメンバに触れません。
いったん *p で「指している実体」を取り出してから、. でメンバにアクセスします。

書式(基本形)

(*p).mem

意味

  • p が指す構造体(*p)のメンバ mem を表します。

図で理解:(*p).mem の意味

例として、構造体 Device と、そのポインタ p を考えます。

図:ポインタ p が device を指す

図の説明

  • p は device の先頭アドレスを持っています。
  • *p は「p が指す実体」なので、*p は device と同じもの(エイリアス)として扱えます。
  • だから、(*p).level は device.level と同じ場所を指します。

重要:*p.mem がダメな理由(優先順位)

ここが最初のつまずきポイントです。

書きたくなるけどダメ

*p.level

これは (p.level) のように解釈されてしまいます。
理由は、. のほうが * より優先順位が高いからです。

正しい書き方

(*p).level

アロー演算子(->)でスッキリ!

(*p).mem は括弧が必要で、書き忘れやすいです。
そこで、C言語には専用の短縮記法があります。それが **->(アロー演算子)**です。

表:アロー演算子の意味

表記意味同じ意味の書き方
p->memp が指す構造体のメンバ mem(*p).mem

書式

p->mem

例で比較:同じ意味でも読みやすさが変わる

どちらも同じ意味

(*p).level = 5;
p->level = 5;

どっちが安全?

  • p->level のほうが括弧が不要で、書き間違いを減らしやすい
  • 読む人も「ポインタ先のメンバだな」と一瞬で分かります

サンプルプログラム

設定値が不正なら初期値に直すプログラム例です。

仕様(このプログラムでやること)

  • 構造体 Device は id と level を持つ。
  • level が 0 以下なら、関数で level を 1 に修正する
  • 修正後の内容を表示する。

プロジェクト名:chap12-3-1 ソースファイル名:chap12-3-1.c

#include <stdio.h>

// 機器設定を表す構造体
struct Device {
    int id;     // 機器ID
    int level;  // レベル(1以上が正しい)
};

// d が指す Device の level が不正なら 1 に直す
void fix_level(struct Device *d)
{
    if (d->level <= 0) {
        d->level = 1;
    }
}

int main(void)
{
    struct Device dev = {101, 0};   // level が不正

    fix_level(&dev);

    printf("設定を確認しました。\n");
    printf("機器ID:%d\n", dev.id);
    printf("レベル:%d\n", dev.level);

    return 0;
}

このプログラムで登場する項目を丁寧に解説

struct Device { ... };

要素何をする?
struct DeviceDevice というタグ名を持つ構造体型を定義する
int id;機器IDのメンバを定義する
int level;レベルのメンバを定義する

説明
これは「機器設定カードのフォーマット」を作っています。まだ実体は作っていません。

struct Device dev = {101, 0};

要素何をする?
struct Device devDevice 型の変数 dev を作る
{101, 0}メンバを宣言順に初期化する(id=101, level=0)。

説明
ここでようやく「書き込めるカード(実体)」ができます。

fix_level(&dev);

要素何をする?
&devdev のアドレスを取る(ポインタを作る)
fix_level(...)関数にポインタを渡して、元の dev を更新できるようにする

説明
構造体を“コピー”で渡すのではなく、“住所”で渡すので、関数内の変更が dev に反映されます。

void fix_level(struct Device *d)

要素何をする?
struct Device *dDevice 型へのポインタを受け取る(元の構造体を触れる)
void戻り値は返さない(更新が目的)

説明
この関数は dev の中身を直す必要があるので、引数は struct Device ではなく struct Device * になります。

if (d->level <= 0) { d->level = 1; }

要素何をする?
d->leveld が指す構造体の level メンバへアクセスする
<= 0不正判定
d->level = 1level を 1 に更新する(元の dev が更新される)

説明
ここがこの記事の主役です。
d->level は (*d).level と同じ意味で、括弧ミスを防ぎつつ読みやすい表現です。

表:. と -> の使い分け(ここは暗記というより感覚でOK)

使いたい対象使う演算子
構造体“そのもの”(変数).dev.level
構造体へのポインタ->d->level

図:-> は「ポインタ先のメンバ」だよ、のイメージ

図の説明

  • d は dev の住所
  • -> を使うと「住所の先にある構造体の中のメンバ」を直感的に書けます

まとめ(重要ポイント)

  • 構造体を関数で更新したいなら、引数は 構造体ポインタ を使う。
  • ポインタ先のメンバアクセスは (*p).mem が基本形
  • ただし . のほうが * より強いので、*p.mem はダメ
  • (*p).mem は p->mem と同じ意味で、-> のほうがミスが減って読みやすい。
  • . と -> はまとめて「メンバアクセス演算子」と呼ばれる。