ポインターはオブジェクトを参照するための機能だ。この点ではリファレンスと同じ機能を提供している。
リファレンスを覚えているだろうか。T
型へのリファレンスはT
型のオブジェクトそのものではなく、T
型のオブジェクトへの参照だ。リファレンスへの操作は、参照したオブジェクトへの操作になる。
int main()
{
// int型のオブジェクト
int object = 0 ;
// オブジェクトを変更
object = 123 ;
// 123
std::cout << object ;
// T型へのリファレンス
// objectを参照する
int & reference = object ;
// objectが変更される
reference = 456 ;
// 456
std::cout << object ;
// referenceはobjectを参照している
object = 789 ;
// 参照するobjectの値
// 789
std::cout << reference ;
}
リファレンスは宣言と同時に初期化する。リファレンスの参照先をあとから変えることはできない。
int main()
{
int x = 0 ;
// rはxを参照する
int & r = x ;
int y = 1 ;
// xに1が代入される
r = y ;
}
最後のr = y ;
はリファレンスr
の参照先をy
に変えるという意味ではない。リファレンスr
の参照先にy
の値を代入するという意味だ。
ポインターはリファレンスに似ている。並べてみるとほとんど同じ意味だ。
T
型へのリファレンスはT
型のオブジェクトを参照するT
型へのポインターはT
型のオブジェクトを参照する
T
型へのリファレンス型がT &
であるのに対し、T
型へのポインター型はT *
だ。
// intへのリファレンス型
using ref_type = int & ;
// intへのポインター型
using ptr_type = int * ;
リファレンスの初期化は、単に参照したい変数名をそのまま書けばよかった。
int object { } ;
int & reference = object ;
ポインターの場合、参照したい変数名に、&
を付ける必要がある。
int object { } ;
int * pointer = &object ;
リファレンスを経由してリファレンスが参照するオブジェクトを操作するには、単にリファレンス名を使えばよかった。
// 書き込み
reference = 0
// 読み込み
int read = reference ;
ポインターの場合、ポインター名に*
を付ける必要がある。
// 書き込み
*pointer = 0 ;
// 読み込み
int read = *pointer ;
ポインター名をそのまま使った場合、それは参照先のオブジェクトの値ではなく、ポインターという値になる。
// オブジェクト
int object { } ;
// オブジェクトのポインター値で初期化
int * p1 = &object
// p1のポインター値で代入
// つまりobjectを参照する
int * p2 = p1 ;
このように比較すると、ポインターはリファレンスと同じ機能を提供していることがわかる。実際、リファレンスというのはポインターのシンタックスシュガーにすぎない。ポインターの機能を制限して、文法をわかりやすくしたものだ。
リファレンスがポインターの機能制限版だというのであれば、ポインターにあってリファレンスにはない機能は何だろうか。代入と、何も参照しない状態だ。
リファレンスは代入ができないが、ポインターは代入ができる。
int x { } ;
int y { } ;
int & reference = x ;
// xにyの値を代入
// リファレンスの参照先は変わらない
reference = y ;
int * pointer = &x ;
// pointerの参照先をyに変更
pointer = &y ;
リファレンスは必ず初期化しなければならない。
// エラー、初期化されていない
int & reference ;
そのため、リファレンスは常にオブジェクトを参照している。
ポインターは初期化しなくてもよい。
int * pointer ;
この場合、具体的に何かを参照していない状態になる。この場合にポインターの値はどうなるかはわからない。初期化のない整数の値がわからないのと同じだ。
// 値はわからない
int data ;
わからない値の整数を読んだ結果は未定義だ。書くことはできる。
int main()
{
// 値はわからない
int data ;
// 未定義
std::cout << data ;
// OK
data = 0 ;
}
このプログラムは未定義動作を含むので、プログラム全体がどのように実行されるかも未定義だ。
そしてここからがポインターの恐ろしいところだが、ポインターの場合にもこのわからない値は発生する。わからない値を持ったポインターの参照先への読み書きは未定義の挙動を引き起こす。
int main()
{
int * pointer ;
// 未定義の挙動
std::cout << *pointer ;
// 未定義の挙動
*pointer = 123 ;
}
なぜ未定義の挙動になるかというと、わからない値のポインターは、たまたまどこかの妥当なオブジェクトを参照してしまっているかもしれないからだ。
未定義の挙動は恐ろしい。未定義の挙動が発生した場合、何が起こっても文句は言えない。なぜならばその挙動は本来存在するはずがないのだから。上のプログラムはコンパイル時にエラーになるかもしれないし、実行時にエラーになるかもしれない。いや、もっとひどいことにはエラーにならないかもしれない。そして人生、宇宙、すべてのものの答えと、あろうことか答えに対する質問まで出力するかもしれない。
ポインターを未初期化にしていると、よくわからない値になってしまう。そのため、何も参照していないことを明示的に示すためのポインターの値、nullポインター値がある。nullptr
だ。
int * pointer = nullptr ;
nullptr
はどんな型へのポインターに対しても、何も参照していない値となる。
// doubleへのポインター
double * p1 = nullptr ;
// std::stringへのポインター
std::string * p2 = nullptr ;
C言語とC++では歴史的な理由で、nullptr
のほかにもNULL
もnullポインター値
int * pointer = NULL ;
C++ではさらに歴史的な理由で、0
もnullポインター値として扱う。
int * pointer = 0 ;
ただし、nullポインター値が実際に0である保証はない。ポインターの値についてはあとで詳しく扱う。
ポインターやリファレンスによって参照先が参照される時点では有効だったが、後に無効になる参照先を作ることができてしまう。
例えば以下のコードだ。
int * f()
{
// 寿命は関数
int variable {} ;
return &variable ;
}
int main()
{
int * ptr = f() ;
// エラー
int read = *ptr ;
}
このコードの問題は、関数f
の中の変数variable
の寿命は関数f
の中だけで、呼び出し元に戻ったときには寿命が尽きるというところにある。変数variable
へのポインターは変数variable
の寿命が尽きたあとも存在してしまうので、存在しないオブジェクトにポインター経由でアクセスしようとしてエラーになる。
同じ問題はリファレンスでも起きるが、ポインターの方がこの問題を起こしやすい。
int & f()
{
int variable {} ;
return variable ;
}