C++のポインタ渡しを実例で解説!初心者でもわかるメモリの住所と仕組み
生徒
「C++を勉強していると『ポインタ』という言葉が出てきて、難しそうで怖気づいています……。」
先生
「大丈夫ですよ。ポインタはただの『住所』のことなんです。今回はその住所を使ってデータをやり取りする『ポインタ渡し』を学びましょう。」
生徒
「住所ですか?データを直接渡すのと何が違うんですか?」
先生
「荷物そのものを送るのではなく、荷物がある場所を教えてあげるイメージですね。実例を見ながら楽しく理解していきましょう!」
1. ポインタ渡しとは?住所を教える魔法の仕組み
C++のポインタ渡し(pass by pointer)とは、関数にデータそのものをコピーして渡すのではなく、そのデータが保存されているメモリ上の「住所(アドレス)」を教える方法のことです。プログラミング未経験の方にとって「ポインタ」という響きは難解に聞こえるかもしれませんが、実は私たちの日常でもよく使っている考え方です。
例えば、あなたが友達に重たいピアノをプレゼントしたいとします。ピアノを抱えて友達の家まで運ぶのは大変ですよね(これが値渡しです)。代わりに、あなたはピアノが置いてある倉庫の「住所」と「鍵」をメモに書いて友達に渡します。友達はそのメモを見て、倉庫へ行き、中のピアノを自由に弾いたり色を塗ったりできます。この「住所を記したメモ」を渡すのが、プログラミングにおけるポインタ渡しなのです。
2. メモリとアドレスの基本を覚えよう
パソコンを動かすとき、あらゆるデータはメモリという広大な作業スペースに置かれます。このメモリには、一箇所ごとに番号が振られており、これをアドレスと呼びます。ちょうど街の一軒一軒に住所があるのと同じですね。
C++では、変数の前に「&」という記号をつけることで、その変数の住所を調べることができます。そして、その住所を保存しておくための特別な箱をポインタ変数と呼びます。ポインタ渡しは、この「住所の箱」を関数の引数として利用する手法です。これにより、関数側は呼び出し元のデータを「遠隔操作」することができるようになります。
3. ポインタ渡しの実例:値を2倍にするプログラム
まずは、数字が保存されている住所を関数に渡し、その住所にある中身を書き換える簡単な例を見てみましょう。ポインタを扱うときは、型に「*(アスタリスク)」をつけます。これは「住所を受け取りますよ」という目印です。
#include <iostream>
// ポインタを使って、住所にある数値を2倍にする関数
void doubleValue(int* pNumber) {
// *pNumber と書くと「住所にある中身」を指します
*pNumber = (*pNumber) * 2;
}
int main() {
int score = 100;
std::cout << "元のスコア: " << score << std::endl;
// &score で「scoreの住所」を渡します
doubleValue(&score);
std::cout << "関数実行後のスコア: " << score << std::endl;
return 0;
}
元のスコア: 100
関数実行後のスコア: 200
このコードでは、scoreという変数の住所を関数に教えています。関数の中の*pNumberは「教えてもらった住所にある中身」を操作しているため、メインの処理にあるscoreが直接書き換わったのです。
4. 「空っぽ」を許容できるポインタの柔軟性
ポインタ渡しの非常に便利な点は、「何もない(nullptr)」という状態を渡せることです。これを「ヌルポインタ」と呼びます。参照渡し(&を使う方法)では必ず実体が必要ですが、ポインタ渡しなら「もしデータがあれば処理して、なければ何もしない」という柔軟なプログラムが書けます。
例えば、名前を登録するシステムで、名前が入力されなかった場合(住所が空の場合)を想定したプログラムを見てみましょう。パソコン初心者の方も、この「空の状態を扱える」というメリットがプログラミングの安全性を高めることを覚えておいてください。
#include <iostream>
#include <string>
// 名前を表示する関数。住所が空(nullptr)なら何もしない
void printName(std::string* pName) {
if (pName == nullptr) {
std::cout << "名前が登録されていません。" << std::endl;
} else {
std::cout << "登録名: " << *pName << " 様" << std::endl;
}
}
int main() {
std::string myName = "山田";
printName(&myName); // 有効な住所を渡す
printName(nullptr); // 「空」を渡す
return 0;
}
登録名: 山田 様
名前が登録されていません。
5. ポインタ渡しで複数の値を書き換える
関数の戻り値は通常一つだけですが、ポインタ渡しを使えば、複数のデータを一度に更新することができます。これは実務でもよく使われるテクニックです。例えば、魔法の呪文で「攻撃力」と「防御力」の両方を同時にアップさせる処理を作ってみましょう。
#include <iostream>
// 攻撃力と防御力の住所を両方受け取って強化する
void powerUp(int* pAtk, int* pDef) {
*pAtk += 10;
*pDef += 5;
}
int main() {
int attack = 50;
int defense = 30;
// 二つの住所を渡して同時に書き換えてもらう
powerUp(&attack, &defense);
std::cout << "強化後の攻撃力: " << attack << std::endl;
std::cout << "強化後の防御力: " << defense << std::endl;
return 0;
}
強化後の攻撃力: 60
強化後の防御力: 35
このように、引数として複数の住所を渡すことで、関数側でまとめて面倒を見てもらうことが可能になります。これは効率的な開発には欠かせない機能です。
6. 実例:お財布の残高を共有するシステム
さらに具体的な例として、家族でお財布の残高を共有するプログラムを考えてみます。お財布そのもの(中身)をコピーして渡すと、誰かが使っても他のお財布の金額は変わりません。しかし、お財布が置いてある場所(ポインタ)を共有すれば、誰かがお金を使えば全員の共通残高が減ることになります。これこそがポインタ渡しの真骨頂です。
#include <iostream>
// 共通の財布から支払いを行う関数
void payMoney(int* pWallet, int price) {
if (*pWallet >= price) {
*pWallet -= price;
std::cout << price << "円支払いました。" << std::endl;
} else {
std::cout << "お金が足りません!" << std::endl;
}
}
int main() {
int sharedWallet = 1000; // 共通の財布(1000円)
// 財布の住所を渡して買い物をしてもらう
payMoney(&sharedWallet, 300);
payMoney(&sharedWallet, 500);
std::cout << "残り残高: " << sharedWallet << "円" << std::endl;
return 0;
}
300円支払いました。
500円支払いました。
残り残高: 200円
7. ポインタ渡しを使う際の注意点
ポインタ渡しは非常に強力ですが、扱う際には「鍵の管理」に注意が必要です。間違った住所(どこも指していない住所や、他人の家の住所)を関数に渡してしまうと、プログラムが強制終了してしまうことがあります。これをセグメンテーションフォールトや「落ちる」と言ったりします。
未経験の方は、まず「住所を渡すときは必ず実体があるか確認する」ということを意識してください。また、最近のC++では、より安全な「参照渡し」が推奨されることも多いですが、ポインタ渡しは古いプログラムの修正や、特定の高度な機能を実装する際に必ず必要になります。住所という概念を正しく理解して、一歩ずつマスターしていきましょう。