参照
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;
//参照
//いずれもエラー
int &r1;
int &r2 = NULL;
int &r3 = (int&)1;
参照を正しく宣言するには、宣言と同時に他のオブジェクトのアドレスで初期化する必要があります。
int num = 10;
int &r1 = num;
//初期化しない参照は作れない
//エラー
int &r2;
参照はポインタ演算によって別のオブジェクトを指すように変更はできません。
それどころか、代入で別のオブジェクトを指すように変更することもできません。
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;
このように、参照は特定のオブジェクトと強く結びついていることがわかります。
参照は「元のオブジェクトそのもの」「元のオブジェクトの別名」と考えるといいでしょう。
引数の参照渡し
参照は関数と共に用いられることが多いです。
そのひとつが引数の参照渡しです。
#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, int len)
//以下のようにしても同じ
//void showArr(int arr[], int len)
//void showArr(int arr[5], int len)
{
for (int 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])
{
int len = sizeof(arr) / sizeof(arr[0]);
for (int 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」と書いてしまっているのだからそれをそのまま使用しても構いません。
(マジックナンバーが嫌なら定数化(#define、const)してしまうと良いです)
ポインタ演算はできない
参照渡しで受け取るのはポインタではないので、ポインタ演算は行えません。
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++; //エラー
}