参照
C言語にはポインタという機能があります。
これは変数等のメモリ上の位置を取得し処理を行うものです。
(C言語のポインタの項を参照)
C++にはポインタに似た概念である参照という機能があります。
参照とは
参照(reference)は何かしらのオブジェクト(変数や関数などのプログラムの部品のこと)を指すものです。
#include <iostream>
int main()
{
int num = 10;
//参照
int &ref = num;
//参照を通してnumを書き換え
ref = 20;
//「20」を表示
std::cout << num << std::endl;
std::cin.get();
}
変数名の頭に&
を付けて宣言されている変数が参照です。
(int& ref
と&記号をデータ型の方に付けても構いません)
&
記号はポインタの項でも登場したアドレス演算子ですが、変数宣言時に使用すると参照変数の宣言となります。
参照変数ref
は宣言時に変数num
で初期化しています。
この参照変数refの値を書き換えると、変数num
を書き換えることができます。
この動作はポインタに似ています。
参照とポインタの違い
ポインタはメモリ上の位置を示すものです。
ポインタが指す先には必ずしも意味のあるデータが存在することは保証されていません。
宣言と同時に初期化をしなければ、何処を指しているかは不定です。
ポインタ変数に意味のないアドレスを渡すこともできますし、NULLポインタといって「何も指していない」ポインタを作ることもできます。
ポインタ演算の結果、意味のないアドレスを指すこともあります。
対して、参照は必ずオブジェクトを指していることが求められます。
ポインタ変数のようにNULLポインタや任意のアドレスを渡すことはできませんし、ポインタ演算で指し示す先が変わることもありません。
//ポインタ
int *p1;
int *p2 = NULL;
int *p3 = (int*)1; //メモリの「1」番地の意味
//参照
//いずれもエラー
int &r1;
int &r2 = NULL;
int &r3 = (int&)1;
参照を正しく宣言するには、宣言と同時に他のオブジェクトで初期化する必要があります。
int num = 10;
int &r1 = num;
//以下は全てエラー
//初期化しない参照は作れない
int &r2;
//一時オブジェクトの参照は作れない
int &r3 = 1;
int &r4 = (int&)1;
参照はポインタ演算によって別のオブジェクトを指すように変更はできません。
代入で別のオブジェクトを指すように変更することもできません。
int nums[] = { 10, 20, 30 };
int &r1 = nums[0];
//ポインタ演算ではない
//nums[0]++;と同義
r1++;
//「11」を表示
std::cout << nums[0] << std::endl;
std::cout << r1 << std::endl;
int numA = 10;
int numB = 20;
int &r2 = numA;
//エラー
&r2 = numB;
//numAにnumBを代入しているに過ぎない
r2 = numB;
このように、参照は特定のオブジェクトと強く結びついていることがわかります。
参照は「元のオブジェクトそのもの」「元のオブジェクトの別名」と考えるといいでしょう。
なお、参照自体は変数のようなオブジェクトではないため、参照のポインタ、参照の参照、参照の配列は作ることができません。
ポインタの参照、配列の参照は可能です。
int a = 1;
//いずれもエラー
int& *p = &a;
int& &r = a;
int& arr[1] = { a };
//ポインタの参照
int* p;
int*& r1 = p;
//配列の参照
int arr[] = { 1, 2, 3 };
int(&r2)[3] = arr;
配列の参照は記法が少し特殊なので注意してください。
参照の宣言側に要素数の指定が必要で、要素数の異なる配列への参照はできません。
引数の参照渡し
参照は関数と共に用いられることが多いです。
そのひとつが引数の参照渡しです。
#include <iostream>
#define NAME_LENGTH 50
enum Gender
{
MALE,
FEMALE
};
struct Person
{
char name[NAME_LENGTH];
int age;
enum Gender gender;
};
void showPerson(Person[], int);
void showPerson(Person&);
void showPerson(Person p[], int len)
{
for (int i = 0; i < len; i++)
{
//ポインタではなくそのまま渡す
showPerson(p[i]);
std::cout << std::endl;
}
}
//引数pは参照渡し
void showPerson(Person &p)
{
using std::cout; using std::endl;
//メンバ変数はドット演算子でアクセスする
cout << "name: " << p.name << "\n";
cout << "age: " << p.age << "\n";
if (p.gender == MALE)
cout << "gender: 男" << endl;
else
cout << "gender: 女" << endl;
}
int main()
{
Person person[] = {
{ "A山B男", 20, MALE },
{ "C下D太", 18, MALE },
{ "E田F子", 21, FEMALE },
{ "G山H美", 19, FEMALE },
};
showPerson(
person,
sizeof(person) / sizeof(person[0])
);
std::cin.get();
}
32~43行目のshowPerson
関数で参照渡しを利用しています。
引数の型にアドレス演算子(&
記号)を付加することで、引数を参照で受け取ることができます。
呼び出し元の実引数はポインタではなく、変数をそのまま渡します。
(26行目)
ポインタ渡しと参照渡しの違い
ポインタ渡しと参照渡しは似ています。
関数内で引数を書き換えれば、呼び出し元にも影響するのは同じです。
しかし同じものではありません。
参照は「元のオブジェクトそのもの」です。
つまり、呼び出し元の実引数と同じ扱いをします。
構造体のメンバ変数へのアクセス方法
実引数そのものなので、構造体のメンバ変数のアクセスにはアロー演算子(->
)ではなくドット演算子(.
)を使います。
アロー演算子はポインタを通してアクセスするためのものだからです。
引数に配列を指定する場合
配列を関数の引数に指定する場合にも違いがあります。
ポインタ渡しの場合は以下のような記述になります。
#include <iostream>
void showArr(int *arr, size_t len)
//以下のようにしても同じ
//void showArr(int arr[], size_t len)
//void showArr(int arr[5], size_t len)
{
for (size_t i = 0; i < len; i++)
{
std::cout << arr[i] << std::endl;
}
}
int main()
{
int nums[] = { 1, 2, 3, 4, 5 };
showArr(nums, 5);
std::cin.get();
}
引数には配列の要素数は指定できず、関数内からその配列の要素数を知る方法がありません。
(int arr[5]
のようにサイズを指定しても無視されます)
配列の要素数が関数内で必要な場合は、要素数を別の引数として関数に渡します。
これを参照渡しに直すと以下のようになります。
#include <iostream>
void showArr(int (&arr)[5])
{
size_t len = sizeof(arr) / sizeof(arr[0]);
for (size_t i = 0; i < len; i++)
{
std::cout << arr[i] << std::endl;
}
}
int main()
{
int nums[] = { 1, 2, 3, 4, 5 };
showArr(nums);
std::cin.get();
}
配列を参照渡しする場合、仮引数は配列の要素数まで指定する必要があります。
この関数で使用できるのは「要素数が5のint型配列」に限定され、要素数が異なる配列を実引数に指定するとエラーになります。
代わりに、関数内からは配列の要素数を知ることができます。
sizeof
演算子で取得しても良いですし、引数の定義で「5」と書いてしまっているのだからそれをそのまま使用しても問題は起こりません。
ポインタ演算はできない
参照渡しで受け取るのはポインタではないので、ポインタ演算は行えません。
void showArr(int (&arr)[5])
{
int len = sizeof(arr) / sizeof(arr[0]);
for (int i = 0; i < len; i++)
{
std::cout << arr[0] << std::endl;
arr++; //エラー
}
}
これは以下のように配列にそのまま整数値を加算しようとしているのと同じことですので、エラーになります。
int main()
{
int nums[] = { 1, 2, 3, 4, 5 };
nums++; //エラー
}
参照返し
関数の戻り値にも参照を指定することができます。
int& func(int& n) {
static int a = n * 2;
return a;
}
int main()
{
int x = 10;
int y = func(x); //static変数の値のコピー
int &z = func(x); //static変数への参照
}
関数の戻り値を非参照型で受け取ると、戻り値をコピーして変数に代入します。
コピーなので、上記の変数y
を書き換えても関数func
内のstatic変数の値に影響はありません。
変数z
は参照型で受け取っているため、それはstatic変数への参照なので、変数zを書き換えるとstatic変数も書き換わります。
ダングリング参照
注意点として、関数内で宣言した非staticなローカル変数を返してはいけません。
関数内のローカル変数の寿命は関数を抜けるまでなので、関数終了後にそのメモリ領域に格納されている値は不定です。
不定なメモリ領域への参照となってしまうので不具合が生じる可能性があります。
関数を抜けた直後はまだメモリ上にデータが残っている可能性はあるので、すぐに使用する場合は問題が起こらない可能性はあります。
しかしそれは偶然そうなっただけで、常にそうである保証があるわけではないので、避けなければなりません。
#include <iostream>
int& func() {
int b = 1;
return b;
}
int main()
{
int& a = func();
std::cout << a; //不定値を出力する
}
このような、参照先のデータが存在しなくなった参照をダングリング参照と言います。
ポインタの場合は容易にダングリングポインタを作ることはできますが、ダングリング参照はプログラマが気付きにくいところで発生することが多いので注意が必要です。
受け取り側が非参照型変数の場合は戻り値はコピーされるので問題は起こりませんが、参照型変数で受け取ることができず、そもそも参照で返す意味のない使いづらい関数となります。
なお、static変数はプログラム実行中は生存しているので参照で返しても問題ありません。
ただし、static変数との演算の結果を返すことはできません。
int& func(int& n) {
static int a = n;
//コンパイルエラー
return a * 2;
}
//これがコンパイルできないのと同じ
int &x = 10;
演算の結果として得られる値は「その場限り」の値で、その式が終わるとすぐに消えてしまう値です。
このような値は一時オブジェクトといいます。
一時オブジェクトは変数へ格納することによって変数への値のコピーが発生し、以降の行で使用することができます。
一時オブジェクトへのポインタや参照は作れないので、コンパイル自体ができません。
(ただし後述するconst参照による例外があります)
クラスのメンバ変数を返す
参照返しはstatic変数を返せるほか、クラスのメンバ変数を返す際にも使用できます。
クラス機能はまだ説明していないので参考程度に見てください。
class C
{
int number;
public:
//メンバ変数numberの参照を返す
int& func() {
return number;
}
};
int main()
{
C a;
//メンバ変数numberの値を変数に代入
int num = a.func();
//メンバ変数numberに値を代入
a.func() = 10;
}
a.func() = 10;
は奇妙なコードに見えますが正常に動作します。
参照は参照先の値そのものなので、これはメンバ変数number
に値を代入することになります。
インスタンスの寿命を超えて参照を持ち続けるとダングリング参照となります。
const参照
参照を通して値が書き換えられると困る場合はconst修飾子を付けることで書き換えを防ぐことができます。
struct S {
int x;
};
//const参照渡し
void func(const S& s) {
//コンパイルエラー
s.x = 10;
}
int main()
{
S s = { 0 };
func(s);
}
ただしポインタのときと同じように、クラスおよび構造体のメンバがポインタや参照である場合に、その参照先は書き換え可能です。
struct S {
int x;
int& y = x;
};
int main()
{
S s;
const S& r = s;
//r.x = 1; //NG
r.y = 1; //OK
}
一時オブジェクトへの参照
数値リテラルや関数の戻り値などは一時オブジェクトであり、使用後にすぐに消えてしまうため参照することはできません。
(変数に格納する場合は一時オブジェクトのコピーが行われます)
しかしconst参照の特殊な仕様として、一時オブジェクトを参照することができます。
int func()
{
return 1;
}
int main()
{
//int& a = 1; //一時オブジェクトは参照できない
const int& b = 2; //const参照ならOK
//int& c = func(); //NG
const int& d = func(); //OK
}
このとき値のコピーは行われず、一時オブジェクトそのものへの参照となります。
一時オブジェクトの生存期間はconst参照の生存期間と同じだけ延長されます。
ただし、参照を返す関数の戻り値にconstを指定しても生存期間は延長されません。
つまり関数内のローカル変数や一時オブジェクトをconst参照で返しても、生存期間を延長することはできません。
const int &f()
{
int a = 1;
return a; //変数aはreturn文の直後に破棄される
}
int main()
{
const int& a = f();
//この時点で参照aの値は不定
}