C++の値渡し(pass by value)とは?仕組みと使い方を初心者向けに徹底解説
生徒
「関数に数字を渡して計算してもらったのに、元の数字が変わっていないんです。これって故障ですか?」
先生
「それは故障ではなく、C++の『値渡し』という仕組みが動いているからですよ。」
生徒
「アタイワタシ……?なんだか難しそうな名前ですね。具体的にどういうことなんですか?」
先生
「一言で言うと『データのコピー』を渡しているんです。それでは、図解するように分かりやすく解説していきますね!」
1. 値渡し(あたいわたし)の正体は「コピー」
プログラミングの世界で関数にデータを与えるとき、一番基本となるのが値渡し(あたいわたし)です。英語では「pass by value」と呼びます。パソコンを全く触ったことがない方でも、学校の宿題を想像すればすぐに理解できます。
あなたが持っているノートの宿題を、友達に見せてあげるとしましょう。このとき、自分のノートそのものを渡すのではなく、コピー機でコピーした紙を渡すのが「値渡し」です。友達がそのコピーされた紙に落書きをしたり、答えを書き換えたりしても、あなたの手元にある「元のノート」は綺麗なままですよね。これと同じことが、C++のプログラムの中でも起きているのです。
値渡しを行うと、関数の中でどんなにデータを加工しても、呼び出し元の元のデータには一切影響を与えません。これは、プログラムの安全性を高めるためにとても重要な仕組みなのです。
2. 変数と引数の役割を整理しよう
ここで少し、難しい言葉の整理をしましょう。プログラムでデータを一時的に保存しておく箱のことを変数(へんすう)と呼びます。そして、その箱の中身を関数に放り込むときの入り口のことを引数(ひきすう)と言います。漢字が難しいですが、「引く数」と書いて、関数に引き込むための材料だとイメージしてください。
値渡しでは、メインの処理で使っている変数の「中身」だけを、関数の入り口にある新しい箱にコピーします。つまり、箱そのものを共有しているわけではなく、全く同じ見た目の「別々の箱」が二つ存在することになります。この「別々である」という感覚が、C++をマスターするための第一歩です。
3. 値渡しのプログラムを見てみよう
実際に、関数の中で数字を書き換えても元の数字が変わらない様子を、コードで確認してみましょう。以下の例では、レベルアップという関数を作っていますが、元のレベルはそのまま維持されます。
#include <iostream>
// レベルを増やす関数(値渡し)
void levelUp(int lv) {
lv = lv + 1; // 箱の中身を1増やす
std::cout << "関数の中のレベル: " << lv << std::endl;
}
int main() {
int currentLevel = 10;
// 関数を呼び出す(コピーが渡される)
levelUp(currentLevel);
// 元のレベルはどうなっているか?
std::cout << "元の場所のレベル: " << currentLevel << std::endl;
return 0;
}
関数の中のレベル: 11
元の場所のレベル: 10
実行結果を見て驚いたかもしれません。関数の中でレベルは「11」になりましたが、メインの処理(main関数)に戻ってくると、元の「10」のままです。これが「コピーを渡している」証拠です。
4. メモリとコピーの仕組み(専門用語解説)
ここで、少しだけ専門的なお話をします。パソコンの中にはメモリという、作業用の机のような場所があります。プログラムが動くとき、変数はこのメモリの上に「場所」を確保します。専門用語で、この場所の番地のことを「アドレス」と呼びます。
値渡しが行われるとき、コンピュータは「新しい机のスペース」を確保し、そこに元のデータをせっせと書き写します。新しい場所で何が起きても、古い場所には関係ありません。この「新しく場所を作る」という作業があるため、あまりにも巨大なデータ(例えば何千ページもある百科事典のようなデータ)を何度も値渡しすると、コピーに時間がかかり、パソコンの動作が少し重くなることがあります。しかし、数字や短い文字であれば、全く気にする必要はありません。
5. 複数の値を渡すパターンの例
関数には、一つの材料だけでなく、たくさんの材料(引数)を渡すことができます。次の例では、初期の体力と受けたダメージを渡して、計算を行うプログラムを作成します。もちろん、これも値渡しなので、元の体力が勝手に削られることはありません。
#include <iostream>
// ダメージ計算をする関数(二つの値をコピーして受け取る)
void calculateDamage(int hp, int damage) {
hp = hp - damage;
std::cout << "計算後の体力(関数内): " << hp << std::endl;
}
int main() {
int myHp = 100;
int enemyAttack = 30;
calculateDamage(myHp, enemyAttack);
std::cout << "現在の残り体力(元の変数): " << myHp << std::endl;
return 0;
}
計算後の体力(関数内): 70
現在の残り体力(元の変数): 100
このように、複数の引数を使っても、それぞれの変数が個別にコピーされる仕組みは変わりません。関数の中でどんなに激しいバトルが行われても、元のmyHpという箱は守られているのです。
6. 文字列(文章)の値渡しに挑戦
数字だけでなく、文字の塊である文字列(string)も値渡しが可能です。文字列を扱うときは、C++ではstd::stringという型を使います。名前を少し加工して表示する関数を作ってみましょう。元の名前が書き換わらない安心感を確認してください。
#include <iostream>
#include <string>
// 名前に敬称をつける関数
void addSan(std::string name) {
name = name + "さん";
std::cout << "呼び出し中: " << name << std::endl;
}
int main() {
std::string myName = "太郎";
addSan(myName);
std::cout << "登録されている名前: " << myName << std::endl;
return 0;
}
呼び出し中: 太郎さん
登録されている名前: 太郎
「太郎さん」という新しい文字列が関数の中で作られましたが、元のmyNameは「太郎」のままです。このように、データの「原本」を守りたいとき、値渡しは非常に心強い味方になります。
7. 値渡しを使うメリットとデメリット
最後に、なぜわざわざコピーなんて面倒なことをするのか、その理由を整理しましょう。プログラミングにおいて、メリットとデメリットを知ることは非常に大切です。
メリット:
何といっても「安全」であることです。関数を作った人と、それを使う人が別人だった場合、勝手に元のデータを変えられてしまうとトラブルの元になります。値渡しなら、関数がどんなに暴走しても元のデータは無事です。また、仕組みがシンプルなので、初心者でもバグ(プログラムのミス)を出しにくいのが特徴です。
デメリット:
「コピーのコスト」がかかることです。先ほど百科事典の例を出しましたが、非常に大きなデータを扱う場合、コピーを作るだけでパソコンのメモリやパワーを消費してしまいます。将来的に、より高度なプログラムを書くようになると「参照渡し」というコピーしない方法も学びますが、まずはこの「安全なコピー」の感覚をしっかりと身につけましょう。
8. 応用編:計算結果を戻り値で受け取る
「元の変数が変わらないなら、どうやって計算結果を反映させればいいの?」と思うかもしれません。その答えは、前回の記事でも触れた「戻り値(もどりち)」を使うことです。値渡しでコピーをもらい、計算した結果をまたコピーして返してあげる。これが一番綺麗な流れです。
#include <iostream>
// 値渡しで受け取り、結果を「戻り値」として返す
int doubleScore(int score) {
return score * 2;
}
int main() {
int myScore = 50;
// 関数の結果を、元の変数に上書きする
myScore = doubleScore(myScore);
std::cout << "最新のスコア: " << myScore << std::endl;
return 0;
}
最新のスコア: 100
このプログラムでは、一度コピーされた値を計算し、その答えをmyScoreという箱に「入れ直して」います。これなら、値渡しの安全性を保ちつつ、データを更新することが可能になります。C++の基本は、この「渡して、返してもらう」のキャッチボールにあるのです。