Menu

C++ のポインタ:アドレス演算子・参照外し・nullptr

C++ のポインタをゼロから解説:ポインタの宣言、& (アドレス演算子)と * (参照外し演算子)、nullptr、配列へのポインタ、そしてクラッシュの原因になるダングリングポインタや未初期化ポインタの落とし穴。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

アドレスを保持する変数

すべての変数はメモリのどこか、アドレスと呼ばれる番号付きの場所に存在します。たいていの場合、それがどこなのかを気にする必要はなく、変数名を使うだけです。ポインタはこれを逆転させます。値そのものがアドレスである変数なのです。42 を保持する代わりに、「42 が格納されている場所」を保持します。

この間接参照こそがポインタを強力にしています。関数はポインタを通じて呼び出し元の変数を変更でき、連結リストのようなデータ構造はポインタでノードどうしをつなぎ、(動的メモリ で見るように)実行時に確保したメモリへ到達するのもポインタによってです。

&score&アドレス演算子で、score の場所を生み出します。*p*参照外し演算子で、アドレスをたどってそこに存在する値へ戻ります。

2つの演算子:& と *

初心者にとって最も紛らわしいのは、* が置かれる場所によって2つの異なる意味を持つことです。これを混同しないようにしましょう。

int* p;     // 宣言:「p は int へのポインタ」
p = &x;     // & = アドレス演算子:x のアドレスを p に格納する
int y = *p; // * = 参照外し:p が指す値を読む
*p = 99;    // 左辺での参照外し:ポインタを通して書き込む

宣言では * は型の一部です。式では * は仕事をします。いったんポインタが設定されれば、それを参照外しすることで元の変数への読み書きの完全なアクセスが得られます。

1行目以降、health を名前で一度も触っていないのに、その値が変わり続けたことに注目してください。これこそが要点です。hp は同じ記憶領域へのエイリアスなのです。空白の入れ方(int* pint *pint*p)は見た目だけの違いで、コンパイラにとっては同一です。本ガイドでは int* p を使います。

nullptr:何も指さない

どこも指していないポインタは nullptr(C++11)に設定すべきです。「まだ対象がない」と明確に、かつ型安全に表す方法であり、参照外しの前に検査できる対象を与えてくれます。

古い NULL マクロや裸の 0 よりも nullptr を優先しましょう。nullptr は本物のポインタ型を持つため、オーバーロード解決の際に整数の 0 と誤読されることが決してありません。これは古いスタイルが引き起こしうる微妙なバグです。

落とし穴:ヌル参照外し。 ヌル(または未初期化)のポインタを通して読み書きするのは未定義動作で、たいていは即座のクラッシュになります。

int* p = nullptr;
cout << *p;   // クラッシュ - null の参照外しは未定義動作

null になりうるものを参照外しする前には、必ず if (p)(または if (p != nullptr))でガードしてください。

ポインタと配列

配列名は先頭要素へのポインタへと減衰(decay)するため、ポインタと配列は深く結びついています。ポインタに 1 を足してもバイト単位で1進むのではなく、1要素分進みます。これがポインタ演算を成り立たせています。

p[i]*(p + i) は文字どおり同じ式です。この同値性こそが、配列が0始まりのインデックスである理由です。ここでの典型的なバグは末尾を越えることです。nums + 4 は比較に使える有効な「末尾の1つ先」のマーカーですが、*(nums + 4) を参照外しすると範囲外を読みます。ポインタにまつわる off-by-one(1個ずれ)のエラーはクラッシュや静かなデータ破壊の主要因なので、停止条件は意図的に注意して書きましょう。

const とポインタ

constポインタが指す先に、ポインタ自身に、あるいはその両方に適用できます。宣言を解読するには右から左へ読みましょう。

const int* p;        // const int へのポインタ  - *p は変更不可、p は付け替え可
int* const p = &x;   // int への const ポインタ  - *p は変更可、p は付け替え不可
const int* const p = &x; // 両方とも固定

これは実際のコードで絶えず重要になります。データを変更しないと約束する関数は、const へのポインタを受け取ります。

指す先を const で印付けすることは意図を記録し、コンパイラに誤った書き込みを止めさせます。実行時コストゼロで得られる無償の安全性です。

最大の罠:ダングリングポインタ

ダングリングポインタは、もはや期待した値を保持していないメモリを指すポインタです。変数がスコープを抜けたか、メモリが解放されたのです。これを参照外しするのは未定義動作であり、厄介なのは、壊れるまではしばしば動いているように見えることです。

int* makeBad() {
    int local = 5;
    return &local;   // バグ:関数が return すると local は消滅する
}                    // 返されたポインタは今やダングリング状態

アドレスは依然として有効な数値ですが、回収されたスタックの領域を指しています。それを読むとゴミが返るかクラッシュします。delete 済みのヒープオブジェクトへのポインタや、後で再確保される vector の要素へのポインタを保持し続けた場合も同じことが起こります。

3つのルールで身を守れます。

  • ローカル変数のアドレスを決して返さないこと。値で返すか、呼び出し元に記憶領域を所有させること。
  • ポインタが指していたものが消えたら nullptr に設定し、使う前に確認すること。
  • 所有権と寿命の管理には、裸の new/delete ではなく スマートポインタ に頼ること。自動的にメモリを解放し、この種のバグ全体を縮小してくれます。

次へ:参照とポインタ

別の変数を間接的に参照する方法はポインタだけではありません。C++ には 参照 もあり、似た感覚ですが null になれず、付け替えもできず、より簡潔な構文を使います。次は 参照とポインタ で両者を並べて比べ、どちらの道具を選ぶべきか――そしてなぜ現代の C++ の多くが、使える場面では参照を好むのか――をはっきりさせます。

よくある質問

C++ のポインタとは何ですか?

ポインタとは、別の値そのものではなく、その値のメモリアドレスを格納する変数です。* を使って宣言し(例:int* p)、& 演算子でアドレスを取得し(p = &x)、*p参照外しすることで、指している先の値を読み書きします。

C++ のポインタにおける & と * の違いは何ですか?

ポインタの文脈では、&アドレス演算子で、&xx のアドレスを返します。* は2つの役割を持ちます。宣言int* p)ではその変数をポインタとして印付けし、*p)ではポインタを参照外しして、そのアドレスに格納された値に到達します。

C++ の nullptr とは何ですか。なぜ NULL の代わりに使うのですか?

nullptr は C++11 で追加された型安全なヌルポインタリテラルです。「何も指していない」という意味です。古い NULL や裸の 0 より nullptr を優先しましょう。nullptr は本物のポインタ型なので、オーバーロード解決の際に整数と取り違えられることが決してないからです。参照外しの前には必ず if (p) で確認してください。ヌルポインタの参照外しは未定義動作です。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める