C++のメモリ管理は、C++を理解するうえで非常に重要なテーマです。
C++では、プログラムが使うメモリをかなり細かく制御できます。
これは、C++が高速で柔軟なプログラムを書ける理由のひとつです。
一方で、メモリの扱いを間違えると、メモリリーク、二重解放、解放済みメモリへのアクセス、未定義動作などの深刻な問題が発生します。
現代C++では、メモリを手動で細かく管理するよりも、標準ライブラリやRAII、スマートポインタを活用して、安全にメモリを扱う考え方が主流です。
重要なのは、単に「メモリを確保する」「メモリを解放する」という操作ではありません。
より大切なのは、次の3つです。
- どのオブジェクトがメモリを所有しているのか
- そのオブジェクトはいつまで生きるのか
- いつ、誰が安全に解放するのか
C++のメモリ管理は、突き詰めると「所有権」と「寿命」を明確にする設計技術だと言えます。
C++で使われる主なメモリ領域
C++のメモリ管理を理解するには、まずプログラム中のデータがどこに置かれるのかを知る必要があります。
一般的には、C++のメモリは次のような領域に分けて説明されます。
スタック領域
スタック領域は、主に関数内のローカル変数や関数呼び出しの情報を管理するために使われます。
関数の中で作られたローカル変数は、その関数の実行が終わると自動的に破棄されます。
プログラマが明示的に解放する必要はありません。
スタック領域の特徴は、確保と解放が高速であることです。
スコープを抜けると自動的に破棄されるため、管理もしやすいです。
ただし、スタック領域にはサイズの上限があります。
非常に大きなデータをスタック上に置くと、スタックオーバーフローを起こす可能性があります。
ヒープ領域
ヒープ領域は、プログラムの実行中に動的にメモリを確保するための領域です。
実行時までサイズが分からないデータや、関数のスコープを超えて生存させたいオブジェクトなどは、ヒープ領域に置かれることがあります。
ヒープ領域は柔軟ですが、スタックよりも管理が複雑です。
メモリを確保したあとに適切に解放しなければ、メモリリークが発生します。
現代C++では、ヒープメモリを直接手動で管理するよりも、スマートポインタや標準コンテナに管理を任せるのが一般的です。
静的領域
静的領域には、グローバル変数や静的変数などが置かれます。
これらの変数は、プログラムの開始から終了まで存在します。
関数の呼び出しとは関係なく、長い寿命を持ちます。
静的領域の変数は便利な反面、依存関係が複雑になると初期化順序の問題や、状態管理の難しさにつながることがあります。
定数領域
定数領域には、文字列リテラルなど、基本的に変更されないデータが置かれます。
この領域のデータは読み取り専用として扱われることが多く、書き換えようとすると未定義動作になる場合があります。
厳密には「スタック」「ヒープ」はC++標準の中心用語ではない
C++の説明では、よく「スタック」「ヒープ」という言葉が使われます。
これは非常に分かりやすい説明ですが、厳密なC++標準の用語としては、より重要なのは「記憶域期間」です。
C++では、オブジェクトがどれくらいの期間存在するかを、記憶域期間という概念で考えます。
代表的なものには、次のようなものがあります。
自動記憶域期間
関数内のローカル変数などが持つ寿命です。
スコープに入ると作られ、スコープを抜けると自動的に破棄されます。
一般的な実装では、こうした変数はスタック上に置かれることが多いです。
動的記憶域期間
実行時に動的に確保されるオブジェクトが持つ寿命です。
一般的にはヒープ領域に確保されると説明されます。
動的に確保されたオブジェクトは、適切に解放されるまで存在します。
静的記憶域期間
グローバル変数や静的変数などが持つ寿命です。
プログラムの開始から終了まで存在します。
スレッド記憶域期間
スレッドごとに存在するオブジェクトが持つ寿命です。
マルチスレッドプログラムで使われることがあります。
C++のメモリ管理で最も重要なのは所有権
C++のメモリ管理では、「誰がそのオブジェクトを所有しているのか」を明確にすることが非常に重要です。
所有権とは、そのオブジェクトの寿命に責任を持つという意味です。
あるオブジェクトを作ったとき、そのオブジェクトを誰が管理するのか、いつ破棄するのかが曖昧だと、さまざまなバグが発生します。
所有権が曖昧だと起きる問題
所有権が曖昧なコードでは、次のような問題が起きやすくなります。
ひとつは、メモリリークです。確保したメモリを誰も解放しないことで、使われなくなったメモリが残り続けます。
もうひとつは、二重解放です。
複数の場所が同じメモリを「自分が解放すべきもの」と考えてしまい、同じメモリを複数回解放してしまう問題です。
さらに、解放済みメモリへのアクセスも起こります。
すでに破棄されたオブジェクトを、まだ有効だと思って使ってしまう状態です。
C++では、これらは未定義動作につながることがあります。
未定義動作とは、プログラムの動作が保証されない状態です。
クラッシュすることもあれば、たまたま動いているように見えることもあります。
RAIIとは何か
C++のメモリ管理を理解するうえで、RAIIは最重要の考え方です。
RAIIは「Resource Acquisition Is Initialization」の略です。
直訳すると、「リソースの取得は初期化である」という意味です。
少し分かりやすく言うと、メモリ、ファイル、ロック、ソケットなどのリソースを、オブジェクトの寿命に結びつけて管理する設計です。
RAIIの基本的な考え方
RAIIでは、リソースの取得をオブジェクトの生成時に行い、リソースの解放をオブジェクトの破棄時に行います。
つまり、オブジェクトが生きている間はリソースも有効であり、オブジェクトがスコープを抜けて破棄されると、リソースも自動的に解放されます。
この仕組みにより、プログラマが毎回手動で解放処理を書く必要が減ります。
RAIIのメリット
RAIIの大きなメリットは、例外が発生しても安全にリソースを解放できることです。
手動でメモリを解放するコードでは、途中で例外が発生すると、解放処理まで到達しない場合があります。
その結果、メモリリークが起こります。
しかし、RAIIを使っていれば、スコープを抜けるときに自動的にデストラクタが呼ばれるため、例外が発生してもリソースを解放できます。
この仕組みが、C++の安全なリソース管理の基礎です。
RAIIはメモリ以外にも使われる
RAIIはメモリ管理だけの考え方ではありません。
ファイルのオープンとクローズ、ミューテックスのロックとアンロック、ネットワーク接続、データベース接続、一時ファイル、GPUリソースなど、さまざまなリソース管理に使われます。
C++では、リソースを直接手動で管理するよりも、オブジェクトの寿命に結びつけて管理する方が安全です。
現代C++では手動のメモリ管理を避ける
昔のC++では、動的に確保したメモリを手動で解放するコードがよく書かれていました。
しかし、現代C++では、通常のアプリケーション開発において手動でメモリ管理を行う場面はかなり少なくなっています。
現在では、次のような標準ライブラリの機能を使うのが一般的です。
標準コンテナを使う
複数の要素を扱う場合は、手動で配列を確保するのではなく、標準コンテナを使います。
代表的なのは、動的配列として使える std::vector、固定長配列として使える std::array、文字列を扱う std::string などです。
これらの型は、自分自身で内部メモリを管理します。
プログラマが明示的にメモリを解放する必要はありません。
スマートポインタを使う
動的に確保されたオブジェクトの所有権を管理する場合は、スマートポインタを使います。
単独所有には std::unique_ptr、共有所有には std::shared_ptr、共有所有を観測するだけの場合には std::weak_ptr を使います。
スマートポインタは、オブジェクトの寿命に応じて自動的にメモリを解放してくれます。
値で持てるなら値で持つ
C++では、何でもポインタにする必要はありません。
むしろ、値として持てるものは値として持つのが自然です。
クラスのメンバとして直接オブジェクトを持てるなら、ポインタやスマートポインタを使わず、値として持つ方がシンプルで安全です。
スマートポインタの役割
スマートポインタは、C++のメモリ管理を安全にするための重要な仕組みです。
通常のポインタは、単にアドレスを持っているだけです。そのポインタがオブジェクトを所有しているのか、それとも一時的に参照しているだけなのかは、型からは分かりません。
一方、スマートポインタは所有権の意味を型で表現できます。
std::unique_ptr
std::unique_ptr は、あるオブジェクトをただひとつの所有者が持つことを表します。
所有者がひとつだけなので、寿命の管理が明確です。
所有者がスコープを抜けると、対象のオブジェクトも自動的に破棄されます。
std::unique_ptr はコピーできません。
これは、同じオブジェクトを複数の所有者が同時に所有してしまうことを防ぐためです。
所有権を別の場所へ移したい場合は、明示的に移動させます。
これにより、「ここで所有権が移る」という意図がコード上で分かりやすくなります。
std::shared_ptr
std::shared_ptr は、複数の所有者が同じオブジェクトを共有する場合に使います。
内部的には参照カウントを持ち、最後の所有者がいなくなったタイミングで対象オブジェクトが破棄されます。
便利な仕組みですが、乱用には注意が必要です。
std::shared_ptr を多用すると、誰が本当にそのオブジェクトの寿命を決めているのか分かりにくくなります。
基本的には、まず値で持てないかを考えます。
次に、動的所有が必要なら std::unique_ptr を検討します。
本当に複数の所有者が必要な場合に限って、std::shared_ptr を使うのがよい設計です。
std::weak_ptr
std::weak_ptr は、std::shared_ptr が管理しているオブジェクトを所有せずに参照するためのものです。
主な用途は、循環参照を避けることです。
たとえば、親オブジェクトが子オブジェクトを共有所有し、子オブジェクトも親オブジェクトを共有所有していると、お互いの参照カウントが0にならず、メモリが解放されないことがあります。
このような場合、片方を std::weak_ptr にすることで、所有関係の循環を断ち切ることができます。
生ポインタの扱い
現代C++では、生ポインタを所有権の管理に使うことは避けるべきです。
ただし、生ポインタ自体が悪いわけではありません。
問題は、生ポインタで所有権を表現しようとすることです。
生ポインタは所有しない参照として使う
現代C++の設計方針では、生ポインタは「所有しない参照」として使うのが望ましいです。
つまり、そのポインタは対象オブジェクトを破棄する責任を持たず、単に一時的に参照しているだけ、という意味で使います。
nullを許可したい場合にはポインタを使い、必ず存在する前提なら参照を使うのが一般的です。
参照との使い分け
参照は、基本的に有効なオブジェクトを指している前提で使います。
nullを表現できないため、「必ず存在するものを借りる」場合に向いています。
一方、生ポインタはnullを表現できます。
そのため、「存在しない可能性があるものを借りる」場合に向いています。
所有権を渡したい場合は、生ポインタではなく、std::unique_ptr などを使うべきです。
Rule of Three、Rule of Five、Rule of Zero
C++では、リソースを管理するクラスを作るときに重要な考え方があります。
それが、Rule of Three、Rule of Five、Rule of Zeroです。
Rule of Three
Rule of Threeとは、デストラクタ、コピーコンストラクタ、コピー代入演算子のうち、どれかひとつを自分で定義するなら、残りも考えるべきだというルールです。
たとえば、クラスが内部で動的メモリを所有している場合、単純なコピーではポインタの値だけがコピーされてしまうことがあります。
その結果、複数のオブジェクトが同じメモリを所有している状態になり、二重解放が発生する可能性があります。
このような問題を避けるため、リソースを所有するクラスでは、コピー時の動作と破棄時の動作を慎重に設計する必要があります。
Rule of Five
C++11以降では、ムーブセマンティクスが導入されました。
そのため、リソースを管理するクラスでは、デストラクタ、コピーコンストラクタ、コピー代入演算子に加えて、ムーブコンストラクタ、ムーブ代入演算子も考える必要があります。
これが Rule of Five です。
ムーブは、リソースの中身をコピーするのではなく、所有権を移す仕組みです。
大きなデータを扱う場合、コピーより効率的に処理できることがあります。
Rule of Zero
現代C++で最も推奨されるのは Rule of Zero です。
これは、デストラクタ、コピーコンストラクタ、コピー代入演算子、ムーブコンストラクタ、ムーブ代入演算子を、できるだけ自分で書かないという考え方です。
標準ライブラリのコンテナやスマートポインタにリソース管理を任せれば、自分で複雑なメモリ管理を書く必要がなくなります。
実務では、Rule of Zeroを目指すのが最も安全です。
メモリリーク
メモリリークとは、確保したメモリが不要になったにもかかわらず、解放されずに残り続ける状態です。
短時間で終了するプログラムでは目立たないこともありますが、長時間動作するサーバー、GUIアプリ、ゲーム、常駐プロセスなどでは深刻な問題になります。
プログラム終了時にはOSがプロセスのメモリを回収することが多いですが、実行中に使えないメモリが増え続けることが問題です。
メモリリークが起こる原因
メモリリークは、動的に確保したメモリを解放し忘れることで発生します。
特に、手動でメモリを管理しているコードでは、例外や早期リターンが原因で解放処理に到達しないことがあります。
メモリリークを防ぐ方法
メモリリークを防ぐには、RAIIを使うことが最も重要です。
標準コンテナやスマートポインタを使えば、スコープを抜けたときに自動的にメモリが解放されます。
そのため、通常のアプリケーション開発では、手動でメモリを解放するコードをできるだけ書かないようにするべきです。
ダングリングポインタ
ダングリングポインタとは、すでに破棄されたオブジェクトを指しているポインタのことです。
オブジェクトの寿命が終わったあとも、そのアドレスを持つポインタだけが残っている状態です。
ダングリングポインタが危険な理由
ダングリングポインタを使うと、すでに無効になったメモリにアクセスすることになります。
このようなアクセスは未定義動作です。
クラッシュすることもあれば、たまたま動いているように見えることもあります。
特に危険なのは、問題がすぐに表面化しない場合です。
バグが再現しにくくなり、原因調査が難しくなります。
防ぐ方法
ダングリングポインタを防ぐには、オブジェクトの寿命を明確にすることが重要です。
所有権を持つ場合はスマートポインタを使い、所有しない参照については、参照先の寿命が自分より長いことを保証する必要があります。
二重解放
二重解放とは、同じメモリを複数回解放してしまうことです。
これは非常に危険なバグです。
メモリ管理情報が壊れ、クラッシュやセキュリティ上の問題につながることがあります。
二重解放が起こる原因
二重解放は、複数のポインタが同じメモリを所有しているように扱われることで起こります。
特に、生ポインタを所有権の表現に使うと、誰が解放する責任を持っているのか分からなくなりやすいです。
防ぐ方法
二重解放を防ぐには、所有者をひとつに限定することが重要です。
単独所有なら std::unique_ptr を使い、共有所有が必要なら std::shared_ptr を使います。
ただし、共有所有は必要な場合に限るべきです。
便利だからという理由だけで使うと、所有関係が複雑になります。
未初期化メモ
C++では、初期化されていない変数を読むと未定義動作になることがあります。
特に、ローカル変数は自動的にゼロで初期化されるとは限りません。
未初期化の危険性
未初期化の変数には、以前そのメモリ領域に残っていた不定の値が入っている可能性があります。
その値を読み取ると、プログラムの動作が予測できなくなります。
防ぐ方法
変数は、宣言時に初期化する習慣を持つべきです。
ポインタを使う場合も、初期状態を明確にすることが重要です。
ただし、所有権を持つポインタであれば、生ポインタではなくスマートポインタを使う方が安全です。
配列とメモリ管理
C++で複数の要素を扱う場合、手動で配列を確保するよりも標準コンテナを使うのが基本です。
動的な配列にはstd::vectorを使う
要素数が実行時に決まる場合や、要素数を後から変更したい場合には、std::vector が適しています。
std::vector は内部のメモリを自動で管理します。
必要に応じて容量を増やし、不要になれば自動的に解放します。
ただし、非常に大きなデータを扱う場合は、メモリ不足になる可能性があります。
その場合、確保に失敗することがあります。
固定長配列にはstd::arrayを使う
要素数がコンパイル時に決まっている固定長配列には、std::array が使えます。
ただし、std::array は要素を直接内部に持つため、巨大な配列をローカル変数として作るとスタックを圧迫する可能性があります。
小〜中規模の固定長配列には std::array、サイズが大きいものや実行時にサイズが決まるものには std::vector が向いています。
std::vectorのメモリ管理
std::vector は、C++で非常によく使われる標準コンテナです。
内部的には連続したメモリ領域に要素を保持します。
そのため、キャッシュ効率が良く、高速にアクセスできることが多いです。
sizeとcapacity
std::vector には、現在の要素数を表すサイズと、再確保なしで保持できる容量があります。
要素を追加して容量を超えると、std::vector はより大きなメモリ領域を確保し、既存の要素を移動またはコピーします。
再確保による注意点
std::vector が再確保を行うと、以前の要素へのポインタ、参照、イテレータが無効になる場合があります。
そのため、std::vector の要素へのポインタを長期間保持する場合は注意が必要です。
大量に要素を追加することが分かっている場合は、あらかじめ容量を確保しておくことで、再確保の回数を減らせます。
malloc/freeとnew/deleteの違い
C++では、C言語由来の malloc や free も使えます。
しかし、C++のオブジェクトを扱う場合には、通常は使うべきではありません。
malloc/freeの特徴
malloc はメモリを確保するだけです。C++のコンストラクタは呼びません。
free はメモリを解放するだけです。C++のデストラクタは呼びません。
そのため、C++のクラスオブジェクトを malloc と free で扱うと、初期化や破棄が正しく行われない可能性があります。
new/deleteの特徴
new はメモリを確保し、オブジェクトのコンストラクタを呼びます。
delete はオブジェクトのデストラクタを呼び、その後メモリを解放します。
ただし、現代C++では、new と delete を直接使うよりも、スマートポインタや標準コンテナを使うのが基本です。
対応を混ぜてはいけない
new で確保したものを free で解放したり、malloc で確保したものを delete で解放したりしてはいけません。
これは未定義動作になります。
同様に、単体オブジェクト用の確保と配列用の確保も、正しく対応させる必要があります。
ガベージコレクションとの違い
C++には、JavaやC#のような標準的なガベージコレクションに頼るメモリ管理モデルはありません。
C++の中心的な考え方は、RAIIと所有権管理です。
C++は決定的に破棄できる
ガベージコレクションのある言語では、不要になったオブジェクトがいつ回収されるかはランタイムに任されることが多いです。
一方、C++では、スコープを抜けるタイミングでオブジェクトが破棄されます。
このように、破棄のタイミングが明確であることは、C++の大きな特徴です。
メモリ以外のリソース管理にも強い
C++のRAIIは、メモリだけでなくファイルやロックなどのリソース管理にも使えます。
スコープを抜けた時点で確実にリソースを解放できるため、リソースの解放タイミングを厳密に制御したい場面に向いています。
メモリ管理とパフォーマンス
C++では、メモリ管理がパフォーマンスに大きく影響します。
特に、頻繁な動的メモリ確保はコストになることがあります。
動的確保のコスト
ヒープへの動的メモリ確保は、一般にスタック上のローカル変数よりもコストが高くなることがあります。
動的確保では、アロケータを経由して空き領域を探したり、管理情報を更新したりする必要があるためです。
ただし、実際の性能は実装、アロケータ、コンパイラ最適化、アクセスパターンに依存します。
メモリ局所性
C++では、メモリ局所性も重要です。
連続したメモリにデータを配置すると、CPUキャッシュに乗りやすくなり、高速に処理できることがあります。
std::vector が高速な場面が多いのは、要素を連続したメモリに保持するためです。
一方、個別に動的確保されたオブジェクトがメモリ上に散らばっていると、キャッシュ効率が悪くなる場合があります。
constとメモリ安全性
const はメモリ管理そのものの機能ではありませんが、安全なコードを書くうえで重要です。
const を適切に使うと、意図しない変更を防ぐことができます。
読み取り専用の意図を表す
関数が引数を変更しない場合は、読み取り専用であることを型で表現できます。
これにより、関数を使う側にも「この関数はオブジェクトを変更しない」という意図が伝わります。
ポインタにおけるconstの注意点
ポインタに const を使う場合、何が変更不可なのかに注意が必要です。
指している先の値を変更できない場合、ポインタ自体を変更できない場合、その両方を変更できない場合があります。
const はC++の型安全性を高める重要な仕組みです。
メモリアライメントとパディング
C++では、型ごとに適切なメモリアライメントがあります。
アライメントとは、データをメモリ上の特定の境界に配置するためのルールです。
アライメントとは
多くのCPUでは、特定の型を特定の境界に配置した方が効率よくアクセスできます。
そのため、コンパイラは必要に応じてオブジェクトの配置を調整します。
パディングとは
構造体では、メンバの間に未使用の領域が挿入されることがあります。
これをパディングと呼びます。
パディングにより、構造体のサイズは単純なメンバサイズの合計より大きくなることがあります。
メンバの順序を変えることでサイズが小さくなる場合もありますが、可読性やABI互換性も考慮する必要があります。
placement newとカスタムアロケータ
C++には、かなり低レベルなメモリ管理機能もあります。
代表的なものが placement new とカスタムアロケータです。
placement new
placement new は、すでに確保済みのメモリ上にオブジェクトを構築するための仕組みです。
通常のアプリケーションコードで使うことは多くありません。
メモリプール、組み込み、ゲームエンジン、独自アロケータなど、メモリを細かく制御したい場面で使われます。
placement new を使った場合、通常の自動破棄や通常の解放処理とは異なる注意が必要です。
オブジェクトの寿命を明示的に管理しなければなりません。
カスタムアロケータ
カスタムアロケータは、標準コンテナなどのメモリ確保方法を差し替えるための仕組みです。
大量の小さなオブジェクトを効率よく管理したい場合や、リアルタイム性が求められる場合、独自のメモリ管理戦略を使いたい場合に利用されます。
ただし、一般的なアプリケーション開発では、まず標準コンテナとスマートポインタを正しく使うことの方が重要です。
デストラクタと例外安全性
C++では、デストラクタの設計も重要です。
デストラクタは、オブジェクトが破棄されるときに呼ばれます。
RAIIでは、リソース解放の中心的な役割を担います。
デストラクタで例外を外に出さない
デストラクタから例外を外に出すのは、原則として避けるべきです。
特に、別の例外が処理されている最中にデストラクタからさらに例外が出ると、プログラムが異常終了する可能性があります。
そのため、デストラクタは例外を投げない設計にするのが基本です。
例外安全な設計
C++では、例外が発生してもリソースリークや不整合が起こらないように設計する必要があります。
RAIIを使えば、例外が発生してもスコープを抜けるときに自動的にリソースが解放されるため、例外安全性を高めることができます。
メモリ管理のデバッグツール
C++のメモリ管理の問題は、ツールを使って検出できます。
手動で見つけるのが難しいバグも、専用ツールを使うことで発見しやすくなります。
AddressSanitizer
AddressSanitizerは、範囲外アクセス、解放済みメモリへのアクセス、スタックやヒープに関する不正なアクセスを検出するためのツールです。
C++のメモリバグを発見するうえで非常に有用です。
LeakSanitizer
LeakSanitizerは、メモリリークを検出するためのツールです。
環境によってはAddressSanitizerと組み合わせて使われることもあります。
UndefinedBehaviorSanitizer
UndefinedBehaviorSanitizerは、未定義動作につながる可能性のあるコードを検出するためのツールです。
整数オーバーフローや不正な型変換など、メモリ以外の問題も検出できます。
Valgrind
Valgrindは、メモリリークや不正なメモリアクセスを検出するためによく使われるツールです。
実行速度は遅くなりますが、詳細なメモリ診断に役立ちます。
実務での活用
実務では、これらのツールを開発中やCIに組み込むことがあります。
特にC++では、メモリ関連のバグが重大な障害につながりやすいため、ツールによる検出は非常に重要です。
実務でのメモリ管理の判断基準
C++でメモリ管理を考えるときは、最初から動的確保を考える必要はありません。
まずは、もっとシンプルな選択肢から検討するべきです。
まず値で持てないか考える
最もシンプルなのは、オブジェクトを値として持つことです。
値として持てば、寿命はスコープや所有オブジェクトに自然に従います。
余計なメモリ管理を考える必要がありません。
複数の要素には標準コンテナを使う
複数の要素を管理するなら、標準コンテナを使います。
動的な配列には std::vector、文字列には std::string、固定長の小さな配列には std::array が基本です。
単独所有ならstd::unique_ptr
動的確保が必要で、所有者がひとつだけなら std::unique_ptr を使います。
所有権が明確になり、スコープを抜けたときに自動で破棄されます。
共有所有が必要ならstd::shared_ptr
複数の場所が本当に同じオブジェクトを所有する必要がある場合に限り、std::shared_ptr を使います。
ただし、共有所有は設計を複雑にしやすいため、安易に使うべきではありません。
循環参照にはstd::weak_ptr
std::shared_ptr 同士が互いに所有し合うと、循環参照が発生することがあります。
そのような場合は、片方を std::weak_ptr にして所有関係を断ち切ります。
所有しないなら参照か生ポインタ
関数がオブジェクトを借りるだけなら、参照または生ポインタを使います。
必ず存在するなら参照、存在しない可能性があるならポインタ、という使い分けが基本です。
よくある誤解
C++のメモリ管理には、初心者が誤解しやすい点があります。
何でもnewすればよいわけではない
C++では、オブジェクトを作るたびに動的確保する必要はありません。
むしろ、値で持てるものは値で持つ方が安全で効率的です。
shared_ptrは万能ではない
std::shared_ptr は便利ですが、万能ではありません。
共有所有が必要ない場面で使うと、所有関係が不明確になり、設計が複雑になります。
生ポインタは完全に禁止ではない
生ポインタは、所有権を持たない参照としてなら使えます。
問題なのは、生ポインタで所有権を管理しようとすることです。
vectorを使えば常に安全というわけではない
std::vector はメモリ管理を自動化してくれますが、巨大なデータを扱えばメモリ不足になる可能性があります。
また、再確保によって要素へのポインタや参照が無効になることにも注意が必要です。
C++メモリ管理のベストプラクティス
C++のメモリ管理では、次の方針を守ると安全性が高まります。
値で持てるものは値で持つ
まずは、ポインタを使わずに値として表現できないかを考えます。
値で持てば、寿命が明確になり、メモリ管理の負担も減ります。
標準コンテナを使う
配列や文字列を手動で管理するより、std::vector や std::string を使うべきです。
標準コンテナはRAIIに基づいてメモリを管理してくれます。
所有権を型で表現する
単独所有なら std::unique_ptr、共有所有なら std::shared_ptr、所有しない参照なら参照または生ポインタを使います。
所有権が型から読み取れるようにすると、コードの安全性と可読性が高まります。
shared_ptrを乱用しない
共有所有は本当に必要な場合だけ使います。
単に関数の中で一時的に使うだけなら、参照で十分なことが多いです。
new/deleteを直接書かない
通常のアプリケーション開発では、new や delete を直接書く必要はほとんどありません。
どうしても必要な低レベル処理を除き、標準ライブラリに任せるべきです。
Rule of Zeroを目指す
自分でデストラクタやコピー処理、ムーブ処理を書かなくて済む設計を目指します。
標準コンテナやスマートポインタをメンバに持てば、多くの場合、コンパイラ生成の特殊メンバ関数で十分です。
最終まとめ
C++のメモリ管理は、単にメモリを確保して解放する話ではありません。
本質は、オブジェクトの寿命と所有権を明確にすることです。
現代C++では、次の考え方が重要です。
まず、値で持てるものは値で持ちます。
複数の要素は標準コンテナで管理します。
動的所有が必要なら std::unique_ptr を使います。
本当に共有所有が必要な場合だけ std::shared_ptr を使います。
循環参照を避けるには std::weak_ptr を使います。
所有しない参照には、参照または生ポインタを使います。
そして、メモリやリソースの解放は、できる限りRAIIに任せます。
C++のメモリ管理で最も大切なのは、「手動で頑張って管理すること」ではありません。
むしろ、手動管理を避け、型とスコープによって安全に管理することです。
そのための中心的な考え方が、RAII、スマートポインタ、標準コンテナ、Rule of Zeroです。
以上、C++のメモリ管理についてでした。
最後までお読みいただき、ありがとうございました。
