他の多くの言語と同じように、C++にも_ポインタ_があります。 _参照_と_ポインタ_が似ていることはもう知っていると思いますが、ポインタはコンピューターの内部の仕組みに、より近い階層のものだと考えてください。 _ポインタ_は、オブジェクトのアドレスを保持する変数です。 ポインタを使うとオブジェクトを直接操作でき、C++での動的なメモリ確保や効率的なデータ操作が可能になります。
_ポインタ_に初めて触れる人には少し不思議に感じられるかもしれませんが、慣れてしまえばとても素直な仕組みです。
C++の重要な要素なので、時間をかけてしっかり理解しましょう。 この概念で扱う骨組みだけの基本形は、_ダムポインタ_や_生のポインタ_とも呼ばれます。 現代のC++には_スマートポインタ_もありますが、基本の型は少しも賢くなく、すべての作業を手動で行う必要があります。
詳細に入る前に、_ポインタ_の用途を理解しておく価値があります。 _ポインタ_は、オブジェクトのアドレスをプログラムの他の部分と共有する方法です。これが特に役立つ理由は2つあります:
C++でポインタを宣言するときは、ポインタが指すデータ型を指定し、そのあとにアスタリスク(*)とポインタ名を書きます。
ポインタは宣言されただけでは自動的に初期化されません。
明示的に代入しない場合、ポインタは通常、不定の値、いわゆる「ゴミのアドレス」を保持します。
一部のコンパイラーはポインタをnullptrに初期化するかもしれませんが、その動作がすべてのコンパイラーで保証されているわけではないので、当てにしてはいけません。
思わぬ問題を避けるためにも、生のポインタは明示的に初期化し、使う前にヌルでないことを確認するのがよい習慣です。
int* ptr{nullptr}; // Declares a pointer and makes sure it is not invalid
変数のアドレスをポインタに代入するには、アドレス演算子(&)を使います。
ポインタの逆参照は、間接参照演算子(*)を使って行います。
std::string opponent{"Solomon Lane"};
// 'ethan' points to the address of the string opponent
std::string* ethan{&opponent};
// Instead of ethan's, the opponent's name address is given to the passPort
std::string passportName{*ethan};
注意:逆参照は明示的に行う必要がありますが、_参照_はエイリアスのようにそのまま使えました。
_ポインタ演算_を使うと、ポインタに対して算術演算を行えます。これは配列を扱うときに特に役立ちます。 ポインタに整数を足すと、別の要素を指すようになります。
// Stargate Coordinate Code
int gateCode[] = {462, 753, 218, 611, 977};
// 'ptr' points to the first element of 'gateCode'
int* ptr{&gateCode[0]};
// Accesses the third Stargate address through pointer arithmetic
int dialedAddress{*(ptr + 2)};
// Chevron encoded! Dialing Stargate address:
openStarGate(dialedAddress);
C++のポインタ演算は、注意深く扱わないと簡単に__未定義動作__を引き起こします。 未定義動作は、予期しないプログラムの結果、クラッシュ、さらにはセキュリティ上の脆弱性として現れることがあります。 未定義動作がもたらした結果として有名なのが、1996年のアリアン5ロケットの爆発です。これは、64ビットの浮動小数点数を16ビットの符号付き整数に変換したことが原因のソフトウェア例外により、壊滅的な失敗に至った事例です。
C++では、->演算子を使うと、オブジェクトへのポインタを通してそのオブジェクトのメンバーにアクセスできます。
これは、ポインタが指すオブジェクトのメンバーにアクセスするための簡略記法です。
たとえば、ptrがメンバー変数xを持つオブジェクトへのポインタだとすると、(*ptr).xと書く代わりにptr->xと直接書けます。
この演算子は、オブジェクトへのポインタを扱うときのコードの読みやすさを高め、記述を簡潔にします。
簡単な例を見てみましょう。メンバー変数superpowerを持つ_struct_のSuperheroを考えます。
main関数は、Superheroオブジェクト(ワンダーウーマンを表します)へのポインタdianaPrinceを作成します。
->演算子を使ってメンバー変数superpowerにアクセスし、ワンダーウーマンの象徴である「真実の投げ縄」を表しています。
struct Superhero {
std::string superpower;
};
Superhero wonder_woman{};
Superhero* dianaPrince = &wonder_woman;
dianaPrince->superpower = "Lasso of Truth";
// Using the -> operator to access member variable superpower:
std::cout << "Wonder Woman, possesses the mighty " << dianaPrince->superpower;
// Memory cleanup:
delete dianaPrince;
ポインタと参照はどちらもオブジェクトへの間接的なアクセスを可能にしますが、できることと安全性の面で違いがあります。 ポインタは、指す対象を変更できる柔軟さがあり、ヌルを代入できます。 しかしこの柔軟さは、ヌルポインタの逆参照やダングリングポインタの生成といったリスクを伴います。 一方、参照はヌルになれず、作成時に有効なオブジェクトに束縛されるため、こうしたリスクを避けられます。 より安全なので、ポインタが提供する追加の機能が必要でない限り、ポインタよりも参照を優先すべきです。
C++では、newを使って実行時にヒープ上にオブジェクト用のメモリを動的に確保します。
newは確保したメモリのアドレスへのポインタを返し、オブジェクトを特定のスコープに縛られない寿命にできます。
ただし、メモリを正しく解放するために、newとdeleteを必ず対にすることが大切です。
動的メモリ確保は、メモリリークや解放済みメモリへのアクセスといったよくある問題を防ぐため、慎重に使うべきです。
ポインタには、_ヌルポインタ_と呼ばれる特別な値を設定することもできます。これは有効なアドレスを指していないことを表します。 ヌルポインタは、ポインタに有効な値を代入する前の初期化によく使われます。
// There is a dark mark bearer, but who is it?
std::string* darkMarkBearer{nullptr};
// It is Bellatrix!
darkMarkBearer = new std::string{"Bellatrix Lestrange"};
// She has been defeated
delete darkMarkBearer;
// Reset pointer to null, symbolizing there is no more darkMarkBearer at the moment
darkMarkBearer = nullptr;
ポインタは、有効なオブジェクトのアドレスを保持しているか、nullptrに設定されているかのどちらかであるべきです。
そうすれば、ポインタがヌルかどうかだけを確認すればよく、ヌルでないポインタは有効だとみなせます。
オブジェクトが破棄されると、そのオブジェクトを指していたポインタは_ダングリング_状態のまま残ります(自動的にnullptrに設定されることはありません)。
こうしたケースを見つけて、そのポインタをnullptrに設定しておく必要があります。
古いコードでは、nullptrに代わる2つの書き方に出会うことがあります。
1つ目は、リテラル0です。これはポインタに対して特別にヌル値として解釈されますが、整数リテラルをポインタに代入できるのはこの場面だけです。
2つ目は、Cから受け継がれ、<cstddef>ヘッダーで定義されているpreprocessor macroのNULLです。これもヌルポインタの表し方の1つですが、現代のC++コードではあまり使われません。