C++の最も強力かつ、多くの初学者がつまずくトピックの一つである「ポインタ」を学びます。ポインタを理解することで、C++がどのようにメモリを扱っているのか、その裏側を垣間見ることができます。他の言語では隠蔽されているメモリへの直接的なアクセスは、パフォーマンスが重要な場面で絶大な効果を発揮します。さあ、メモリを直接操作する感覚を掴んでいきましょう。
C++を深く理解する上で避けて通れないのがメモリの概念です。変数を宣言すると、コンピュータのメモリ上にその型に応じたサイズの領域が確保されます。
例えば、int x = 10;
と書くと、
int
型に必要なメモリサイズ(例: 4バイト)を判断します。x
という名前が割り当てられ、値として10
が格納(バイナリ形式で書き込み)されます。この「メモリ上のどこか」を示すのがメモリアドレス(単にアドレスとも)です。ちょうど、現実世界の家の住所のようなものだと考えてください。アドレスは、メモリ上の各バイトに割り振られた通し番号のようなもので、通常は16進数で表現されます。
変数名の前に&
(アドレス演算子)を付けることで、その変数が格納されているメモリアドレスを知ることができます。
ポインタとは、このメモリアドレスを格納するための専用の変数です。
変数の型に応じて、対応するポインタの型が存在します。例えば、int
型の変数のアドレスを格納するなら int*
型、double
型の変数のアドレスを格納するなら double*
型のポインタを使います。アスタリスク *
がポインタ型であることを示します。
ポインタ変数の宣言時に *
を型の横に付けるか、変数名の横に付けるかは好みが分かれますが、意味は同じです (int* p;
と int *p;
は等価)。このチュートリアルでは int* p;
のように型の側に付けます。
ポインタを操作するための2つの重要な演算子を学びましょう。
&
): 変数の前に付けると、その変数のメモリアドレスを取得できます。*
): ポインタ変数の前に付けると、そのポインタが指し示しているアドレスに格納されている値を取得(または変更)できます。「参照先の値」を取り出すイメージです。言葉だけだと少し分かりにくいので、コードで見てみましょう。
実行結果のアドレス (0x7ffc...
の部分) は、実行するたびに変わる可能性があります。
この例から、p_number = &number;
によって p_number
が number
を指すようになり、*p_number
を使うことで number
の中身を読み書きできることが分かります。*p_number
は number
とほぼ同じように扱えるわけです。
これまでの変数は、プログラムの実行前にコンパイラが必要なメモリ領域(スタック領域)を確保していました。しかし、プログラム実行中に、必要な分だけメモリを確保したい場合があります。例えば、ユーザーの入力に応じて、可変長のデータを保存するようなケースです。
このように、プログラムの実行中に動的にメモリを確保する仕組みが用意されており、確保される領域はヒープ領域(またはフリーストア)と呼ばれます。
new
: ヒープ領域から指定した型のメモリを確保し、その領域へのポインタを返します。delete
: new
で確保したメモリを解放します。new
で確保したメモリは、自動的には解放されません。自分で責任を持って delete
を使って解放する必要があります。これを怠ると、メモリリーク(確保したメモリが解放されずに残り続け、利用可能なメモリがどんどん減っていくバグ)の原因となります。
new
と delete
は必ずペアで使います。図書館で本を借りたら(new
)、必ず返却カウンターに返す(delete
)のと同じです。返さなければ、他の人がその本を借りられなくなってしまいますよね。
C++では、Cスタイルの配列とポインタは非常に密接な関係にあります。実は、配列名はその配列の先頭要素を指すポインタとして扱うことができます。
p_numbers++
のようにポインタをインクリメントすると、ポインタは「次の要素」を指すようになります。int
型ポインタなら4バイト(環境による)、double
型ポインタなら8バイトといったように、指している型に応じて適切なバイト数だけアドレスが進みます。これをポインタ演算と呼びます。
この仕組みにより、ポインタを使って配列の要素を順に辿っていくことができます。ただし、配列の範囲外を指すポインタ(例えば、5つの要素を持つ配列の6番目を指すなど)を間接参照してしまうと、未定義の動作を引き起こし、プログラムがクラッシュする原因になるため、細心の注意が必要です。
&
演算子で変数のアドレスを取得し、ポインタに代入します。*
演算子でポインタが指す先の値にアクセス(読み書き)します。new
を使うと、プログラム実行中にヒープ領域から動的にメモリを確保できます。new
で確保したメモリは、使い終わったら必ず delete
で解放しなければならず、怠るとメモリリークになります。ポインタはC++の強力な機能ですが、使い方を誤ると厄介なバグの原因にもなります。しかし、この仕組みを理解することは、C++をより深く知る上で不可欠です。後の章で学ぶスマートポインタなど、現代のC++ではポインタをより安全に扱うための機能も提供されています。
double
型の変数 pi
を 3.14159
で初期化してください。次に、double*
型のポインタ p_pi
を宣言し、pi
のアドレスを代入してください。最後に、ポインタ p_pi
を使って pi
の値を 2.71828
に変更し、変数 pi
の値が変更されたことを確認するプログラムを書いてください。
2つの整数を足し算する関数 add
を作成し、その結果をポインタを使って返すプログラムを作成してください。
int* add(int a, int b)
というシグネチャの関数を定義します。new
を使って int
型のメモリを動的に確保します。a + b
の計算結果を格納します。main
関数でこの add
関数を呼び出し、結果を受け取るポインタ変数を宣言します。main
関数内で delete
を使って、add
関数内で確保されたメモリを解放するのを忘れないでください。