こんにちは、ナナです。
C++に新しく追加された「参照」と呼ばれる機能があります。
C言語において関数の引数には「値渡し」と「ポインタ渡し(アドレス渡し)」がありますが、「参照」機能が追加されたことにより、新たに「参照渡し」と呼ばれる渡し方が生まれました。
「参照」とはどのような機能なのか、そして「参照渡し」とはいったい何なのかを学んでいきましょう。
C++で追加された「参照」という機能
C++の新しい機能である「参照」からまずは紹介しましょう。
変数定義のおさらい
「参照」は変数定義と関連する機能です。変数定義とは、メモリ上に変数ラベルを貼りつける行為でした。
#include <stdio.h>
int main()
{
short num = 50; // 変数定義
printf("num:%d\n", num);
return 0;
}
このプログラムが動き出すと「num」という変数ラベルがメモリ上に貼り付けられることになります。
変数定義で基本となるイメージは、メモリに変数名ラベルを貼りつけることですよ。
「参照」はどのような機能なのか?
C++に新しく追加された「参照」とは、
とある変数ラベルに対して、異なるラベルを貼りつけることができる機能
です。
まずはプログラムで「参照」を示しましょう。
#include <stdio.h>
int main()
{
short num = 50; // 変数定義
short & refnum = num; // 参照変数の定義
printf("num:%d\n", num);
refnum = 80; // 参照変数の書き換え
printf("num:%d\n", num);
return 0;
}
num:50
num:80
プログラムと実行結果をよく考察しましょう。
「refnum」変数に「80」の値を代入した結果、「num」変数の値が「80」に変化しました。
実際に同一メモリに存在することを確認してみましょう。
#include <stdio.h>
int main()
{
short num = 50;
short & refnum = num;
// 「num」「refnum」の番地表示
printf("num :0x%p\n", &num);
printf("refnum:0x%p\n", &refnum);
return 0;
}
num :0x009DF984
refnum:0x009DF984
このように、同じメモリ番地に存在していることがわかりますね。これこそが「参照」の機能となります。
初期化後の「参照変数」は、参照元の変数と同じように扱うことが可能となります。
参照変数ことを「エイリアス」とも呼びます。エイリアスとは「別名」を意味する言葉であり、参照の機能とは「とある変数ラベルに別名のラベルを貼る機能」なのです。
参照変数の定義方法
参照変数の定義は次のように行います。参照先の変数名を必ず初期化で指定します。
具体的には次のように指定することになります。
一度初期化で設定した参照ラベルの場所を、後から別の変数へと変更することはできません。
変数名の前に「&」を付けることで、変数が「参照変数」であることを示します。「ポインタ変数」の場合に「*」を付けるのと似ていますね。
定義の間違い①:参照先のデータ型名を間違えた場合
short num = 50;
long & refnum = num;
error C2440: '初期化中': 'short' から 'long &' に変換できません。
このように参照先のデータ型と参照変数の型は一致させないとビルドエラーが発生します。
定義の間違い②:初期値を指定しない場合
short & refnum;
error C2530: 'refnum': 参照が初期化されずに宣言されています。
このように参照変数は初期化を行わないとビルドエラーが発生します。
関数の引数で利用できる「参照渡し」とは
C++では「参照」という機能を関数の引数に利用することで、「参照渡し」と呼ばれる渡し方が可能になりました。
引数が「参照」である関数の定義と使い方
さっそく、「参照」を引数に持つ関数定義をお見せしましょう。
#include <stdio.h>
void funcB(short & numB)
{
numB = 80;
}
int main()
{
short numA = 50;
printf("numA:%d\n", numA);
funcB(numA);
printf("numA:%d\n", numA);
return 0;
}
このプログラムを動かすと、C言語ではありえない結果が表示されます。
numA:50
numA:80
funcB関数を呼び出した後に、ローカル変数「numA」の値が書き換わっていますね。
これが「参照」を引数で利用した「参照渡し」の効果です。
funcB関数で「numB」書き換えた結果が、呼び出し元の参照先である「numA」変数に影響を与えていますね。
引数で「参照」を使うと呼び出し元の変数にアクセスできる
先ほどのプログラムをイメージ図で表現してみましょう。
このように「numA」変数と「numB」参照変数は同一メモリであり、「numB」を書き換えると「numA」の値も変化するわけです。
このような芸当はC言語では「ポインタ」を使う必要性がありました。
しかし、C++では「ポインタ」以外の選択肢として「参照」を利用して呼び出し元の変数にアクセスできるようになったのです。
関数呼び出し元のローカル変数に対して、呼び出された側の関数から自然な表現でアクセスできるようにしたのが「参照」です。
「参照渡し」と「ポインタ渡し」のプログラムの違い
それでは、「参照渡し」と「ポインタ渡し」で同等のプログラムを書いて差分を見てみましょう。
#include <stdio.h>
void funcB(short & numB)
{
numB = 80;
}
int main()
{
short numA = 50;
printf("numA:%d\n", numA);
funcB(numA);
printf("numA:%d\n", numA);
return 0;
}
numA:50
numA:80
#include <stdio.h>
void funcB(short * numB)
{
*numB = 80;
}
int main()
{
short numA = 50;
printf("numA:%d\n", numA);
funcB(&numA);
printf("numA:%d\n", numA);
return 0;
}
numA:50
numA:80
「ポインタ」を利用した例では、アドレス演算子(&)や間接参照演算子(*)を駆使して変数にアクセスできるようにする必要があります。
それに対し、「参照」の場合はそれらが不要であるにもかかわらず、同じ結果を出力できるようになっています。
つまり、「参照」という機能は変数を関数で受け渡す際の「ポインタ」の記述を簡易的に行えるようにするための機構であるということです。
「参照」を利用するとポインタのような独特な記述が不要になるため、初学者の方はより扱いやすくなるでしょう。
戻り値を「参照」とするときの注意
引数だけでなく、戻り値を「参照」とすることも可能です。
#include <stdio.h>
// グローバル変数として「numB」を定義
short numB = 50;
// 「参照」を戻り値の型を持つ関数
short& funcB()
{
return numB; // 「numB」の参照を戻り値とする
}
int main()
{
printf("numB:%d\n", numB);
short& numA = funcB(); // numBへの参照を設定
numA = 80; // numAを書き換えると参照先の「numB」も変わる
printf("numB:%d\n", numB);
return 0;
}
numB:50
numB:80
このように、呼び出し先である「funcB関数」からグローバル変数の参照を返却するなんてことも可能です。
ただし、次のようにローカル変数への「参照」を返してはいけません。
#include <stdio.h>
short& funcB()
{
// ローカル変数として「numB」を定義
short numB = 50;
return numB;
}
int main()
{
short& numA = funcB();
// 不正!numAを書き換えると破棄されたnumBのメモリを書き換える
numA = 80;
return 0;
}
ローカル変数である「numB」は、funcB関数をreturnした瞬間にスタックメモリから破棄されます。
破棄されたメモリへの参照を持つ「numA」は不正なメモリを示す参照となります。
「参照」とは、あくまでも本物に対する別名なのです。本物がいなくなった状態での別名は使ってはならないのです。
そのため、戻り値で「参照」を返す変数は、静的メモリ上の変数かメンバ変数となります。
スタックメモリに関して忘れている方は『C言語 スタックメモリ【ローカル変数が確保される仕組みを解説】』を読んでおくとよいでしょう。
VisualStudioのコンパイラは、この辺りのチェックが厳しくエラーを出力してくれます。
コンパイラのエラーやワーニングは、コンパイラの種類のよって変化するため、皆さん自身もこのようなプログラムを書いてはいけないことを知っておく必要があります。
「参照」に関するその他の豆知識を紹介
ここまでの「参照」に関して紹介しきれてない特別なルールを紹介しましょう。
定数への「参照」の作り方
「参照」は変数だけでなく定数も指定が可能です。ただし、定数は書き換えができませんので次のように「const」修飾子を指定する必要があります。
#include <stdio.h>
int main()
{
const short & num = 100; // 定数への参照変数定義
printf("num:%d", num);
return 0;
}
num:100
もちろん、「const」な参照変数であるため書き換えることはできません。
#include <stdio.h>
int main()
{
const short& num = 100;
num = 500; // 書き換えはビルドエラー
printf("num:%d", num);
return 0;
}
error C3892: 'num': const である変数へは割り当てることはできません
定数値を参照することは「ポインタ変数」ではできない機能であり、「参照」ならではの機能です。
配列への「参照」の作り方
配列に対しても「参照」を作ることが可能です。
#include <stdio.h>
int main()
{
short num[5] = {0};
short (&refnum)[5] = num;
int i;
// 参照から配列データを変更
for (i=0; i < 5; i++)
{
refnum[i] = i * 100;
}
// 参照元の配列を表示
for (i = 0; i < 5; i++)
{
printf("num[%d]:%d\n", i, num[i]);
}
return 0;
}
num[0]:0
num[1]:100
num[2]:200
num[3]:300
num[4]:400
このように「配列への参照」を使って、配列にアクセスすることもできます。
ただし、参照変数の定義時に配列要素数を一致させる必要があります。
関数を呼び出す際に配列要素の数は呼び出す側に依存するため、「配列への参照」に配列要素数の指定が必要なことはかなり大きな制約です。
つまり、配列の引数に対する渡し方は「参照渡し」よりも「ポインタ渡し」の方が適しているということになります。
「参照」にはNULLポインタが存在しない
「ポインタ」には、NULLポインタという概念があり、ポインタが無効であることを示す特別なデータとして「NULL」が定義されています。
それに対し、「参照」には「NULL」という概念がありません。
「参照」とは初期化した変数に対する別名であり、無効な参照というものがないためです。
int func(int * data)
{
if (data == NULL)
{
return -1;
}
*data = 100;
return 0;
}
int func(int & data)
{
data = 100;
return 0;
}
このように「ポインタ」でよく行うNULLチェックは「参照」では必要ありません(というよりも、NULLチェックはできないと言った方が正しいです)。
ポインタにおける「NULLチェック」は念のためのチェックであり、結構面倒なプログラムです。「参照」を使うことでNULLチェックをしなくてよいのはメリットでしょう。
どっちを使う?「参照渡し」VS「ポインタ渡し」
関数への引数の渡し方において「参照渡し」と「ポインタ渡し」は同様の効果を得ることができます。
そうなると困るのは「どっちを使った方がいいの?」という点です。
ここは個人の見解になりますが、次のような方針で私は対応しています。
C++のシステムはクラスが連動して動作するため、クラスオブジェクトが引数で登場することは非常に多いです。そのため、「参照」による受け渡しが基本になります。
これは関数への引数の渡し方に関しての見解です。「ポインタ」という機能は引数の受け渡し以外でも色々と使い道があります。
「ポインタ」はC言語やC++において欠かせない道具であることは、言うまでもありません。
組み込み型の受け渡し方法に対して「ポインタ渡し」がよい理由
int型のような組み込み型の場合は、「参照渡し」を使うと関数の呼び出し側のプログラムを見ても書き換わる可能性を検知しづらくなります。
左側の関数を呼び出す側のプログラムだけを見てください。
int main()
{
short num = 50;
// numの値が変わることが
// func関数を見ないとわからない
func(num);
printf("num:%d", num);
return 0;
}
void func(short & num)
{
num = 80;
}
このプログラムだけでは「num」変数が書き換えられる可能性を検知することができません。
右側のfunc関数の引数が「参照渡し」になっていることを見て初めて書き換えられることもあるかもしれない、と知ることができます。
対して「ポインタ渡し」の場合は、次のように呼び出し側のプログラムを見ただけで書き換えられる可能性を検知することができます。
int main()
{
short num = 50;
// アドレス演算子があるため
// numが書き換わる可能性を検知できる
func(&num);
printf("num:%d", num);
return 0;
}
void func(short * num)
{
*num = 80;
}
C言語出身の開発者は、このような呼び出し側のプログラムを見て「アドレス演算子を使ってるから、num変数が書き換わってるかもしれないな~」と感じ取りながらプログラムを解析しています。
「参照」にしてしまうと、呼び出し側のプログラムにおいて「値渡し」と「参照渡し」の区別がつかなくなるのです。
「参照渡し」「ポインタ渡し」以外に「値渡し」があります。「値渡し」はコピー値を渡す最も基本的な受け渡しなので、皆さん大丈夫ですね。