記事内にはプロモーションが含まれています

【C言語】ポインタのポインタとは?メリットや2次元配列での活用法

【C言語】ポインタのポインタとは?メリットや2次元配列での活用法 C言語

C言語を学習していて、多くの人が最初の壁として感じるのが「ポインタ」です。

そして、ようやくポインタを理解したと思った矢先に現れるのが、アスタリスクが2つ並んだ 「ポインタのポインタ(ダブルポインタ)」 ではないでしょうか。

「ポインタのポインタは、ポインタのアドレスを格納する変数? どういうこと?」
「普通にポインタを使うだけではダメなの?」

このように混乱してしまうのは無理もありません。

しかし、ポインタのポインタは、複雑なデータ構造やライブラリのAPIを扱う上で避けては通れない重要な機能です。

この記事では、C言語におけるポインタのポインタの仕組みから、なぜそれが必要なのかというメリット(使いどころ)、そして実務で頻出する「動的な2次元配列」や「関数引数としての利用」まで、サンプルコード付きでわかりやすく解説します。

【本記事の信頼性】
プロフィール
執筆者:マヒロ
  • 執筆者は元エンジニア
  • SES⇒大手の社内SE⇒独立
  • 現在はプログラミングスクールを運営
  • モットーは「利他の精神」
💻 本記事の検証環境(2026年2月確認)
  • OS:Windows 11 / macOS Sequoia
  • IDE:Visual Studio / VS Code / IntelliJ IDEA
  • その他:Chrome DevTools / 各言語最新安定版

※本メディアでは、上記環境にてコードの動作と情報の正確性を検証済みです。

ポインタのポインタとは何か?「住所の書かれた紙の置き場所」

ポインタのポインタとは、文字通り「ポインタ変数のアドレス(メモリ上の場所)を格納するための変数」です。

宣言時には int **pp; のようにアスタリスクを2つ重ねて記述します。

概念をイメージで理解する

通常の変数とポインタ、そしてポインタのポインタの関係は、よく「宝箱」と「住所が書かれた紙」に例えられます。

  • 通常の変数 (int a): 「100円」が入っている宝箱
  • ポインタ (int *p): 宝箱が置いてある場所(住所)が書かれた
  • ポインタのポインタ (int **pp): その「紙」が置いてある場所(住所)が書かれた別の紙

つまり、pp(別の紙)を見れば、p(紙)の場所がわかり、そのpを見れば、a(宝箱)の場所がわかるという、2段階の参照関係になっています。

これを「多重間接参照」と呼びます。

基本的な宣言と初期化

#include <stdio.h>

int main(void) {
    int target = 123;       // 通常の変数
    int *p = &target;       // ポインタ(targetのアドレスを保持)
    int **pp = &p;          // ポインタのポインタ(pのアドレスを保持)

    printf("targetの値: %d\n", target);
    printf("p経由の値 : %d\n", *p);
    printf("pp経由の値: %d\n", **pp);

    return 0;
}

実行結果

targetの値: 123
p経由の値 : 123
pp経由の値: 123

コード内では、まず整数型の変数 target を定義しています。
次にポインタ変数 p を定義し、target のアドレス(&target)を代入しました。

ここまでは通常のポインタ操作です。

次に、ダブルポインタ変数 pp を定義し、ポインタ p 自体のアドレス(&p)を代入しています。

pp から値を取り出す際は、アスタリスクを2回使って **pp と記述することで、2回参照を辿り、最終的に target の値である 123 にアクセスしています。

なぜ必要なのか?ポインタのポインタを使う3つのメリット

「複雑になるだけでメリットが分からない」と感じるかもしれませんが、ポインタのポインタでなければ解決できない課題が存在します。

主に以下の3つのシーンでその威力を発揮します。

関数内で「ポインタの向き先」を変更したい場合

C言語の関数引数は「値渡し」です。

ポインタを引数に渡しても、それは「アドレスという値」のコピーが渡されるだけなので、関数内でそのポインタが別の場所を指すように変更しても、呼び出し元のポインタには影響しません。

呼び出し元のポインタ変数の中身(アドレス)を書き換えたい場合は、そのポインタのアドレス、つまり「ポインタのポインタ」を渡す必要があります。

動的な2次元配列(行列)を扱いたい場合

画像のピクセルデータや行列計算など、縦と横のサイズが可変のデータを扱う場合、ダブルポインタを使用して「ポインタの配列」を作成し、さらにその各要素が「データの実体」を指すような構造を作ります。

文字列の配列を操作したい場合

C言語で文字列は char *(文字型ポインタ)として扱われます。

つまり、「文字列の配列」を作ろうとすると、それは char * の配列、すなわち char **(ポインタのポインタ)として扱うことになります

コマンドライン引数の char *argv[] が代表的な例です。

【実践】関数引数での活用!ポインタの値を書き換える

ここでは、関数を使ってポインタの参照先を変更する例を見てみましょう。

「エラーが起きたら、ポインタをNULLやエラー用オブジェクトに向け直す」といった処理でよく使われます。

ポインタの参照先を変更するコード例

#include <stdio.h>

int valueA = 10;
int valueB = 20;

// シングルポインタで書き換えようとする(失敗する例)
void try_change(int *p) {
    p = &valueB; // ローカル変数pの中身が変わるだけ
}

// ダブルポインタで書き換える(成功する例)
void change_pointer(int **pp) {
    *pp = &valueB; // 呼び出し元のポインタの中身を書き換える
}

int main(void) {
    int *ptr = &valueA;

    printf("初期状態: %d\n", *ptr);

    // 失敗するパターン
    try_change(ptr);
    printf("try_change後: %d (変わっていない)\n", *ptr);

    // 成功するパターン(ポインタのアドレスを渡す)
    change_pointer(&ptr);
    printf("change_pointer後: %d (変わった!)\n", *ptr);

    return 0;
}

実行結果

初期状態: 10
try_change後: 10 (変わっていない)
change_pointer後: 20 (変わった!)

try_change 関数では、引数として ptr の値(valueA のアドレス)のコピーを受け取っています。

関数内で p = &valueB としても、それは関数内のローカル変数 p が書き換わるだけで、main 関数の ptr には影響しません。

一方、change_pointer 関数では、ptr そのもののアドレス(&ptr)を受け取っています。

関数内で *ppppが指す場所=ptr)に対して代入を行うことで、main 関数の ptr が指す先を valueB に変更することに成功しています。

2次元配列の動的確保!ダブルポインタの真骨頂

ポインタのポインタが最も頻繁に使われるのが、malloc 関数と組み合わせた「多次元配列の動的確保」です。

行数と列数が実行時までわからない場合、この方法でメモリを確保します。

行列データを動的に作成するコード例

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

int main(void) {
    int rows = 3; // 行数
    int cols = 4; // 列数
    int **matrix;
    int i, j;

    // 1. 行のポインタを格納する配列を確保(int* 型の配列)
    matrix = (int **)malloc(sizeof(int *) * rows);
    if (matrix == NULL) return 1; // エラー処理

    // 2. 各行の実体データを確保(int 型の配列)
    for (i = 0; i < rows; i++) {
        matrix[i] = (int *)malloc(sizeof(int) * cols);
        if (matrix[i] == NULL) return 1; // エラー処理
    }

    // データの代入
    for (i = 0; i < rows; i++) {
        for (j = 0; j < cols; j++) {
            matrix[i][j] = i * 10 + j;
        }
    }

    // データの表示
    for (i = 0; i < rows; i++) {
        for (j = 0; j < cols; j++) {
            printf("%2d ", matrix[i][j]);
        }
        printf("\n");
    }

    // 3. メモリの解放(確保と逆の順序で行う)
    for (i = 0; i < rows; i++) {
        free(matrix[i]); // 各行を解放
    }
    free(matrix); // ポインタ配列を解放

    return 0;
}

実行結果

 0  1  2  3 
10 11 12 13 
20 21 22 23

まず、matrix というダブルポインタに対して、rows 個分の「ポインタを格納できる領域」を確保します。

この時点で matrix は「行の先頭アドレスを管理する配列」のような状態になります。

次に、ループを使って各行ごとに cols 個分の「整数データを格納できる領域」を確保し、そのアドレスを matrix[i] に格納します。

これにより、matrix[i][j] という記述で、まるで静的な2次元配列のようにデータへアクセスできるようになります。

注意点として、解放時は確保した順序とは逆に、まず各行の領域(matrix[i])を解放し、最後に大元の領域(matrix)を解放する必要があります。

「ポインタのポインタのポインタ」は存在する?

理論上、ポインタの参照回数に制限はありません。
したがって、int ***pptr; のような「トリプルポインタ」も定義可能です。

これは「3次元配列の動的確保」などで稀に使用されることがありますが、参照が深くなるほどコードの可読性は著しく低下し、バグの原因になります。

実務においては、ダブルポインタまでは頻出しますが、トリプルポインタ以上が必要になる設計は「構造体を使って整理できないか?」と見直されるケースがほとんどです。

まずはダブルポインタまでを確実に理解しておけば十分でしょう。

C言語のスキルを活かして年収を上げる方法

以上、C言語のポインタのポインタについて解説してきました。

なお、C言語のスキルがある場合には、「転職して年収をアップさせる」「副業で稼ぐ」といった方法を検討するのがおすすめです。

C言語を扱えるエンジニアは比較的希少価値が高く、転職によって数十万円の年収アップはザラで、100万円以上年収が上がることも珍しくありません。

なお、転職によって年収を上げたい場合は、エンジニア専門の転職エージェントサービスを利用するのが最適です。
今すぐ転職する気がなくとも、とりあえず転職エージェントに無料登録しておくだけで、スカウトが届いたり、思わぬ好待遇の求人情報が送られてきたりするというメリットがあります。
併せて、副業案件を獲得できるエージェントにも登録しておくと、空いている時間を活かして稼げるようなC言語の案件を探しやすくなります。

転職エージェントも副業エージェントも、登録・利用は完全無料なので、どんな求人や副業案件があるのか気になる方は、気軽に利用してみるとよいでしょう。
エンジニアのキャリア・スキルアップ相談窓口
当ブログの読者に選ばれている、実績豊富な転職エージェントを厳選しました。
【転職】年収・環境を改善したい
年収アップにこだわりたい方 (平均アップ額138万円の実績)
未経験・経験者問わず幅広く探したい方
業界に精通した担当者に相談したい方
ゲーム業界への転職を志望する方
エンジニア未経験からキャリアを築く方
【独立】フリーランスとして稼ぎたい
国内最大級のフリーランス案件数から比較したい方
週1〜3日など柔軟な働き方を希望する方
【学習】スキルに不安のある方向け(格安スクール「デイトラ」)