C++入門|引数の参照渡し

コピーされた気ではなく、本物の気に直接触れる。参照渡しを使えば、関数の中から呼び出し元の変数を変えられます。

C++では、関数へデータを渡す方法として、値渡し、ポインタ渡し、参照渡しがあります。

これまでよく使ってきた値渡しでは、関数へ変数の値を渡すと、その値のコピーが作られます。関数の中でコピーを書き換えても、呼び出し元の変数には影響しません。

一方、参照渡しでは、関数の仮引数が呼び出し元の変数の別名として働きます。そのため、関数の中で仮引数を書き換えると、呼び出し元の変数も変化します。

ドラゴンボール風にたとえるなら、値渡しは戦士の気力データを複製した訓練用ホログラムを渡す方法です。ホログラム上の数値を変更しても、本物の戦士の気力は変わりません。

参照渡しは、修行装置が戦士本人の気力データへ直接接続する方法です。装置の中で気力を変更すると、戦士が実際に持っている気力も変化します。

参照渡しは便利ですが、関数の中から呼び出し元の変数を書き換えられるため、意図しない変更を起こす危険もあります。

渡し方関数へ渡されるもの呼び出し元への影響
値渡し値のコピー関数内で変更しても影響しない
ポインタ渡しオブジェクトを指すアドレス指している変数を変更できる
参照渡し呼び出し元の変数に対する参照呼び出し元の変数を変更できる

値渡しでは元の変数は変わらない

まず、値渡しの動きを確認しておきましょう。

次の関数では、int型の値を値渡しで受け取っています。

void trainCopy(int energy) {
    energy = 100;
}

このenergyは、呼び出し元の変数そのものではありません。呼び出し時に渡された値をコピーした、関数内だけの変数です。

たとえば、main関数に次の変数があるとします。

int warriorEnergy = 50;
trainCopy(warriorEnergy);

trainCopyの中ではenergyが100になります。しかし、warriorEnergyの値は50のままです。

処理のイメージは次のようになります。

場所変更前関数内で代入したあと
main関数のwarriorEnergy5050
trainCopy関数のenergy50100

値渡しでは、関数へ渡した値のコピーが作られます。そのため、関数の中で値を変更しても、呼び出し元には影響しません。

図:値渡しと参照渡しの違い

この図が示していること

値渡しでは、関数が扱うのは元の変数から作られたコピーです。関数内の値を変更しても、呼び出し元の変数は変化しません。

参照渡しでは、仮引数が呼び出し元の変数の別名として働きます。関数内で仮引数を変更すると、呼び出し元の変数にも同じ変更が反映されます。

参照渡しとは

参照渡しでは、関数の仮引数に参照型を指定します。

int型の変数を参照として受け取る場合は、型名のあとに&を付けます。

void chargeEnergy(int& energy)

このint&は、int型への参照を表します。

関数が呼び出されると、仮引数energyは呼び出し元の変数の別名になります。別の変数が新しく作られるのではなく、同じ変数を別の名前で扱えるようになります。

たとえば、main関数にwarriorEnergyという変数があるとします。

int warriorEnergy = 50;
chargeEnergy(warriorEnergy);

chargeEnergyの仮引数energyは、warriorEnergyを参照します。

関数の中で次の処理を行うと、warriorEnergyの値が変更されます。

energy = 100;

言語上は、energyがwarriorEnergyの別名になっていると考えるのが自然です。実装上は、コンパイラがアドレスを利用して処理することがありますが、プログラムを書く側はポインタのようにアドレス演算を行う必要がありません。

参照渡しを使ったサンプルプログラム

次のプログラムでは、戦士の気力を表す変数energyをmain関数で用意します。

最初に気力を表示したあと、chargeEnergyへ参照渡しします。chargeEnergyの中で値を変更すると、main関数のenergyにも変更が反映されます。

プロジェクト/ファイル名: Chap7_01/energy_reference.cpp

#include <iostream>

using namespace std;

// 戦士の気力を参照渡しで変更する関数
void chargeEnergy(int& energy);

// 現在の気力を表示する関数
void printEnergy(int energy);

int main(int argc, char** argv) {
    // 戦士の現在の気力
    int energy = 50;

    // 修行前の気力を表示する
    printEnergy(energy);

    // 参照渡しで本来の気力を変更する
    chargeEnergy(energy);

    // 修行後の気力を表示する
    printEnergy(energy);

    return 0;
}

// 参照している呼び出し元の変数を変更する
void chargeEnergy(int& energy) {
    energy = 100;
}

// 値渡しで受け取り、現在値を表示する
void printEnergy(int energy) {
    cout << "気力=" << energy << endl;
}

実行結果

気力=50
気力=100

chargeEnergyを呼び出す前は、energyの値は50です。

int energy = 50;

次に、energyをchargeEnergyへ渡します。

chargeEnergy(energy);

chargeEnergyの仮引数はint&なので、参照渡しになります。

void chargeEnergy(int& energy)

関数内のenergyは、main関数にあるenergyの別名です。そのため、次の代入はmain関数のenergyを書き換えます。

energy = 100;

chargeEnergyの処理が終わったあとも、main関数のenergyは100のままです。

関数の宣言でも&が必要になる

今回のプログラムでは、chargeEnergyをmain関数より前で宣言しています。

void chargeEnergy(int& energy);

関数の宣言と定義では、引数の型を一致させる必要があります。

宣言がint&なら、定義もint&にします。

void chargeEnergy(int& energy) {
    energy = 100;
}

宣言だけをintにして、定義をint&にすることはできません。値渡しと参照渡しは異なる引数の型として扱われるためです。

場所記述
関数宣言void chargeEnergy(int& energy);
関数定義void chargeEnergy(int& energy)
関数呼び出しchargeEnergy(energy);

関数を呼び出す側では、&を付ける必要はありません。

chargeEnergy(energy);

呼び出し方だけを見ると、値渡しとよく似ています。参照渡しかどうかは、関数の宣言や定義にある仮引数の型によって決まります。

参照渡しによる値の変更

参照渡しの流れを順番に確認してみましょう。

main関数では、energyという変数が50で初期化されています。

int energy = 50;

chargeEnergyを呼び出します。

chargeEnergy(energy);

chargeEnergyの仮引数energyは、main関数のenergyを参照します。

void chargeEnergy(int& energy)

関数内でenergyへ100を代入します。

energy = 100;

仮引数energyとmain関数のenergyは、同じ変数を表しているため、main関数へ戻ったあとも値は100です。

処理main関数のenergy
初期化50
chargeEnergyを呼び出す50
関数内でenergy = 100を実行100
関数終了後100

図:参照渡しで同じ変数を操作する流れ

この図が示していること

main関数のenergyとchargeEnergyの仮引数energyは、別々のデータではありません。

名前は同じでも、重要なのは参照型で受け取っていることです。chargeEnergyのenergyは、呼び出し元にあるenergyの別名として働きます。

そのため、関数内でenergyを100へ変更すると、main関数のenergyも100になります。

値渡しと参照渡しを比較する

値渡しと参照渡しは、関数の仮引数に&があるかどうかで区別できます。

void trainCopy(int energy);
void trainDirectly(int& energy);

trainCopyは値渡しです。関数には値のコピーが渡されます。

trainDirectlyは参照渡しです。仮引数が呼び出し元の変数の別名になります。

どちらの関数も、呼び出し側の書き方はよく似ています。

trainCopy(energy);
trainDirectly(energy);

そのため、呼び出し部分だけを見ても、元の変数が変更されるかどうかは判断できません。

関数の宣言や定義を確認する必要があります。

比較項目値渡し参照渡し
仮引数int energyint& energy
関数内で扱うもの値のコピー呼び出し元の変数の別名
関数内の変更呼び出し元へ反映されない呼び出し元へ反映される
呼び出し方trainCopy(energy)trainDirectly(energy)
主な用途元の値を変更せず利用する元の変数を変更する

ポインタ渡しと参照渡しの違い

ポインタを使っても、関数から呼び出し元の変数を変更できます。

ポインタ渡しの関数は、次のように定義できます。

void chargeWithPointer(int* energy) {
    *energy = 100;
}

関数を呼び出すときは、変数のアドレスを渡します。

chargeWithPointer(&energy);

参照渡しでは、関数の仮引数に&を付けます。

void chargeWithReference(int& energy) {
    energy = 100;
}

呼び出すときは、通常の変数名をそのまま渡します。

chargeWithReference(energy);
比較項目ポインタ渡し参照渡し
関数定義void func(int* value)void func(int& value)
関数呼び出しfunc(&value)func(value)
関数内の代入*value = 100value = 100
間接参照演算子必要不要
nullptr渡される可能性がある通常の参照では対象が必要

ポインタでは、関数内で*を使って指している先の値へアクセスします。

参照では、通常の変数と同じように扱えます。そのため、参照渡しは値渡しに近い見た目で、呼び出し元の変数を直接操作できます。

ただし、参照渡しであることが呼び出し部分からは分かりにくい点には注意が必要です。

参照渡しの便利なところ

参照渡しを使うと、関数から呼び出し元の変数を更新できます。

たとえば、修行前と修行後の気力を同じ変数で管理したい場合に便利です。

void chargeEnergy(int& energy) {
    energy += 50;
}

energyが50なら、この関数を呼び出したあとに100になります。

また、大きなクラスや文字列を関数へ渡す場合、参照を使うことでオブジェクト全体のコピーを避けられる場合があります。

ただし、読み取りだけを行う関数で元の値を変更したくない場合は、const参照を使う方法があります。

void printWarriorName(const string& name) {
    cout << name << endl;
}

const string&にすると、文字列のコピーを避けながら、関数内から値を書き換えないことを保証できます。

参照の形目的
int& value呼び出し元の値を変更できる
const int& valueコピーを避けながら読み取り専用で扱う
const string& name大きな文字列をコピーせず読み取る

参照渡しの危険性

参照渡しは便利ですが、関数の中から呼び出し元の変数を書き換えられます。

次の呼び出しだけを見ると、energyが変更されるかどうかは分かりにくいかもしれません。

chargeEnergy(energy);

値渡しの関数と見た目が同じだからです。

関数の定義を確認すると、参照渡しであることが分かります。

void chargeEnergy(int& energy)

参照渡しを使う関数では、関数名やコメントから、引数が変更されることを分かりやすくしておくと安心です。

たとえば、printEnergyという名前なら表示だけを行うと予想できます。chargeEnergyやresetEnergyという名前なら、気力が変更される可能性を想像しやすくなります。

次のように、意図しない変更が起こるプログラムには注意が必要です。

void resetEnergy(int& energy) {
    energy = 0;
}

呼び出し側が値の変更を予想していなければ、あとから不具合の原因になる可能性があります。

参照渡しを使うときは、次の点を意識するとよいでしょう。

確認すること理由
本当に元の変数を変更する必要があるか不要な副作用を防ぐため
関数名から変更内容が分かるか呼び出し側が動作を予想しやすくするため
読み取りだけならconst参照にできるか間違った書き換えを防ぐため
関数内のどこで値を変更しているか動作を追いやすくするため

図:参照渡しの利便性と危険性

この図が示していること

参照渡しを使うと、関数の中から呼び出し元の変数を直接変更できます。また、大きなデータのコピーを避ける目的でも利用できます。

一方で、呼び出し側の見た目だけでは、変数が変更されることが分かりにくい場合があります。

値を変更する必要がないならconst参照を使うことで、コピーを避けながら意図しない書き換えを防げます。

参照渡しとconst参照の使い分け

参照渡しには、元の変数を変更するための参照と、読み取りだけを行うconst参照があります。

元の変数を変更したい場合は、通常の参照を使います。

void chargeEnergy(int& energy) {
    energy += 50;
}

値を読むだけで変更しない場合は、const参照を使えます。

void showEnergy(const int& energy) {
    cout << energy << endl;
}

showEnergyの中で次のような代入をしようとすると、コンパイルエラーになります。

energy = 100;

constによって変更が禁止されているためです。

intのような小さな型は、読み取りだけなら値渡しでも十分なことが多いです。しかし、stringやvector、自作クラスなど、コピーにある程度の処理が必要な型では、const参照がよく使われます。

void showTechniques(const vector<string>& techniques);

この書き方なら、vector全体をコピーせずに関数へ渡し、関数内では読み取り専用として扱えます。

参照渡しを使うときに身につけたい感覚

参照渡しの仮引数は、呼び出し元の変数の別名として働きます。

void chargeEnergy(int& energy)

この関数へ変数を渡すと、関数内のenergyから呼び出し元の変数を操作できます。

int energy = 50;
chargeEnergy(energy);

関数内でenergyを100へ変更すると、main関数のenergyも100になります。

値渡しではコピーを操作します。
参照渡しでは呼び出し元の変数を操作します。

ポインタ渡しでも呼び出し元の変数を変更できますが、呼び出し時にはアドレスを渡し、関数内では間接参照が必要です。

参照渡しは、通常の変数に近い書き方で扱えるため、コードを簡潔にできます。ただし、簡潔だからこそ、関数内で値が変更されることを見落とさないように注意が必要です。

ドラゴンボール風に考えるなら、値渡しは戦士の気力データを複製した訓練用ホログラムです。参照渡しは、戦士本人の気力へつながる調整装置です。

ホログラムを変更しても本人は変わりませんが、本人へつながった装置を操作すれば、実際の気力も変わります。

元の変数を変更したいなら参照渡しを使い、読み取りだけなら値渡しやconst参照を選びます。この使い分けができるようになると、関数へデータを渡す方法を、目的に合わせて正しく選べるようになります。

表現の硬さや記事の分量は、これまでの6章の記事に合わせて調整できます。