C言語を学習していて、多くの人が最初の壁として感じるのが「ポインタ」です。
そして、ようやくポインタを理解したと思った矢先に現れるのが、アスタリスクが2つ並んだ 「ポインタのポインタ(ダブルポインタ)」 ではないでしょうか。
「普通にポインタを使うだけではダメなの?」
このように混乱してしまうのは無理もありません。
しかし、ポインタのポインタは、複雑なデータ構造やライブラリのAPIを扱う上で避けては通れない重要な機能です。
この記事では、C言語におけるポインタのポインタの仕組みから、なぜそれが必要なのかというメリット(使いどころ)、そして実務で頻出する「動的な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のアドレスを保持)
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)を受け取っています。
関数内で *pp(ppが指す場所=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言語を扱えるエンジニアは比較的希少価値が高く、転職によって数十万円の年収アップはザラで、100万円以上年収が上がることも珍しくありません。
なお、転職によって年収を上げたい場合は、エンジニア専門の転職エージェントサービスを利用するのが最適です。
転職エージェントも副業エージェントも、登録・利用は完全無料なので、どんな求人や副業案件があるのか気になる方は、気軽に利用してみるとよいでしょう。
| 年収アップにこだわりたい方 (平均アップ額138万円の実績) | テックゴー |
| 未経験・経験者問わず幅広く探したい方 | ユニゾンキャリア |
| 業界に精通した担当者に相談したい方 | キッカケエージェント |
| ゲーム業界への転職を志望する方 | ファミキャリ |
| エンジニア未経験からキャリアを築く方 | イーチキャリア |



