U - 2.04.参照 Editorial /

Time Limit: 0 msec / Memory Limit: 0 KB

前のページ | 次のページ

キーポイント

  • 参照先の型 &参照の名前 = 参照先;で参照を宣言できる
  • 通常の変数のように参照を宣言するときは参照先を指定する必要がある
  • 関数の引数に参照を用いる場合は、その関数を呼び出す時に渡した変数が参照先になる
  • 参照先を後から変更することはできない
  • 引数が参照になっている関数を呼び出すことを参照渡しという
  • 参照渡しは、無駄なコピーを避けたり複数の結果を返したいときに便利

参照

参照の機能を使うと、ある変数に別名を付けるようなことができます。
ある変数への参照を作ったとき、参照からその変数へアクセスすることができます。

次の例を見てください。

#include <bits/stdc++.h>
using namespace std;

int main() {
  int a = 3;
  int &b = a;  // bは変数aの参照

  cout << "a: " << a << endl;  // aの値を出力
  cout << "b: " << b << endl;  // bの参照先の値を出力(aの値である3が出力される)

  b = 4;  // 参照先の値を変更(aが4になる)

  cout << "a: " << a << endl;  // aの値を出力
  cout << "b: " << b << endl;  // bの参照先の値を出力(aの値である4が出力される)
}
実行結果
a: 3
b: 3
a: 4
b: 4

この例では、変数aを参照するbという参照を用意して、b = 4;と書き換えています。 その結果、aの値が4に変更されています。 参照bは変数aと同じように(aであるかのように)振る舞っています。

変数の宣言方法と似ていますが、通常の変数とは違い参照変数自体の値にアクセスすることはできません。 参照変数に対してアクセスを行うように書くと、そのアクセスが参照先についてなされるイメージです。

参照の宣言

参照は次のように宣言します。

参照先の型 &参照の名前 = 参照先;

基本的には宣言時に参照先を指定して初期化する必要がある点に注意してください。
また、あとから参照先を変更することはできません。

例:
int a = 123;
int &b = a;  // int型変数aへの参照

string s = "apg4b";
string &t = s;  // string型変数sへの参照

vector<int> v = {1, 2, 3, 4, 5};
vector<int> &w = v;  // vector<int>型変数vへの参照

int &c;  // 参照先が指定されていないためコンパイルエラーになる

参照のアクセス

参照に対して行った操作が、参照先に対して行ったように扱われます。

例1: 整数の場合
int a = 0;
int &b = a;

b = b + 1;  // a = a + 1; と同じ結果になる

cout << a << endl;  // "1"が出力される

例2: 文字列の場合
string s = "apg4b";
string &t = s;

// 以下の操作で参照先のsが書き換わる
t.at(0) = 'A';
t.at(1) = 'P';
t.at(2) = 'G';

cout << s << endl;  // "APG4b"が出力される
cout << t << endl;  // 参照先のsの値"APG4b"が出力される

参照の複製

既にある参照と同じ参照先をもつ参照を作ることもできます。

int a = 123;
int &b = a;  // 変数aへの参照
int &c = b;  // 変数aへの参照

「参照への参照」のようにならない点に注意してください。


関数の引数での参照

関数の引数を参照にすることもできます。

関数の引数を参照にすると、関数の中で呼び出す側の変数を書き変えることができます。

通常の関数(値渡し)

まずは参照を用いない関数の例です。

#include <bits/stdc++.h>
using namespace std;

int f(int x) {
  x = x * 2;  // 2. xを2倍
  return x;   // 3. xの値を返す
}

int main() {
  int a = 3;  // "呼び出す側の変数"
  int b = f(a);  // 1. aの値をfに渡し、4. 結果をbに代入
  cout << "a: " << a << endl;
  cout << "b: " << b << endl;
}
実行結果
a: 3
b: 6

順を追ってプログラムの動きを確認します。

  1. f(a)faを渡します。このとき、fの引数のxint x = a;のようにaの値がコピーされます。つまり、x = 3です。
  2. fの内部ではxを2倍します。
  3. 変更後の値を返します。x = 3なので、2倍されてx = 6となり、6が返されます。
  4. int b = f(a);で結果をbにコピーします。
  5. a = 3, b = 6となります。

int b = f(a);の呼び出しを展開してみると次のプログラムのようになります。

#include <bits/stdc++.h>
using namespace std;

int main() {
  int a = 3;  // "呼び出す側の変数"
  int b;
    // int b = f(a); を展開
    {
      int x = a;  // aの値がxにコピーされる(引数)
      x *= 2;     // xが2倍される
      b = x;      // xの値がbに代入される(返り値)
    }
  cout << "a: " << a << endl;  // "a: 3"
  cout << "b: " << b << endl;  // "b: 6"
}

このように、渡した変数の値がコピーされるような渡し方を値渡しといいます。

引数が参照になっている関数(参照渡し)

次に、引数を参照にした関数の例です。

#include <bits/stdc++.h>
using namespace std;

int g(int &x) {
  x = x * 2;  // xを2倍 (参照によって"呼び出す側の変数"が変更される)
  return x;
}

int main() {
  int a = 3;  // 関数を呼び出す側の変数
  int b = g(a);  // xの参照先がaになる
  cout << "a: " << a << endl;
  cout << "b: " << b << endl;
}
実行結果
a: 6
b: 6

gの呼び出しの前後で、変数aの値が変わっていることが分かります。
「参照の宣言」では宣言時に参照先を指定して初期化する必要があると書きましたが、引数を参照にした場合は、 呼び出し時に渡した変数が参照先となります。
上のプログラムでは引数のxが参照であり、参照先が呼び出す側のaになるので、関数内でxを書き換えるとaの値が変更されます。

int b = g(a);の呼び出しを展開してみると次のプログラムのようになります。

#include <bits/stdc++.h>
using namespace std;

int main() {
  int a = 3;  // "呼び出す側の変数"
  int b;
    // int b = g(a); を展開
    {
      int &x = a; // xの参照先がaになる
      x *= 2;     // xが2倍される(つまりaが2倍される)
      b = x;      // xの値(aの値)がbに代入される
    }
  cout << "a: " << a << endl;  // "a: 6"
  cout << "b: " << b << endl;  // "b: 6"
}

このように、渡した変数が参照引数の参照先になるような呼び出し方を参照渡しといいます。


参照渡しの利点

参照渡しが便利な例を紹介します。

関数の結果を複数返したい

通常は関数の結果を返り値を使って呼び出し元に返します。 返り値は1つしか返すことができませんが、関数によっては結果を複数返したい場合があります。
このような場合の1つの実現方法として配列に結果を入れて返すという方法があります。

別の方法として、参照の引数を用いて結果を返す方法があります。

#include <bits/stdc++.h>
using namespace std;

// a,b,cの最大値、最小値をそれぞれminimumの参照先、maximumの参照先に代入する
void min_and_max(int a, int b, int c, int &minimum, int &maximum) {
  minimum = min(a, min(b, c));  // 最小値をminimumの参照先に代入
  maximum = max(a, max(b, c));  // 最大値をmaximumの参照先に代入
}

int main() {
  int minimum, maximum;
  min_and_max(3, 1, 5, minimum, maximum);  // minimum, maximumを参照渡し
  cout << "minimum: " <<  minimum << endl;  // 最小値
  cout << "maximum: " <<  maximum << endl;  // 最大値
}
実行結果
minimum: 1
maximum: 5

無駄なコピーを減らす

関数に引数を渡す場合、無駄なコピーが発生することがあります。

#include <bits/stdc++.h>
using namespace std;

// 配列の先頭100要素の値の合計を計算する
int sum100(vector<int> a) {
  int result = 0;
  for (int i = 0; i < 100; i++) {
    result += a.at(i);
  }
  return result;
}

int main() {
  vector<int> vec(10000000, 1);  // すべての要素が1の配列

  // sum100 を500回呼び出す
  for (int i = 0; i < 500; i++) {
    cout << sum100(vec) << endl;  // 配列のコピーが生じる
  }
}
実行結果
100
100
100
100
100
(省略)
...
実行時間

7813 ms
※一例なので異なる結果になることがあります。

上のプログラムでは関数sum100を呼び出すたびに配列の要素がコピーされるので、1000万要素の配列のコピーが500回生じています。
この実行結果では全体で7秒以上の時間がかかっていて、配列を1回コピーするのに約15ミリ秒(0.015秒)程かかっていることになります。

ここで、引数のvector<int> avector<int> &aとして参照渡しするように変更します。

#include <bits/stdc++.h>
using namespace std;

// 配列の先頭100要素の値の合計を計算する (参照渡し)
int sum100(vector<int> &a) {
  int result = 0;
  for (int i = 0; i < 100; i++) {
    result += a.at(i);
  }
  return result;
}

int main() {
  vector<int> vec(10000000, 1);  // すべての要素が1の配列

  // sum100 を500回呼び出す
  for (int i = 0; i < 500; i++) {
    cout << sum100(vec) << endl;  // 参照渡しなので配列のコピーは生じない
  }
}
実行結果
100
100
100
100
100
(省略)
...
実行時間

15 ms
※一例なので異なる結果になることがあります。

こちらのプログラムでは、配列を参照渡ししているので呼び出し時に配列の要素がコピーされず、 実行時間が約15ミリ秒に収まっています。
値渡しを用いたsum100では1回あたりの呼び出しに約15ミリ秒かかっていたことを踏まえれば、 約500倍程高速に動作するようになったことになります。

これらのサンプルプログラムはあまりない意味の無い極端な例ですが、現実的なプログラムでも、 複数回呼び出される関数で配列の値渡しを行うと上の例のように実行時間が遅くなってしまうことがあります。
詳しくは2.06.計算量で扱いますが、プログラムを高速化したい場合には配列のコピーについて特に気をつける必要があります。 コピーが必要のない場合は参照渡しを用いるのが良いでしょう。


注意点

参照先の指定が必要

参照を宣言するときは基本的に参照先を指定して初期化する必要があります。
参照先が定まっていない参照を作ることはできないということに注意しましょう。

関数の引数に用いる参照は呼び出し時に自動的に参照先が決まります。

#include <bits/stdc++.h>
using namespace std;

int main() {
  int a = 0;
  int &b;  // コンパイルエラー
}
エラー出力
./Main.cpp: In function ‘int main()’:
./Main.cpp:6:8: error: ‘b’ declared as reference but not initialized
   int &b;
        ^

「bが参照として宣言されているのに初期化されていない」というエラーです。

参照先を変更することはできない

一度宣言した参照の参照先を後から変更することもできません。


細かい話

&の位置

参照の宣言を以下のように紹介しました。

参照先の型 &参照の名前 = 参照先;

実は、&の位置は参照の名前の直前である必要はなく、次のようにすることもできます。

参照先の型& 参照の名前 = 参照先;  // 型の直後
参照先の型 & 参照の名前 = 参照先;  // 中間

しかし、複数の参照を同時に宣言しようとした場合には注意が必要です。

int a = 123;
int& b = a, c = a;

例えば、上のようにした場合に、bcはどちらもaへの参照になるように見えてしまいますが、 実際にはcは参照ではなく、「aの値で初期化されたint型の変数」になります。(baの参照になります。)

cを参照にするには、cの前にも&をつける必要があります。

&の位置は参照の名前の直前でも型名の直後でもいいのですが、このような問題があるため、名前の直前に書くのが良いでしょう。

int a = 123;
int &b = a, &c = a;  // bとcはどちらもaへの参照

範囲for文での参照

2.02 ループの書き方では、範囲for文を用いて配列の要素の値を読み取る場合についてのみ紹介しました。

vector<int> a = {1, 3, 2, 5};
for (int x : a) {
  x = x * 2;
}
// aは{1, 3, 2, 5}のまま

例えば上のように書いても、x自体は2倍になるものの、対応する配列の要素は変更されません。

参照は範囲for文でも用いることができ、これによって配列の要素を書き換える処理を簡潔に書くことができます。

vector<int> a = {1, 3, 2, 5};
for (int &x : a) {
  x = x * 2;
}
// aは{2, 6, 4, 10}となる
参照を用いた範囲for文の書き方
for (配列の要素の型 &変数名 : 配列変数) {
  // 変数名 を使う(変数を経由して配列の要素を書き換え可能)
}

変数以外の参照

参照先を変数以外にすることもできます。

vector<int> v = {1, 2, 3};

// 以下の操作で参照先のvの要素が書き換わる
int &e = v.at(1);
e = -2;

cout << v.at(0) << ", " << v.at(1) << ", " << v.at(2) << endl;  // "1, -2, 3"が出力される

この例では、配列の2番目の要素を参照先とする参照を宣言しています。
ただし、この例のようにvectorの要素に対する参照を作る場合には注意が必要です。

vectorの要素への参照を作った後に、vectorに対して要素を追加したり要素を削除するような操作を行うと、参照先が無効になり、意図しない動作をすることがあります。
vectorの要素への参照を生成した後は元のvectorの要素数が変わるような操作を行わないように注意しましょう。

具体的には、要素への参照を生成した後に、たくさんの要素をpush_backで追加するようなケースです。
次のプログラムでは、たくさんの要素をpush_backした結果、参照が期待した動作をしていないことが分かります。

#include <bits/stdc++.h>
using namespace std;

int main() {
  vector<int> v = {1, 2, 3};
  int &e = v.at(1);
  // 大量のpush_backで要素数を大幅に増やす
  for (int i = 0; i < 1000; i++) {
    v.push_back(i + 4);
  }
  cout << "e: " << e << endl;  // "e: 2"とならないことがある
}
実行結果
e: 32734

※一例なので異なる結果になることがあります。

参照型の返り値

#include <bits/stdc++.h>
using namespace std;

// int型の参照を返す関数f
int &f() {
  int x = 12345;
  return x;  // xを参照として返そうとする
}

int main() {
  int &y = f();
  cout << y << endl;  // "12345"が出力される?
}
実行結果
(※何も出力されない)
終了コード: 139

※一例なので異なる結果になることがあります。

このようなプログラムは未定義動作(どのように動作するか決まっていない)であり、避けなければなりません。

1.08 変数のスコープでスコープについて説明したとおり、関数fの中の変数xは、関数fの中でのみ使用可能ですが、このプログラムでは変数xの参照を関数の外側に返しています。
参照を用いても、スコープの制約を超えて変数を使用することはできません。

3章で説明するグローバル変数への参照を返り値として返すことは可能です。

扱わなかった機能のリスト

このページで紹介しきれなかった機能のリストを次に示します。 より詳しく勉強したい人は調べてみてください。

  • 右辺値参照
  • など

問題

リンク先の問題を解いてください。