C++の最も強力かつ、多くの初学者がつまずくトピックの一つである「ポインタ」を学びます。ポインタを理解することで、C++がどのようにメモリを扱っているのか、その裏側を垣間見ることができます。他の言語では隠蔽されているメモリへの直接的なアクセスは、パフォーマンスが重要な場面で絶大な効果を発揮します。さあ、メモリを直接操作する感覚を掴んでいきましょう。
C++を深く理解する上で避けて通れないのがメモリの概念です。変数を宣言すると、コンピュータのメモリ上にその型に応じたサイズの領域が確保されます。
例えば、int x = 10; と書くと、
int型に必要なメモリサイズ(例: 4バイト)を判断します。xという名前が割り当てられ、値として10が格納(バイナリ形式で書き込み)されます。この「メモリ上のどこか」を示すのがメモリアドレス(単にアドレスとも)です。ちょうど、現実世界の家の住所のようなものだと考えてください。アドレスは、メモリ上の各バイトに割り振られた通し番号のようなもので、通常は16進数で表現されます。
変数名の前に&(アドレス演算子)を付けることで、その変数が格納されているメモリアドレスを知ることができます。
#include <iostream>
#include <string>
int main() {
int age = 30;
double pi = 3.14;
std::string name = "Alice";
std::cout << "変数 'age' の値: " << age << std::endl;
std::cout << "変数 'age' のメモリアドレス: " << &age << std::endl;
std::cout << std::endl;
std::cout << "変数 'pi' の値: " << pi << std::endl;
std::cout << "変数 'pi' のメモリアドレス: " << &pi << std::endl;
std::cout << std::endl;
std::cout << "変数 'name' の値: " << name << std::endl;
std::cout << "変数 'name' のメモリアドレス: " << &name << std::endl;
return 0;
}変数 'age' の値: 30 変数 'age' のメモリアドレス: 0x7ffee3b8c9ac 変数 'pi' の値: 3.14 変数 'pi' のメモリアドレス: 0x7ffee3b8c9a0 変数 'name' の値: Alice 変数 'name' のメモリアドレス: 0x7ffee3b8c990
ポインタとは、このメモリアドレスを格納するための専用の変数です。
変数の型に応じて、対応するポインタの型が存在します。例えば、int型の変数のアドレスを格納するなら int* 型、double型の変数のアドレスを格納するなら double* 型のポインタを使います。アスタリスク * がポインタ型であることを示します。
#include <iostream>
int main() {
// int型の変数のアドレスを格納するためのポインタ変数
int* p_number;
// double型の変数のアドレスを格納するためのポインタ変数
double* p_value;
// どの変数も指していないことを示す特別な値 nullptr
// ポインタを初期化する際は nullptr を使うのが安全です
int* p_safe = nullptr;
if (p_safe == nullptr) {
std::cout << "p_safe は何も指していません。" << std::endl;
}
return 0;
}p_safe は何も指していません。
ポインタ変数の宣言時に * を型の横に付けるか、変数名の横に付けるかは好みが分かれますが、意味は同じです (int* p; と int *p; は等価)。このチュートリアルでは int* p; のように型の側に付けます。
ポインタを操作するための2つの重要な演算子を学びましょう。
&): 変数の前に付けると、その変数のメモリアドレスを取得できます。*): ポインタ変数の前に付けると、そのポインタが指し示しているアドレスに格納されている値を取得(または変更)できます。「参照先の値」を取り出すイメージです。言葉だけだと少し分かりにくいので、コードで見てみましょう。
#include <iostream>
int main() {
int number = 42;
// int型のポインタ変数 p_number を宣言
int* p_number;
// アドレス取得演算子(&)を使って、変数numberのアドレスをp_numberに代入
p_number = &number;
std::cout << "変数 number の値: " << number << std::endl;
std::cout << "変数 number のアドレス: " << &number << std::endl;
std::cout << "ポインタ p_number の値 (格納しているアドレス): " << p_number << std::endl;
// 間接参照演算子(*)を使って、p_numberが指す先の値を取得
std::cout << "ポインタ p_number が指す先の値: " << *p_number << std::endl;
std::cout << std::endl << "--- ポインタ経由で値を変更 ---" << std::endl;
// ポインタ経由で、変数numberの値を100に変更
*p_number = 100;
std::cout << "変更後の変数 number の値: " << number << std::endl;
std::cout << "ポインタ p_number が指す先の値: " << *p_number << std::endl;
return 0;
}変数 number の値: 42 変数 number のアドレス: 0x7ffc... ポインタ p_number の値 (格納しているアドレス): 0x7ffc... ポインタ p_number が指す先の値: 42 --- ポインタ経由で値を変更 --- 変更後の変数 number の値: 100 ポインタ p_number が指す先の値: 100
実行結果のアドレス (0x7ffc...の部分) は、実行するたびに変わる可能性があります。
この例から、p_number = &number; によって p_number が number を指すようになり、*p_number を使うことで number の中身を読み書きできることが分かります。*p_number は number とほぼ同じように扱えるわけです。
これまでの変数は、プログラムの実行前にコンパイラが必要なメモリ領域(スタック領域)を確保していました。しかし、プログラム実行中に、必要な分だけメモリを確保したい場合があります。例えば、ユーザーの入力に応じて、可変長のデータを保存するようなケースです。
このように、プログラムの実行中に動的にメモリを確保する仕組みが用意されており、確保される領域はヒープ領域(またはフリーストア)と呼ばれます。
new: ヒープ領域から指定した型のメモリを確保し、その領域へのポインタを返します。delete: new で確保したメモリを解放します。new で確保したメモリは、自動的には解放されません。自分で責任を持って delete を使って解放する必要があります。これを怠ると、メモリリーク(確保したメモリが解放されずに残り続け、利用可能なメモリがどんどん減っていくバグ)の原因となります。
#include <iostream>
int main() {
// new を使ってヒープ領域にint一つ分のメモリを確保
// 確保された領域へのアドレスが p_int に格納される
int* p_int = new int;
// ポインタ経由で確保した領域に値を書き込む
*p_int = 2025;
std::cout << "確保したメモリが指す先の値: " << *p_int << std::endl;
std::cout << "格納されているアドレス: " << p_int << std::endl;
// 使い終わったメモリは必ず delete で解放する
delete p_int;
// 解放後のポインタはどこを指しているか分からない危険な状態(ダングリングポインタ)
// なので、nullptr を代入して安全な状態にしておくのが良い習慣
p_int = nullptr;
return 0;
}確保したメモリが指す先の値: 2025 格納されているアドレス: 0x55a1...
new と delete は必ずペアで使います。図書館で本を借りたら(new)、必ず返却カウンターに返す(delete)のと同じです。返さなければ、他の人がその本を借りられなくなってしまいますよね。
C++では、Cスタイルの配列とポインタは非常に密接な関係にあります。実は、配列名はその配列の先頭要素を指すポインタとして扱うことができます。
#include <iostream>
int main() {
int numbers[] = {10, 20, 30, 40, 50};
// 配列名は先頭要素へのポインタとして扱える
int* p_numbers = numbers;
std::cout << "numbers[0] の値: " << numbers[0] << std::endl;
std::cout << "p_numbers が指す先の値: " << *p_numbers << std::endl;
// ポインタの指すアドレスを1つ進める(次の要素を指す)
p_numbers++;
std::cout << "ポインタをインクリメントした後、p_numbers が指す先の値: " << *p_numbers << std::endl;
std::cout << "これは numbers[1] と同じ: " << numbers[1] << std::endl;
// ポインタとオフセットで配列要素にアクセス
// *(numbers + 2) は numbers[2] と等価
std::cout << "*(numbers + 2) の値: " << *(numbers + 2) << std::endl;
return 0;
}numbers[0] の値: 10 p_numbers が指す先の値: 10 ポインタをインクリメントした後、p_numbers が指す先の値: 20 これは numbers[1] と同じ: 20 *(numbers + 2) の値: 30
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 の値が変更されたことを確認するプログラムを書いてください。
#include <iostream>
int main() {
}3.14159 2.71828
2つの整数を足し算する関数 add を作成し、その結果をポインタを使って返すプログラムを作成してください。
int* add(int a, int b) というシグネチャの関数を定義します。new を使って int 型のメモリを動的に確保します。a + b の計算結果を格納します。main 関数でこの add 関数を呼び出し、結果を受け取るポインタ変数を宣言します。main 関数内で delete を使って、add 関数内で確保されたメモリを解放するのを忘れないでください。#include <iostream>
int* add(int a, int b) {
}
int main() {
}7