【C言語】ポインタのポインタを攻略。メモリ構造から理解する現場の流儀

【C言語】ポインタのポインタを攻略。メモリ構造から理解する現場の流儀 C言語

皆さん、こんにちは。リーダーです。

C言語を学んでいると、誰もが一度は「ポインタ」でつまづきます。
そして、ようやくポインタが分かりかけてきた頃に立ちはだかるのが、今回のテーマである「ポインタのポインタ」です。

「ポインタだけでも大変なのに、そのポインタをまたポインタにするなんて……」

と、頭を抱えたくなる気持ち、よく分かります。
私も若手の頃、通信プロトコルの実装でダブルポインタを多用した際、どこを指しているのか分からなくなり、机の上にメモリの図を何枚も描いて格闘したものです。

しかし、この仕組みを理解すると、C言語でのプログラミングの自由度が劇的に上がります。

今日は、メモリという「箱」の中に何が入っているのかをイメージしながら、一緒に紐解いていきましょう。

【執筆者の簡易プロフィール】
リーダー 執筆者:リーダー
  • 当ブログの統括者兼ライター
  • 45歳男性、既婚、2児の父
  • 組み込みエンジニア
  • 新卒で大手電機メーカーに入社し、開発部門に配属、現在も勤務
  • 主な使用言語はC言語とC++
  • 趣味は毎晩の晩酌

ポインタのポインタ(ダブルポインタ)という高い壁を越える

ポインタのポインタ(ダブルポインタ)とは、一言で言えば「ポインタ変数のアドレスを格納する変数」のことです。

通常のポインタが「データの入っている場所」を指し示すのに対し、ポインタのポインタは「場所を指し示している指、そのものの居場所」を指し示します。

これが必要になるのは、単にデータを書き換えるだけでなく、「今見ている場所そのものを、別の場所へ切り替えたい」という時です。

私が以前、大規模な設定ファイルの読み込みプログラムをメンテナンスしていた時、読み込むデータのサイズに応じてバッファのアドレスを動的に変更する必要がありました。
そんな時、ポインタのポインタなしでは、スマートな実装は不可能だったでしょう。

メモリのアドレスを指し示す仕組みを視覚的に整理する

まずは、言葉で考えるよりも図をイメージしてください。
メモリは、住所(アドレス)がついた「箱」の集まりです。

通常のポインタ変数 p には、ある変数 a のアドレスが入っています。
そして、ポインタのポインタ pp には、そのポインタ変数 p 自体が置かれている場所のアドレスを入れるわけです。

実際のコードで見ると、星印(アスタリスク)が2つ並ぶことになります。

#include <stdio.h>

int main() {
int val = 100;
int *p = &val;   // valのアドレスを指すポインタ
int **pp = &p;  // ポインタpのアドレスを指す「ポインタのポインタ」

printf("valの値: %d\n", val);
printf("pが指す先の値 (*p): %d\n", *p);
printf("ppが指す先の、その先の値 (**pp): %d\n", **pp);

return 0;
}

このコードの前後を解説しましょう。

まず int *p = &val; で、整数型の変数 val の居場所を p に教えます。
次に int **pp = &p; で、その「教えてもらった内容が入っている箱(p)」の居場所を pp に教えています。
**pp と書くことで、2回扉を開けるようにして、最終的な val の中身に辿り着くことができるのです。

関数にポインタの居場所を教えて値を書き換える

ポインタのポインタが最も実務で活躍するのは、関数の中で「呼び出し元のポインタ変数の値(指し示す先)」を書き換えたい時です。

普通のポインタを関数に渡すと、関数内では「そのポインタが指しているデータ」を書き換えることはできますが、「どのデータを指すか」というポインタ自体の向き先を変えることはできません。
これを無理やりやろうとして、ポインタが更新されずにバグを出すのが新人の「あるある」です。

#include <stdio.h>
#include <stdlib.h>

// 引数にポインタのポインタを受け取る
void allocate_init(int **pp) {
// 新しくメモリを確保し、そのアドレスを「呼び出し元のポインタ」に直接書き込む
*pp = (int *)malloc(sizeof(int));
if (*pp != NULL) {
**pp = 200; // 確保したメモリ先に値を代入
}
}

int main() {
int *p = NULL;

// pのアドレスを渡すことで、関数内でpの中身を書き換えてもらう
allocate_init(&amp;p);

if (p != NULL) {
    printf("関数で割り当てられた値: %d\n", *p);
    free(p); // 確保したメモリを忘れずに解放
}

return 0;
}

このプログラムでは、allocate_init 関数に &p を渡しています。

関数側では int **pp で受け取り、*pp = ... とすることで、main 関数の中にある p の中身を直接書き換えています。

もしここを単なる int *p で受け取っていたら、関数の中でいくら p = malloc(...) としても、main 側の pNULL のまま。
これに気づかず後続の処理でクラッシュさせる……。

そんな苦い経験、皆さんもこれからするかもしれません。
でも、この仕組みを知っていれば大丈夫です。

文字列の配列を動的に扱う際の実践的な作法

もう一つ、ポインタのポインタが頻出するのが「文字列の配列」です。

C言語において文字列は char * ですが、その配列、つまり「文字列が複数入っているリスト」は char ** と表現されます。

皆さんがよく見る main(int argc, char **argv)argv がまさにこれですね。

#include <stdio.h>

int main() {
// 文字列(ポインタ)の配列
char *names[] = {"Apple", "Orange", "Banana"};

// このnamesの先頭を指すのが char **型
char **pp = names;

for (int i = 0; i &lt; 3; i++) {
    // pp[i] は *(pp + i) と同じ意味。各文字列の先頭アドレスを指す
    printf("フルーツ [%d]: %s\n", i, pp[i]);
}

return 0;
}

現場では、設定ファイルから読み込んだ複数の文字列をメモリ上に保持する際、あらかじめ数が分からないため char ** 型でメモリを確保し、各要素にまた malloc で文字列を割り当てる、といった二段階の動的確保をよく行います。

複雑に見えますが、「ポインタ(住所)のリスト(名簿)」を作っているだけだと考えれば、少し気が楽になりませんか?

現場で学んだポインタのポインタが引き起こすバグの防ぎ方

ポインタのポインタを使い始めると、星印(*)の数が増えて混乱しがちです。
「今の *pp はアドレスなのか、それともデータなのか?」と迷ったら、必ず一度紙に書いて整理してください。

  • pp は「ポインタ変数のアドレス」
  • *pp は「ポインタ変数そのもの(中身はデータのアドレス)」
  • **pp は「最終的な実データ」

この三階層を頭に叩き込みましょう。

また、多段階のポインタを使う際は、必ず NULL チェックを丁寧に行ってください。
pp は有効でも、*ppNULL だった場合、**pp にアクセスした瞬間にプログラムは無慈悲に終了します。

「急がば回れ」。
複雑なポインタ操作ほど、一歩ずつ確認しながら実装するのが、結局は最短ルートになります。

まとめ

C言語のポインタのポインタについて、その本質と実務での使われ方を解説してきました。

ポインタのポインタは、ポインタ自体の居場所を指す。
関数内で呼び出し元のポインタを書き換える(向き先を変える)のに必須。
文字列の配列や多次元の動的メモリ確保で活躍する。
混乱したら「箱の階層」を紙に書いて整理する。

最初は難しく感じるかもしれませんが、自分でコードを書き、メモリの状態をデバッガで追いかけてみると、ある時ふっと理解できる瞬間が来ます。
その瞬間、あなたのエンジニアとしての視界は大きく開けるはずです。

それでは、また次の記事でお会いしましょう。リーダーでした。

コメント

タイトルとURLをコピーしました