こんにちは。田原です。
C言語で挫折する要因の1つはポインタと言われています。そして、C++にはポインタに良く似て非なる「参照」があります。ポインタの仕組みを把握していれば参照の仕組みは簡単なのですが、使い方はハードです。そこで、今回は参照の基本、および、ポインタと比較しつつ、参照の使いどころについても解説致します。
C++の参照はポインタと同様に指定されたメモリを参照(ポイント)します。
また、変数を定義できる場所で、参照も定義可能です。すなわち、「グローバル変数やstaticなローカル変数」、「関数の仮引き数や通常のローカル変数」、「(構造体やクラスの)メンバ変数」、「関数の戻り値」を定義できるところで参照も定義できますし、その有効範囲もそれぞれの変数と同じです。
また、変数は「第6回目 左辺値・右辺値は演算子で決まる!!」で解説したように左辺値(容器)ですが、それと同様、参照も左辺値(容器)です。
参照は型名に&
(アンパサンド)を付けたものですので、下記のようにして定義します。
型名& 参照名;
ただし、参照は初期化時に参照先を設定します。その初期化方法は参照の種別により異なります。以下の通りです。具体的な書き方については後述します。
番号 | 参照種別 | 初期化方法 |
---|---|---|
① | 仮引数 | 関数呼び出し |
② | メンバ変数 | コンストラクタの初期化子リスト |
③ | グローバル変数 | 初期化付きの宣言文 |
④ | ローカル変数 | 初期化付きの宣言文 |
⑤ | 参照を返却する関数 | その関数のreturn文 |
基本型への参照の場合、元の変数と同じように書くだけです。ポインタのように*
(間接演算子)を付けるなどする必要はありません。
int aInt=2; // 参照先となるint型変数 int& aIntRef=aInt; // aIntRefはaIntを参照する aInt=789; // 参照先のローカル変数の内容を変更してみる std::cout << "aIntRef=" << aIntRef<< std::endl;
構造体やクラスの変数をポイント/参照している場合、ポインタはメンバ変数にアクセスする際->
(間接メンバ)を使います。しかし、参照では通常の変数と同じく.
(ドット演算子)を使います。
struct Hoge { int mFuga0; int mFuga1; int mFuga2; }; Hoge aHoge; // Hogeのインスタンス Hoge* aHogePtr=&aHoge; // aHogeへのポインタ Hoge& aHogeRef=aHoge; // aHogeの参照 aHoge.mFuga0 =123; // aHogeのメンバ変数mFuga0 aHogePtr->mFuga1=456; // aHogeのメンバ変数mFuga1 aHogeRef.mFuga2 =789; // aHogeのメンバ変数mFuga2
aHogeRefはaHogeの参照なのでaFooとaHogeRefは同じものを指します。
#include <iostream> struct Foo { int& mIntRef; // mIntRefはint型変数への参照 ・・・②定義 Foo(int& iIntRef) : // iIntRefはint型変数への参照仮引数 ・・・①定義 mIntRef(iIntRef) // iIntRefでmIntRefを初期化 ・・・②初期化 { } }; int main() { int aInt=0; // 参照先となるint型変数 Foo aFoo(aInt); // aIntでiIntRefを初期化 ・・・①初期化 aInt=123; // 参照先変数の内容を変更してみる std::cout << aFoo.mIntRef << std::endl; return 0; }
#include <iostream> int gInt=1; // 参照先となるint型グローバル変数 int& gIntRef=gInt; // gIntRefはgIntを参照する ・・・③定義と初期化 int main() { int aInt=2; // 参照先となるint型変数 int& aIntRef=aInt; // aIntRefはaIntを参照する ・・・④定義と初期化 gInt=456; // 参照先のグローバル変数の内容を変更してみる aInt=789; // 参照先のローカル変数の内容を変更してみる std::cout << "gIntRef=" << gIntRef<< std::endl; std::cout << "aIntRef=" << aIntRef<< std::endl; return 0; }
グローバル参照やローカル参照を使うことは比較的少ないです。新しく定義した参照をアクセスできる場所は、その参照先の変数も直接アクセスできるため、あまりメリットがないのです。
構造体やクラスの中で、他の構造体やクラスをアクセスしたいような時に良く使われます。
なお、あまり使うことはありませんが、ポインタと同様変数名(参照名)側に&を付けることもできますが、ポインタ同様あまりお勧めできません。
int aInt1=3; int aInt2=4; int &aIntRef1=aInt1, &aIntRef2=aInt2;
ポインタを返却する関数を書くことができますが、それと同様に参照を返却する関数も書けます。
下記サンプル・コードで、func()関数はグローバル変数gDataへの参照を返却しています。
return文にて「戻り値の参照」を「参照先gData」にて初期化して返却しています。
#include <iostream> static int gData=123; int& func() // 戻り値を参照として定義 ・・・⑤定義 { return gData; // 戻り値の参照をgDataで初期化 ・・・⑤初期化 } int main() { func()=456; std::cout << func() << std::endl; return 0; }
なお、この例ではグローバル変数の参照を返却していますが、もし、通常のローカル変数(staticでないローカル変数)の参照を返却すると、「第14回目 C++のポインタ+間違うとハマる5つの例」で解説したように、そのローカル変数は関数からのreturnと共に破棄されるので不正な領域への参照となってしまいます。
関数からの戻り値は、その関数を呼び出した文の終わりまで有効です。参照の場合も原則として同じです。
上記のmain()関数のreturn文の前に次のような文を入れてみました。
int aData=func(); // ① ++aData; // aDataは457、gDataは456のまま
①の文では、func()が戻したgDataへの参照はこの文の ;(セミコロン)まで生きてます。
その間に参照に保存されている値が取り出されてint型変数aDataへ代入されています。
その後、aDataをインクリメントしていますが、既にint型の別の変数へ代入されていますので、gDataの値は元のままです。
更に次の文を追加してみます。
int aData=func(); // ① ++aData; // aDataは457、gDataは456のまま int& aDataRef=func(); // ② --aDataRef; // gDataが455へ変化する
②の文では、func()が返却した参照を更に参照aDataRefの初期化に用いました。
その結果、func()が返却した参照は、aDataRefへ引き継がれたため寿命が伸びます。aDataRefが有効な間有効です。
#include <iostream> static int gData=123; int& func() { return gData; } int main() { func()=456; std::cout << "func() =" << func() << std::endl; int aData=func(); // ① ++aData; // aDataは457、gDataは456のまま std::cout << "\n"; std::cout << "gData =" << gData << std::endl; std::cout << "aData =" << aData << std::endl; int& aDataRef=func(); // ② --aDataRef; // gDataが455へ変化する std::cout << "\n"; std::cout << "gData =" << gData << std::endl; std::cout << "aDataRef=" << aDataRef<< std::endl; return 0; }
func() =456 gData =456 aData =457 gData =455 aDataRef=455
以上のように参照は通常の変数と同じように取り扱えるよう設計されています。
つまり、元の変数を別の名前でアクセスできるようにしたという効果もあるので、参照は変数の別名とも呼ばれます。
下記2点が最大の差です。
- 初期化時のみ参照先を指定でき、その後変更することができない。
- 参照そのものの値(参照先アドレス)が保存されているメモリのアドレスを獲得できない。
上記の1.変更できないと言う性質のため、下記のような特性もあります。
- 参照は初期化を省略することができません。
ポインタは初期化しないことも可能(不定値になる場合もある)ですが、参照は省略できません。初期化後、変更できないので初期化を省略した参照に存在意義がないからです。 -
ボインタの無効値はnullptrですが、参照には「無効値」は存在しません。
ポインタを無効値であるnullptrで初期化することも多いと思います。参照はそのような無効値は存在せず、無効な値で初期化しておくこともできません。なぜなら、後で意味のある値を設定することができないので、無効値で初期化した参照は無効なので、それに存在意義がないからです。
ポインタはポイント先を変更できますが、参照は参照先を変更できません。そもそも参照先を変更するための書き方が存在しません。
#include <iostream> int main() { int aInt0=123; int aInt1=456; int* aPointer=&aInt0; int& aReference=aInt0; // ポインタはポイント先の変更になる aPointer=&aInt1; std::cout << "aInt0=" << aInt0 << std::endl; // 参照は参照先の値の変更になる aReference=aInt1; std::cout << "aInt0=" << aInt0 << std::endl; return 0; }
ポインタはポインタ変数に割り当てられているメモリ・アドレスを獲得できますが、参照は参照先アドレスが記録されているメモリ・アドレスを獲得できません。そもそもそのための書き方が存在しません。
#include <iostream> int main() { int aData=100; int* aPointer=&aData; int& aReference=aData; // ポインタはポインタ変数のアトレスになる std::cout << "&aPointer =" << &aPointer<< std::endl; // 参照は参照先のアドレスになる std::cout << "&aData =" << &aData<< std::endl; std::cout << "&aReference=" << &aReference<< std::endl; return 0; }[toggle title=”CMakeLists.txt”]
project(reference) if(MSVC) set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /W4 /EHsc") else() set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wall -std=c++11") endif() add_executable(referencereference.cpp dump.h)[/toggle]
&aPointer =012FF6F8 &aData =012FF704 &aReference=012FF704
この時のメモリ・イメージは下図となります。
参照でできることは全てポインタを使って処理することが可能です。ならば、参照を使う必要はないとも言えますが、それは極論ですね。C++ででいることはC言語でもできますから、C++が不要という事にはなりません。
でも、参照っていつ使う?と言う疑問は残ると思います。
参照はポインタで特に間違いを犯しやすい機能を削り、間違いを犯しにくくしたものと考えて良いと思います。つまり、可能な時はできるだけ参照を使った方が間違いを犯しにくくなり、デバックが楽になるのです。
特に、ポインタはNULLポインタが許されますが、NULL参照は許されません。そして、普通に参照を変数で初期化する限りNULL参照を作ることはできませんので、NULL例外フリーなプログラムも不可能ではありません。
ただ、ポインタを変換する際にNULL参照を作ることができますので注意は必要です。
例えば、NULLポインタを参照へ変換するとは、形式的には下記のような手順となります。
int* aIntPtr=nullptr; int& aIntRef=*aIntPtr; // aIntRefはNULL参照 aIntRef=100; // 多くの処理系ではメモリ不正アクセスで異常終了
これを防ぐために参照変換マクロを作れます。ポインタがnullptrの時は参照に変換せず、呼び出したソースと行番号情報を表示する例外を投げると、デバッグが楽になります。(関数テンプレートと例外を使っていますので、解説は割愛します。悪しからず。)
例えば下記です。(例によって
私からの保証はありません
ので、使う時は自己責任でお願いします。)
#include <stdexcept> #include <sstream> template<typename tType> tType& makeReference(tType* iPointer, char const* file, unsigned line) { if (!iPointer) { std::stringstream ss; ss << "nullptr at " << file << "(" << line << ")"; throw std::invalid_argument(ss.str()); } return *iPointer; } #define MAKE_REFERENCE(dPointer) makeReference(dPointer, __FILE__, __LINE__)
なお、他にもC言語的なテクニックを使うことでNULL参照を作ることは可能です。(例えば、参照先が記録されていると思われる領域をmemset()で0クリアする等。)
ですが、そのような使い方はC++では非推奨ですし、処理系に依存しますので、使わないことを強くお勧めします。
C++はC言語と同様、関数へ実引数を渡す時は全て値渡しですので、原則として実引数が仮引数領域へコピーされます。
しかし、関数側ではその変数を読み出すだけで値を変更しないような場合も少なくありません。
C言語ではそのような時はポインタで渡していました。C++では参照で渡すとNULLが渡る可能性をなくすことができるので、安全です。
const参照の方がより望ましい
元の変数を変更する必要が無い場合は、const参照と呼ばれる仕組みで渡すと更に安全です。受け取った側が元の変数を変更できないように制限されますので。なお、const参照を含めconstは実に様々なところで使われる概念ですので、もうしばらく後でまとめて解説する予定です。
このような場合もC言語ならポインタで渡しますが、C++では3-1同様参照を使って渡すとNULLが渡る可能性をなくすことができるので、安全です。
#include <iostream> bool func(int& oInt) { oInt=123; return true; } int main() { int aInt=0; if (func(aInt)) { std::cout << "aInt=" << aInt << std::endl; } return 0; }
コンテナ型のクラス(std::vectorなど)では良く使われる手法です。まだ、クラスについて解説していないので、staticなローカル変数と通常の関数で説明します。
(クラスを使うともう少しスマートに記述できるようになります。もうしばらくの我慢です。)
#include <iostream> #include <stdexcept> const static unsigned ArraySize=10; // int型配列の指定要素を返却する int& IntArray(unsigned iIndex) { static int ArrayData[ArraySize]={}; // 指定位置が範囲外なら、エラー(例外)とする if (ArraySize <= iIndex) throw std::out_of_range("IntArray"); // 指定要素を返却する return ArrayData[iIndex]; } int main() { // int型配列に値を設定する for (unsigned i=0; i < ArraySize; ++i) { IntArray(i)=i*2; } // 設定した値を表示する for (unsigned i=0; i < ArraySize; ++i) { std::cout << "IntArray(" << i << ")=" << IntArray(i) << std::endl; } return 0; }
例外:
例外については後日解説しますが、ここではエラーを返却する1つの手段として捉えて下さい。従来のエラー処理のように関数から返ってくる度にエラー・チェックするのではなく、`try-catch`を使って「まとめて」エラーを受け取れる仕組みです。(たいへん便利なのですが、使い方がなかなか難しいです。)
さて、C言語ならやはりポインタを返却する場面ですね。ポインタの場合はやはりNULLチェックすることが推奨されますので下記のような使い方になると思います。
#include <iostream> #include <stdexcept> const static unsigned ArraySize=10; // int型配列の指定要素を返却する int* IntArray(unsigned iIndex) { static int ArrayData[ArraySize]={}; // 指定位置が範囲外なら、エラー(例外)とする if (ArraySize <= iIndex) throw std::out_of_range("IntArray"); // 指定要素へのポインタを返却する return &ArrayData[iIndex]; } int main() { // int型配列に値を設定する for (unsigned i=0; i < ArraySize; ++i) { *IntArray(i)=i*2; } // 設定した値を表示する for (unsigned i=0; i < ArraySize; ++i) { std::cout << "*IntArray(" << i << ")=" << *IntArray(i) << std::endl; } return 0; }
間違いではないのですが、ポインタのNULLチェックせずに使っているので、なかなか気持ち悪いと思います。
今回は参照だけなので、もう少し簡単に解説が終わると思っていたのですが、予想外に膨らみました。
もう少し先の予定ですが、const参照や右辺値参照と言う難物が控えていますので、それらの基本である「参照」を丁寧に解説したためです。C言語でポインタが活躍したように、C++でも「参照」はかなり有用です。少したいへんなのですが、頑張ってください。
次回は、数値や文字の内部表現について解説します。10進数や16進数とコンピュータ内部の数値表現の違いや、文字コードの解説を行う予定です。(量が多くなりすぎたらどちらか一方に絞るかも。)
なお、次回から日曜日の夜24:00に公開することに致します。では、また来週。