第4章: ポインタと動的メモリ

C++の最も強力かつ、多くの初学者がつまずくトピックの一つである「ポインタ」を学びます。ポインタを理解することで、C++がどのようにメモリを扱っているのか、その裏側を垣間見ることができます。他の言語では隠蔽されているメモリへの直接的なアクセスは、パフォーマンスが重要な場面で絶大な効果を発揮します。さあ、メモリを直接操作する感覚を掴んでいきましょう。

変数とメモリ

C++を深く理解する上で避けて通れないのがメモリの概念です。変数を宣言すると、コンピュータのメモリ上にその型に応じたサイズの領域が確保されます。

例えば、int x = 10; と書くと、

  1. コンパイラはint型に必要なメモリサイズ(例: 4バイト)を判断します。
  2. プログラム実行時、メモリ上のどこかに4バイトの領域が確保されます。
  3. その領域にxという名前が割り当てられ、値として10が格納(バイナリ形式で書き込み)されます。

この「メモリ上のどこか」を示すのがメモリアドレス(単にアドレスとも)です。ちょうど、現実世界の家の住所のようなものだと考えてください。アドレスは、メモリ上の各バイトに割り振られた通し番号のようなもので、通常は16進数で表現されます。

変数名の前に&(アドレス演算子)を付けることで、その変数が格納されているメモリアドレスを知ることができます。

memory_address.cpp

ポインタの正体: アドレスを格納する変数

ポインタとは、このメモリアドレスを格納するための専用の変数です。

変数の型に応じて、対応するポインタの型が存在します。例えば、int型の変数のアドレスを格納するなら int* 型、double型の変数のアドレスを格納するなら double* 型のポインタを使います。アスタリスク * がポインタ型であることを示します。

pointer_declaration.cpp

ポインタ変数の宣言時に * を型の横に付けるか、変数名の横に付けるかは好みが分かれますが、意味は同じです (int* p;int *p; は等価)。このチュートリアルでは int* p; のように型の側に付けます。

ポインタの操作: 間接参照 (`*`) とアドレス取得 (`&`)

ポインタを操作するための2つの重要な演算子を学びましょう。

  • アドレス取得演算子 (&): 変数の前に付けると、その変数のメモリアドレスを取得できます。
  • 間接参照演算子 (*): ポインタ変数の前に付けると、そのポインタが指し示しているアドレスに格納されている値を取得(または変更)できます。「参照先の値」を取り出すイメージです。

言葉だけだと少し分かりにくいので、コードで見てみましょう。

pointer_operations.cpp

実行結果のアドレス (0x7ffc...の部分) は、実行するたびに変わる可能性があります。

この例から、p_number = &number; によって p_numbernumber を指すようになり、*p_number を使うことで number の中身を読み書きできることが分かります。*p_numbernumber とほぼ同じように扱えるわけです。

動的なメモリ確保: `new` と `delete`

これまでの変数は、プログラムの実行前にコンパイラが必要なメモリ領域(スタック領域)を確保していました。しかし、プログラム実行中に、必要な分だけメモリを確保したい場合があります。例えば、ユーザーの入力に応じて、可変長のデータを保存するようなケースです。

このように、プログラムの実行中に動的にメモリを確保する仕組みが用意されており、確保される領域はヒープ領域(またはフリーストア)と呼ばれます。

  • new: ヒープ領域から指定した型のメモリを確保し、その領域へのポインタを返します。
  • delete: new で確保したメモリを解放します。

new で確保したメモリは、自動的には解放されません。自分で責任を持って delete を使って解放する必要があります。これを怠ると、メモリリーク(確保したメモリが解放されずに残り続け、利用可能なメモリがどんどん減っていくバグ)の原因となります。

dynamic_memory.cpp

newdelete は必ずペアで使います。図書館で本を借りたら(new)、必ず返却カウンターに返す(delete)のと同じです。返さなければ、他の人がその本を借りられなくなってしまいますよね。

ポインタと配列: 切っても切れない関係性

C++では、Cスタイルの配列とポインタは非常に密接な関係にあります。実は、配列名はその配列の先頭要素を指すポインタとして扱うことができます。

pointer_and_array.cpp

p_numbers++ のようにポインタをインクリメントすると、ポインタは「次の要素」を指すようになります。int型ポインタなら4バイト(環境による)、double型ポインタなら8バイトといったように、指している型に応じて適切なバイト数だけアドレスが進みます。これをポインタ演算と呼びます。

この仕組みにより、ポインタを使って配列の要素を順に辿っていくことができます。ただし、配列の範囲外を指すポインタ(例えば、5つの要素を持つ配列の6番目を指すなど)を間接参照してしまうと、未定義の動作を引き起こし、プログラムがクラッシュする原因になるため、細心の注意が必要です。

この章のまとめ

  • ポインタは、変数のメモリアドレスを格納する特殊な変数です。
    • & 演算子で変数のアドレスを取得し、ポインタに代入します。
    • * 演算子でポインタが指す先の値にアクセス(読み書き)します。
    • new を使うと、プログラム実行中にヒープ領域から動的にメモリを確保できます。
    • new で確保したメモリは、使い終わったら必ず delete で解放しなければならず、怠るとメモリリークになります。
    • 配列名は、その配列の先頭要素を指すポインタとして扱うことができます。

ポインタはC++の強力な機能ですが、使い方を誤ると厄介なバグの原因にもなります。しかし、この仕組みを理解することは、C++をより深く知る上で不可欠です。後の章で学ぶスマートポインタなど、現代のC++ではポインタをより安全に扱うための機能も提供されています。

練習問題1

double 型の変数 pi3.14159 で初期化してください。次に、double* 型のポインタ p_pi を宣言し、pi のアドレスを代入してください。最後に、ポインタ p_pi を使って pi の値を 2.71828 に変更し、変数 pi の値が変更されたことを確認するプログラムを書いてください。

practice4_1.cpp

練習問題2

2つの整数を足し算する関数 add を作成し、その結果をポインタを使って返すプログラムを作成してください。

  1. int* add(int a, int b) というシグネチャの関数を定義します。
  2. 関数内で new を使って int 型のメモリを動的に確保します。
  3. その確保したメモリに a + b の計算結果を格納します。
  4. 確保したメモリへのポインタを返します。
  5. main 関数でこの add 関数を呼び出し、結果を受け取るポインタ変数を宣言します。
  6. ポインタを間接参照して計算結果を出力します。
  7. 最後に、main 関数内で delete を使って、add 関数内で確保されたメモリを解放するのを忘れないでください。
practice4_2.cpp