C言語で構造体をポインタ経由で扱うときに登場するのが「->(アロー演算子)」です。
ドット演算子(.)との使い分けに最初は戸惑いがちですが、仕組みはとてもシンプルです。
本記事では、アロー演算子の基本と書き方、ドット演算子との違い、実践的なコード例、そしてよくある間違いを丁寧に解説します。
構造体ポインタと->(アロー演算子)とは
構造体ポインタのメンバーにアクセスする演算子
構造体のメンバーにアクセスするには通常ドット演算子を使いますが、構造体の「ポインタ」からメンバーにアクセスする場合はアロー演算子(->)を使います。
例えば、Person *p
が構造体Person
を指すとき、p->age
のように書いてメンバーへアクセスします。
アロー演算子は「ポインタを間接参照して、その結果のメンバーにアクセスする」ことを1つにまとめた記法です。
機能的には(*p).age
と同じ意味ですが、p->age
のほうが簡潔で読みやすいです。
p->member と (*p).member は同じ意味
p->member
と(p).member
は等価です。
アロー演算子は「間接参照()とメンバーアクセス(
.
)」を合成した糖衣構文(syntactic sugar)です。
演算子の優先順位の都合で(*p).member
のように括弧が必要になります。
括弧を省くと別の意味に解釈されるため注意してください。
-> の使い方と.ドット演算子の違い
基本構文 p->member
基本は次の形です。
pointer_to_struct->member
たとえばPerson *p
がPerson
を指すなら、p->name
やp->age
のように書けます。
左辺が「ポインタ」であることが重要です。
変数には . 、ポインタには ->
実体(値そのもの)にはドット、ポインタにはアローというルールで覚えると混乱しにくくなります。
以下に使い分けを表で整理します。
対象 | 使う演算子 | 例 | 説明 |
---|---|---|---|
構造体の実体(変数) | . | s.age | s がstruct 変数であるとき |
構造体のポインタ | -> | p->age | p がstruct * のとき |
ポインタの先の配列メンバー | -> + 添字 | p->arr[i] | 配列メンバーarr にアクセス |
ネストしたポインタ | -> の連鎖 | p->child->name | child もポインタのとき |
(&s)->member の書き方
構造体変数s
のアドレスを取ってすぐアローでアクセスしたい場合は(&s)->member
と書きます。
括弧を省いて&s->member
とすると&(s->member)
に解釈される(演算子優先順位のため)ので、必ず括弧で&s
を囲むようにしてください。
初心者向けコード例
構造体の宣言とポインタ取得
以下の例では、親子のPerson
構造体を用意し、ポインタでアクセスします。
配列メンバーscores
も用意し、p->scores[i]
の形も合わせて示します。
#include <stdio.h>
#include <string.h>
// Person 構造体の定義
typedef struct Person {
char name[32];
int age;
struct Person *child; // 子どもを指すポインタ(なければ NULL)
int scores[3]; // 配列メンバーの例
} Person;
// NULL安全に表示する補助関数
void print_person(const Person *p) {
if (p == NULL) {
puts("[print_person] person は NULL です");
return;
}
printf("Person{name=\"%s\", age=%d}\n", p->name, p->age);
}
int main(void) {
// 子の実体(構造体変数)
Person child = {"Taro", 10, NULL, {80, 85, 90}};
// 親の実体(構造体変数)。あとでポインタ経由で埋める
Person parent;
// 親を指すポインタ p を取得
Person *p = &parent;
// ここから p->member の形でメンバーを書き込み(代入)
// 文字列の代入は snprintf を使うと安全
snprintf(p->name, sizeof(p->name), "%s", "Hanako");
p->age = 35;
p->child = &child; // 連鎖アクセスのために子へのポインタをセット
// 配列メンバー scores への書き込み
p->scores[0] = 70;
p->scores[1] = 75;
p->scores[2] = 80;
// ここから読み出し(アクセス)
printf("親: %s (%d歳)\n", p->name, p->age);
// ドット演算子とアロー演算子の比較
printf("ドット: parent.age=%d, アロー: p->age=%d\n", parent.age, p->age);
// (*p).member と p->member は同じ
printf("(*p).age=%d, p->age=%d\n", (*p).age, p->age);
// (&s)->member の例(括弧が必要)
printf("(&parent)->age=%d\n", (&parent)->age);
// 連鎖アクセス: p->child->name
printf("子: %s (%d歳)\n", p->child->name, p->child->age);
// 配列メンバーのアクセスと出力
printf("親の scores: %d, %d, %d\n", p->scores[0], p->scores[1], p->scores[2]);
// NULL ポインタを扱うときはチェックしてから
print_person(&parent);
print_person(NULL); // これは安全に何もしない
return 0;
}
実行結果の一例は次のようになります。
親: Hanako (35歳)
ドット: parent.age=35, アロー: p->age=35
(*p).age=35, p->age=35
(&parent)->age=35
子: Taro (10歳)
親の scores: 70, 75, 80
Person{name="Hanako", age=35}
[print_person] person は NULL です
p->member で値を読み書き
上のコードのとおり、p->name
やp->age
と書くことで、代入と参照の両方ができます。
文字列は配列なのでstrcpy
やsnprintf
などを使って代入し、整数は通常の代入で問題ありません。
p->child->name の連鎖アクセス
child
自体がPerson *
であるため、p->child->name
のように-> を連鎖して辿ることができます。
途中のどのポインタもNULLでないことの確認が重要です。
配列メンバー p->arr[i] のアクセス
配列メンバーscores
へのアクセスはp->scores[i]
の形で行います。
これは(*p).scores[i]
と同じ意味です。
添字の範囲外アクセスは未定義動作になるため、配列サイズに注意してください。
よくある間違いと注意点
*p.member は誤り
*p.member
は誤りです。
演算子の優先順位によりp.member
が先に解釈されますが、p
はポインタなのでp.member
自体が不正です。
正しくは(*p).member
またはp->member
です。
// 誤りの例(コンパイルエラー):
// *p.age = 20; // '.' が先に結合し p.age を試みるため不正
// 正しい書き方:
(*p).age = 20;
p->age = 20; // こちらが推奨(簡潔)
NULL ポインタに対する -> は使わない
NULLポインタに対して->
でアクセスすると未定義動作です。
多くの場合、実行時エラー(セグメンテーション違反)になります。
必ず NULL チェックを行うか、関数の契約として「NULL不可」を明記してください。
void safe_set_age(Person *p, int age) {
if (p == NULL) {
// ここで早期リターンするか、エラーを報告する
return;
}
p->age = age; // p が非 NULL であれば安全
}
ドット演算子との混同に注意
「実体にはドット、ポインタにはアロー」というルールを常に意識します。
途中でアドレス演算子&
を使ったり、ポインタを渡したりすると、どちらを使うべきか見失いがちです。
たとえばs
が実体ならs.age
、&s
はポインタなので(&s)->age
になります。
括弧の有無にも注意してください。
状況 | 正しい例 | 間違いやすい例 | ポイント |
---|---|---|---|
実体にアクセス | s.age | s->age | s はポインタではない |
ポインタにアクセス | p->age | p.age | p はポインタ |
アドレスを取ってすぐアクセス | (&s)->age | &s->age | & より-> が優先されるため括弧が必要 |
迷ったら「その左側はポインタか実体か」を確かめると間違いを避けられます。
まとめ
本記事では、構造体へのポインタに用いる ->(アロー演算子) の基本を解説しました。
ポイントは次のとおりです。
アローはポインタからメンバーへアクセスするための記法で、p->m
は(p).m
と同じ意味です。
実体には . 、ポインタには ->という使い分けを覚え、NULLポインタに対して -> を使わないこと、p.member
のような誤用をしないことに注意してください。
配列メンバーやネストしたポインタでもp->arr[i]
、p->child->name
のように素直に書けます。
基本を正しく押さえれば、構造体とポインタのコードはぐっと読みやすく、安全になります。