Menu

C言語の構造体へのポインタ:アロー演算子とヒープ上の構造体

C言語で構造体を指す方法 - アロー演算子、(*p).x にかっこが必要な理由、変更やコピー回避のためにポインタで構造体を渡すこと、構造体を malloc すること、そして連結リストのノードの作り方。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

関数に渡された構造体はコピーされます。int 2つの点ならそれで構いませんが、200バイトのレコードでは無駄ですし、変更も不可能になります。関数は自分のコピーしか見ないからです。構造体を指すことはこの両方の問題を一度に解決しますし、成長する構造 - リスト、木、グラフ - を作る唯一の方法でもあります。

構造体へのポインタを取る

特別なことは何も要りません。& がアドレスを与え、その型は「構造体へのポインタ」です。

3つとも 3 を表示します。後ろの2つは同じ操作を2通りに書いたものです。

(*ptr).x にかっこが必要な理由

かっこを落として *ptr.x と書きたくなるかもしれません。それはまったく別のものにコンパイルされるか、もっと多くの場合はそもそもコンパイルを拒否されます - メンバ演算子 . は参照外しの * より優先順位が高いからです。

*ptr.x        // *(ptr.x) と解釈される -- 誤り
(*ptr).x      // まず参照外しし、それからメンバを取る -- 正しい
ptr->x        // 同じことを短く言ったもの

ptr.xポインタのメンバを求めており、ポインタにメンバはありません。コンパイラの苦情(「request for member 'x' in something not a structure or union」)は、姿を変えた優先順位のバグです。

かっこの付いた形は見苦しく間違えやすいので、Cは -> を用意しています。実際のコードで (*p).x を見ることはまずありません。p->x に手を伸ばし、アローが何を意味するかの説明を除けば、長い形の存在は忘れてしまいましょう。

ポインタで構造体を渡す

ポインタの引数は呼び出し元の実際の構造体を関数に与えるので、変更が残ります。

ここで身につける価値のある習慣が2つあります。

  • 読み取り専用のポインタには const を付ける。 const struct Point *p は、関数が p の指す先を変更しないと約束します。あとで誰かが中に p->x = 0 を足したら、バグが出荷される代わりにコンパイルが失敗します。呼び出し側での意図の説明にもなります。
  • 読むだけでも大きな構造体はポインタで渡す。 64バイトの名前バッファを持つ構造体は、値渡しなら呼び出しごとに64バイトのコピーがかかります。ポインタなら8バイトです。

小さな構造体は値で渡してもまったく構いません - 8バイトの struct Point はポインタと同じくらい安くコピーできますし、値渡し版は NULL になりえないので、失敗の道筋がまるごと1つ消えます。

構造体の配列を指すポインタ

ポインタ演算は int の配列と同じように構造体の配列でも働きます。p + 1 はパディングを含めた構造体1つぶんだけ進みます。

配列名は先頭要素へのポインタに成り下がるので、呼び出し時点で staff はすでに struct Employee * です。だからこの関数は n を別に渡してもらう必要があるのです - 関数の中の sizeof は配列ではなくポインタを測ってしまいます。

ヒープに構造体を確保する

スタック上の構造体はスコープの終わりで死にます。それを作った関数より長生きするものを作るには - あるいは入力が要求するだけの数を作るには - malloc で確保します。

この小さな関数の中の4つの細部は、どれも荷重を支えています。

  • sizeof(struct Employee) ではなく sizeof *ee が指しているものの大きさ」と読めるので、型が変わっても確保は自動的についてきます。両者がずれる余地がありません。
  • NULL を確認する。 malloc は要求を満たせないとき NULL を返します。ヌルポインタを通じて e->id に書くのはセグメンテーション違反です。
  • すべてのメンバを初期化する。 malloc はメモリをゼロにしません。構造体はゴミで満たされた状態で届きます。(calloc(1, sizeof *e) ならゼロにしてくれます。)
  • 誰かが free しなければならない。 確保したポインタを返すことは、その義務を呼び出し元へ移すことです。コメントでそう述べましょう - 所有権の規則が不明瞭なところからメモリリークが始まります。

見返り:連結リストのノード

これがポインタなしでは構造体にできないことです。構造体は自分自身を含められません - 無限に大きくなってしまいます - が、自分と同じ種類のものへのポインタなら含められます。そのたった1つの技が、C言語のあらゆる連結データ構造を組み立てます。

struct Node *next;まだ struct Node を宣言している途中で struct Node を参照している点に注目してください。これが合法なのは、まさにポインタが指す先にかかわらず既知の大きさを持つからです - コンパイラはまだ完全な定義を必要としません。ここはまた、typedef も書いていたとしても struct Node というタグを使わなければならない唯一の場所でもあります。typedef の名前は、自分自身の定義の中ではまだ存在しないのです。

free_list のループは free(head) を呼ぶ前に head->next を保存しています。解放したあとで head->next を読むのは use-after-free です - そのメモリはもうあなたのものではなく、読み取る値はアロケータがそこに置いたものです。

よくある間違い

  • 未初期化または NULL のポインタに対する p->x 構造体を多用するコードでクラッシュの最もよくある原因です。ポインタは NULL で初期化し、参照外しの前に確認しましょう。
  • ローカルな構造体へのポインタを返す。 struct Point *bad(void) { struct Point p = {1,2}; return &p; } は、関数が戻った瞬間に存在しなくなるメモリのアドレスを返します。構造体を値で返すか、確保しましょう。
  • 二重に解放する、あるいは解放を忘れる。 どの malloc もちょうど1つの free と対になります。解放したらポインタを NULL にして、あとでうっかり使ったときに静かにメモリを壊すのではなく派手にクラッシュするようにしましょう。
  • ポインタを含む構造体をコピーする。 構造体の代入はポインタのをコピーするのであって、その指す先をコピーするわけではありません。これで2つの構造体が同じバッファを参照することになり、先に解放したほうが、もう一方を宙に浮かせます。

よくある質問

C言語の -> 演算子は何をしますか?

p->x はポインタ p を通じてメンバ x にアクセスします。(*p).x とまったく等価です - ポインタを参照外しし、それからメンバを取ります。構造体へのポインタはあまりによく使われ、長い形ではコードが煩雑になるので、アローが用意されているのです。

なぜ構造体のポインタに対して *p.x は動かないのですか?

.* より強く結びつくからです。*p.x*(p.x) と解釈されます - p(構造体ではなくポインタです)のメンバ x を取り、それを参照外しする、という意味になります。(*p).x と書くか、単に p->x と書きましょう。

C言語では構造体を値で渡すべきですか、ポインタで渡すべきですか?

関数が呼び出し元の構造体を変更しなければならないとき、あるいは構造体が大きくてコピーが無駄になるときはポインタで渡します。小さな読み取り専用の構造体は値で渡しましょう。コピーが安価でコードも単純です。読み取り専用のポインタ引数は const struct T * と印を付け、コンパイラにその約束を守らせましょう。

C言語で malloc を使って構造体を確保するには?

struct Node *n = malloc(sizeof *n); です - sizeof *n の形は n が指しているものの大きさを尋ねるので、あとで型を変えても正しいままです。結果を NULL と比べて確認し、すべてのメンバを初期化し、終わったら free(n) します。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める