C++のダブルポインタを完全理解!多次元配列への応用と仕組みを徹底解説
生徒
「先生、ポインタを勉強していたら『**』という記号が二つ付いた『ダブルポインタ』というものが出てきました。これって一体なんですか?」
先生
「それはポインタのポインタ、つまり『住所が書いてあるメモの場所』を指し示すものですよ。」
生徒
「住所の住所……? なんだか頭が混乱しそうです。どんな時に使うんですか?」
先生
「実は、表のような縦横があるデータ(多次元配列)を自由に操る時にとても役立つんです。一緒に仕組みを紐解いていきましょう!」
1. ダブルポインタとは?「住所の住所」を指す仕組み
C++のダブルポインタ(pointer to pointer)とは、その名の通りポインタを指し示すポインタのことです。通常のポインタは変数の「住所」を保存しますが、ダブルポインタはその「住所を保存している箱」自体の住所を記録します。記号では ** とアスタリスクを二つ並べて表現します。
プログラミング未経験でパソコンを触ったことがない方にも分かるように例えると、通常のポインタが「宝物の隠し場所が書かれたメモ」だとすれば、ダブルポインタは「そのメモを入れたロッカーの場所」を指しています。ロッカーを開けてメモを取り出し、そのメモを読んでようやくお宝(実際のデータ)に辿り着けるという二段構えの構造です。Google検索で「C++ ポインタ 難しい」と調べる方の多くがここで悩みますが、この「二段階の案内図」というイメージを持てば大丈夫です。
2. メモリ上でのダブルポインタの動き
コンピューターのメモリ(作業机)には、全てのデータに番地が付いています。ダブルポインタがメモリ上でどうなっているかを確認しましょう。まず、数値が入った変数 A があります。次に、その A の番地を記録したポインタ B があります。さらに、そのポインタ B が置かれている番地を記録したのがダブルポインタ C です。
なぜこんな遠回りをしなくてはいけないのでしょうか? それは、プログラムを動かしている途中で「案内するメモ(ポインタ)」自体を別のものに差し替えたい場合があるからです。これを理解すると、より高度なメモリ管理ができるようになります。専門用語で言うところの間接参照(かんせつさんしょう)を二回行っている状態です。
3. ダブルポインタの基本コードを見てみよう
まずは、数値データの住所を二段階で辿るシンプルなコードを書いてみましょう。記号の * が増えるだけで、基本的な考え方は通常のポインタと同じです。
#include <iostream>
int main() {
int treasure = 777; // 実際のデータ(お宝)
int* ptr = &treasure; // お宝の住所を指すポインタ
int** dptr = &ptr; // ポインタptrの住所を指すダブルポインタ
std::cout << "お宝の値: " << treasure << std::endl;
std::cout << "ポインタ経由: " << *ptr << std::endl;
std::cout << "ダブルポインタ経由: " << **dptr << std::endl;
return 0;
}
お宝の値: 777
ポインタ経由: 777
ダブルポインタ経由: 777
このように、**dptr と書くことで、二つの壁を突き抜けて中身の数値にアクセスできます。一回目の * で「メモ(ポインタ)」を取り出し、二回目の * で「その先のデータ」を見に行くという動作をしています。
4. 多次元配列への応用:表形式のデータ管理
ダブルポインタが最も力を発揮するのは、多次元配列(たじげんはいれつ)、特に「行と列」を持つ表のようなデータを扱うときです。パソコンの「エクセル」のような縦横のマス目をイメージしてください。C++では、一列に並んだデータを「配列」と呼びますが、その配列をさらに縦に並べたものが多次元配列です。
ダブルポインタを使うと、「各行がどこから始まるか」という住所のリストを管理できます。これにより、各行の長さがバラバラな特殊な表(ジャグ配列)を作ったり、プログラムの実行中に表の大きさを自由に変えたりすることが可能になります。これを動的確保(どうてきかくほ)と呼び、C++の柔軟性を支える非常に重要な技術です。
5. ダブルポインタで表データを操作する例
少し難しいかもしれませんが、ダブルポインタを使って2x3(2行3列)のデータ構造を扱うイメージのコードを見てみましょう。ここでは初心者の方にも分かりやすいように、既存の配列を指し示す方法を紹介します。
#include <iostream>
int main() {
int row1[3] = {1, 2, 3}; // 1行目のデータ
int row2[3] = {4, 5, 6}; // 2行目のデータ
// 各行の先頭住所を保存する「ポインタの配列」
int* rows[2] = {row1, row2};
// その配列を指し示すダブルポインタ
int** table = rows;
// table[行][列] の形式でデータにアクセスできる
std::cout << "1行目の2列目: " << table[0][1] << std::endl;
std::cout << "2行目の3列目: " << table[1][2] << std::endl;
return 0;
}
1行目の2列目: 2
2行目の3列目: 6
ダブルポインタ table を通じて、まるで普通の表のように [行][列] という書き方でデータを取り出すことができました。これは、一段目のポインタが行を選び、二段目のポインタがその行の中の列を選んでいるのです。
6. なぜダブルポインタを使うのか? メリットと重要性
普通の二次元配列( int table[2][3]; )を使えばいいのでは? と思うかもしれません。しかし、ダブルポインタには以下の強力なメリットがあります。
- 柔軟なサイズ変更: 実行中に「やっぱり行を増やしたい」といった変更が可能です。
- メモリの節約: 行ごとに必要な分だけメモリを割り当てられるため、無駄がありません。
- 関数の引数での活用: 関数の中で「外にあるポインタの指し先」を書き換えたい場合、ダブルポインタを渡す必要があります。
例えば、大量の文章データを一行ずつ保存する場合、一行の長さはバラバラですよね。ダブルポインタを使えば、それぞれの文章の長さに合わせたメモリ管理ができ、パソコンの資源を賢く使うことができるのです。
7. 初心者が注意すべき「ヌルポインタ」とエラー
ダブルポインタを扱う際に最も怖いのが、セグメンテーションフォールト(強制終了)というエラーです。これは、存在しない住所を無理やり見に行こうとした時に発生します。ダブルポインタの場合、一段目が「空(nullptr)」なのに二段目を見に行こうとすると、パソコンがパニックを起こしてしまいます。
「ロッカーを開けたら空っぽだったのに、そこにあるはずのメモを読もうとする」ようなものです。これを防ぐためには、必ず if (dptr != nullptr && *dptr != nullptr) のように、二段階で安全確認を行う癖をつけましょう。これは、Google検索でバグの解決法を探す際にもよく出てくる重要な防衛策です。
8. 文字列の配列とダブルポインタの関係
最後に応用例として、複数の「名前」を管理する例を考えてみましょう。C++において、文字列は文字の配列です。つまり、複数の名前を扱うということは「配列の配列」になります。ここでダブルポインタの知識が活きてきます。
#include <iostream>
int main() {
const char* names[] = {"Alice", "Bob", "Charlie"};
const char** pNames = names; // ダブルポインタで名前リストを指す
for(int i = 0; i < 3; i++) {
std::cout << "名前 " << i + 1 << ": " << pNames[i] << std::endl;
}
return 0;
}
この char** という型は、C++の古いプログラムや、コンピューターのOSに近い部分のプログラムで非常によく見かけます。一見難解な記号の羅列に見えますが、ここまで読んだあなたなら「名前というデータの住所が並んでいるリストを指しているんだな」と理解できるはずです。一歩ずつ、この「住所の案内図」をマスターしていきましょう!