閉じる

C言語のアロー演算子とは?ドット演算子との違いと使い方・注意点

C言語で構造体とポインタを学び始めると、必ず登場するのがアロー演算子(->)です。

ドット演算子との違いがあいまいなまま進んでしまうと、エラーの原因になったり、バグを埋め込みやすくなってしまいます。

本記事では、アロー演算子の基本から応用、さらにありがちなエラーと対策までを、図解とコード例を交えながら丁寧に解説します。

C言語のアロー演算子とは?基本の意味と役割

アロー演算子(->)の定義と読み方

アロー演算子は、C言語で構造体ポインタが指している先のメンバにアクセスするための演算子です。

  • 記号: ->
  • 読み方: 「アロー」「アロー演算子」「矢印」などと読みます。
  • 役割: 「ポインタが指す構造体」の「中のメンバ」を取り出すためのショートカット記法です。

正式には、次のような意味を持ちます。

p->x(*p).x の略記です。

ここで p構造体へのポインタでなければなりません。

この前提を外れるとコンパイルエラーや実行時エラーにつながります。

ドット演算子(.)との根本的な違い

ドット演算子(.)アロー演算子(->)の根本的な違いは、「左側に来るものがポインタか、実体(値)か」です。

  • ドット演算子(.)
    • 左側: 構造体変数そのもの(実体)
    • 例: person.age
  • アロー演算子(->)
    • 左側: 構造体へのポインタ
    • 例: p->age (p は構造体へのポインタ)

この違いを、表で整理します。

演算子左側に必要なもの読み替え
.構造体の実体(値)person.age
->構造体へのポインタp->age(*p).age

「値にはドット」「ポインタにはアロー」と覚えておくと混乱しにくくなります。

構造体ポインタとアロー演算子の関係

アロー演算子は「構造体ポインタが指している先」を自動的に参照して、そのメンバにアクセスするための演算子です。

  • *p で「ポインタが指している構造体の実体」を取り出します。
  • さらに (*p).age と書くことで、そのメンバageにアクセスできます。
  • これを簡潔にしたのが p->age です。

つまり、アロー演算子は「間にある*.をまとめて書ける記号」と考えると理解しやすくなります。

構造体とアロー演算子の使い方

構造体(struct)とドット演算子の基本

まずは、アロー演算子の前提になる構造体とドット演算子の基本を押さえます。

以下のような構造体を例にします。

C言語
#include <stdio.h>

struct Person {
    char  name[32];  // 名前
    int   age;       // 年齢
    float height;    // 身長
};

int main(void) {
    struct Person p1 = {"Taro", 20, 170.5f};

    // ドット演算子でメンバにアクセス
    printf("名前: %s\n", p1.name);
    printf("年齢: %d\n", p1.age);
    printf("身長: %.1f\n", p1.height);

    return 0;
}
実行結果
名前: Taro
年齢: 20
身長: 170.5

このように構造体変数そのものを持っている場合は、p1.name のようにドット演算子を使います。

構造体ポインタの宣言と初期化

アロー演算子は構造体へのポインタがあって初めて意味を持ちます。

C言語
#include <stdio.h>

struct Person {
    char  name[32];
    int   age;
    float height;
};

int main(void) {
    struct Person p1 = {"Hanako", 25, 160.2f};

    // 構造体へのポインタを宣言し、p1 のアドレスで初期化
    struct Person *pp = &p1;

    // ポインタの中身(アドレス)を表示してみる
    printf("p1 のアドレス: %p\n", (void *)&p1);
    printf("pp の値(指している先): %p\n", (void *)pp);

    return 0;
}
実行結果
p1 のアドレス: 0x7ff... (環境によって異なる)
pp の値(指している先): 0x7ff... (上と同じ値)

このように「構造体変数のアドレス」を取ることで、構造体へのポインタを用意できます。

ポインタで構造体メンバにアクセスする2通り

構造体ポインタからメンバにアクセスする方法は2通りあります。

C言語
#include <stdio.h>

struct Person {
    char  name[32];
    int   age;
    float height;
};

int main(void) {
    struct Person p1 = {"Ken", 30, 175.0f};
    struct Person *pp = &p1;

    // 1. 間接参照(*) と ドット(.) を組み合わせる方法
    printf("(*pp).age = %d\n", (*pp).age);

    // 2. アロー演算子(->)を使う方法
    printf("pp->age   = %d\n", pp->age);

    return 0;
}
実行結果
(*pp).age = 30
pp->age   = 30

両方まったく同じ意味ですが、アロー演算子のほうが読みやすく簡潔なので、実務ではほぼ常にpp->ageが使われます。

ドット演算子とアロー演算子の書き換え(「(*p).x」と「p->x」)

書き換えのルールは1つだけです。

  • p->x(*p).x

この関係を意識しておけば、コンパイラエラーの原因を見つけるときにも役立ちます。

例えば次のように、どちらも同じ意味です。

C言語
pp->height = 180.0f;
// は
(*pp).height = 180.0f;
// と同じ意味

文章にすると、 「pp が指す構造体の height メンバに 180.0 を代入する」という意味になります。

アロー演算子の応用と書き方のコツ

アロー演算子はどこまで繋げられるか

アロー演算子は、「ポインタをたどれる限り」繋げて書くことができます。

よく出てくる例が、連結リストです。

C言語
#include <stdio.h>

struct Node {
    int value;
    struct Node *next;  // 次のノードを指すポインタ
};

int main(void) {
    struct Node n3 = {30, NULL};
    struct Node n2 = {20, &n3};
    struct Node n1 = {10, &n2};

    struct Node *head = &n1;

    // 1番目のノード
    printf("1番目: %d\n", head->value);
    // 2番目のノード
    printf("2番目: %d\n", head->next->value);
    // 3番目のノード
    printf("3番目: %d\n", head->next->next->value);

    return 0;
}
実行結果
1番目: 10
2番目: 20
3番目: 30

アロー演算子の数に理論上の制限はありませんが、長くなりすぎると読みづらくなるため、途中で一時変数に分解することをおすすめします。

ネストした構造体・構造体配列でのアロー演算子

ネスト(入れ子)された構造体や構造体配列では、アロー演算子とドット演算子、配列添字[]を組み合わせることになります。

C言語
#include <stdio.h>

struct Player {
    char name[32];
    int  score;
};

struct Team {
    char name[32];
    struct Player players[3];  // プレイヤー3人分の配列
};

int main(void) {
    struct Team team = {
        "Red",
        {
            {"Alice", 10},
            {"Bob",   20},
            {"Carol", 30}
        }
    };

    struct Team *pt = &team;

    // 2番目のプレイヤー(Bob)のスコアにアクセス
    printf("Team: %s\n", pt->name);
    printf("Player: %s\n", pt->players[1].name);
    printf("Score: %d\n", pt->players[1].score);

    return 0;
}
実行結果
Team: Red
Player: Bob
Score: 20

ここでの式pt->players[1].scoreは、次のように分解して理解できます。

  1. pt->players で「プレイヤー配列」を取り出す
  2. [1] で「2番目の要素」を選ぶ
  3. .score で「そのプレイヤーのスコア」を取り出す

アローとドットと[]が混ざるときほど、段階的に読む意識が重要です。

関数ポインタをメンバに持つ構造体とアロー演算子

アロー演算子は関数ポインタをメンバに持つ構造体でもよく使われます。

GUIやコールバック処理の設計でよく登場します。

C言語
#include <stdio.h>

// ボタンを表す構造体
struct Button {
    const char *label;
    void (*onClick)(struct Button *self);  // 自分自身を受け取る関数ポインタ
};

// クリックされたときに呼ばれる関数
void handleClick(struct Button *self) {
    printf("Button \"%s\" clicked!\n", self->label);
}

int main(void) {
    struct Button okButton = {"OK", handleClick};

    struct Button *pBtn = &okButton;

    // 関数ポインタメンバにアクセスして呼び出す
    pBtn->onClick(pBtn);  // (*pBtn).onClick(pBtn) と同じ

    return 0;
}
実行結果
Button "OK" clicked!

この例では、pBtn->onClick「関数へのポインタ」を表し、それに(pBtn)という引数リストを付けて関数を実行しています。

C言語とC++におけるアロー演算子の違い比較

C言語とC++では、アロー演算子の「使える場所」が異なります

  • C言語
    • 対象: 構造体ポインタ、共用体ポインタのみ
    • 意味: (*p).x の略記としてのみ使われる
  • C++
    • 対象:
      • 構造体・クラスへのポインタ
      • スマートポインタ(例: std::unique_ptr)
      • イテレータなど演算子->をオーバーロードしたオブジェクト
    • 意味: クラスのメンバ関数呼び出しなど、オブジェクト指向的に拡張される

C言語では「演算子オーバーロード」は存在しないため、アロー演算子の意味は常に固定です。

このシンプルさは、逆に理解しやすいとも言えます。

読みやすいアロー演算子の書き方

読みやすさを保つコツとして、次のような点を意識すると良いです。

  1. アローの連鎖が長くなるときは一時変数で区切る
    • 悪い例: head->next->next->next->value
    • 良い例:
      struct Node *second = head->next;
      struct Node *third  = second->next;
      int value = third->value;
      
  2. ポインタの型がわかりにくいときはコメントを添える
    struct Team *team = get_team();  // チーム情報へのポインタ
    printf("%s\n", team->players[0].name);
    
  3. アローとドット、添字[]の組み合わせは段階的に読む
    • まずアローで「どの構造体か」を決める
    • 次に添字で「どの要素か」を決める
    • 最後にドットで「どのメンバか」を決める

読み手が一度で理解できるように意識することが大切です。

アロー演算子の注意点とエラー対策

NULLポインタでのアロー演算子使用によるクラッシュ

NULLポインタに対してアロー演算子を使うと、ほぼ確実に実行時エラー(クラッシュ)になります。

C言語
#include <stdio.h>

struct Data {
    int value;
};

int main(void) {
    struct Data *p = NULL;

    // 危険: p はどこも指していない
    // printf("%d\n", p->value);  // 実行時エラー(セグメンテーションフォルトなど)

    return 0;
}

このような問題を避けるために、アロー演算子を使う前には NULL チェックを行うのが基本です。

C言語
if (p != NULL) {
    printf("%d\n", p->value);
}

「NULL の可能性があるポインタに対して、チェックなしでアローを使わない」という習慣を付けておくと、バグを大きく減らせます。

未初期化ポインタとダングリングポインタの危険性

アロー演算子が危険なのはNULLだけではありません

  1. 未初期化ポインタ
    1. 宣言しただけで値を入れていないポインタ
      struct Data *p;  // 初期化していない
      p->value = 10;   // どこに書いているか分からない → 非常に危険
      
  2. ダングリングポインタ
    1. すでにfreeされたメモリを指し続けているポインタ
      struct Data *p = malloc(sizeof(struct Data));
      free(p);
      p->value = 10;  // 解放済みメモリへのアクセス → 未定義動作
      

どちらも「どこを指しているか保証されていないポインタに対してアロー演算子を使っている」点が共通しています。

対策としては、次のような習慣が効果的です。

  • 宣言時にNULLで初期化する
  • freeしたら、すぐにp = NULL;とする
  • デバッガや静的解析ツール(例: Valgrind、clang-tidy など)を活用する

「ポインタでないもの」に対するアロー演算子使用エラー

アロー演算子の左側には、必ず「構造体(または共用体)へのポインタ型」が来なければなりません。

以下はコンパイルエラーになります。

C言語
struct Person {
    int age;
};

int main(void) {
    struct Person p1 = {20};

    // エラー: p1 はポインタではなく構造体の実体
    // printf("%d\n", p1->age);

    return 0;
}

この場合はp1.ageドット演算子を使う必要があります。

もしコンパイラからmember reference base type 'struct Person' is not a pointerのようなエラーが出た場合は、「左側がポインタなのか実体なのか」を見直すと原因が分かりやすくなります。

const構造体ポインタとアロー演算子の扱い

const 修飾された構造体ポインタでも、アロー演算子は普通に使えます。

ただし書き込みは禁止されます。

C言語
#include <stdio.h>

struct Config {
    int timeout;
    int retries;
};

void print_config(const struct Config *cfg) {
    // 読み出しは OK
    printf("timeout = %d, retries = %d\n",
           cfg->timeout, cfg->retries);

    // 書き込みは NG
    // cfg->timeout = 100;  // コンパイルエラー
}

int main(void) {
    struct Config config = {30, 3};
    print_config(&config);
    return 0;
}
実行結果
timeout = 30, retries = 3

ここでのポイントは、「ポインタが指す先の構造体が const かどうか」です。

  • const struct Config *cfg
    • 構造体の中身を書き換え不可
    • cfg->timeout = ...; はエラー
  • struct Config * const cfg
    • ポインタ自体を書き換え不可
    • cfg = &otherConfig; はエラー
    • cfg->timeout の書き換えは可能

アロー演算子自体の書き方は同じですが、「何が const なのか」を意識しておくことが大切です。

アロー演算子のデバッグ・練習方法

アロー演算子を確実に理解するには、手を動かして小さな実験を繰り返すのが最も有効です。

練習・デバッグの具体的な方法としては、次のようなステップが挙げられます。

  1. 最小限の構造体を定義する
    1. 例: struct Point { int x, y; };
  2. 構造体変数とポインタを両方作る
    1. struct Point p = {1, 2};
    2. struct Point *pp = &p;
  3. ドットとアローの両方で同じメンバを表示してみる
    1. printf("%d\n", p.x);
    2. printf("%d\n", pp->x);
  4. デバッガで p, &p, pp, *pp の値を観察する

例えば、以下のような練習用コードを作り、ステップ実行してみると理解が深まります。

C言語
#include <stdio.h>

struct Point {
    int x;
    int y;
};

int main(void) {
    struct Point p = {10, 20};
    struct Point *pp = &p;

    // ここにブレークポイントを置き、p, &p, pp, *pp を観察するとよい
    printf("p.x = %d, p.y = %d\n", p.x, p.y);
    printf("pp->x = %d, pp->y = %d\n", pp->x, pp->y);

    return 0;
}
実行結果
p.x = 10, p.y = 20
pp->x = 10, pp->y = 20

オンラインの C 実行環境や、Visual Studio Code、CLion などの IDE には、変数ウォッチ機能があるものが多いので、実行しながらメモリや変数の中身を目で確認してみてください。

まとめ

アロー演算子(->)は、「構造体へのポインタ」から「その先のメンバ」にアクセスするためのショートカットであり、p->x(*p).xの略記です。

値にはドット、ポインタにはアローという原則を守れば、使い分けで迷うことはほとんどありません。

ただし、NULLポインタや未初期化ポインタ、解放済みメモリを指すダングリングポインタに対するアロー演算子の使用は深刻なバグにつながります。

小さなサンプルでドットとの対応関係を確認しながら、デバッガでポインタと構造体の中身を観察する練習を重ねることで、アロー演算子を安全かつ自在に使いこなせるようになります。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!