目次
導入
コンピューター プログラミングの分野では、特に C や C++ などの低レベル プログラミング言語では、メモリ管理が重要なタスクです。適切なメモリ管理を実践することで、プログラムのパフォーマンスと安定性を確保できます。この記事では、C/C++ メモリ管理のさまざまな側面を詳しく説明し、読者がメモリを正しく割り当て、解放する方法、および最適化戦略を通じてプログラムを改善する方法をよりよく理解できるようにします。
1. スタックおよびヒープメモリ
1.1 スタックメモリ
スタック メモリは、ローカル変数と関数呼び出しを管理するコンピュータ メモリに格納される領域です。スタック メモリは次のように動作します。
-
自動管理: スタック メモリの割り当てと解放はコンパイラによって自動的に処理されます。関数を開始すると、コンパイラはスタック上のローカル変数にメモリを割り当て、関数が終了すると、これらのローカル変数によって占有されていたメモリが自動的に解放されます。この自動管理メカニズムにより、ローカル変数の有効期間が関数呼び出しの有効期間と一致することが保証されます。
-
高速: スタック メモリの管理はコンパイラによって自動的に行われるため、スタック上でのメモリの割り当てと解放の操作が非常に高速です。これにより、スタック メモリは一時的で寿命の短い変数に適したものになります。
-
サイズ制限: スタックのサイズは通常、オペレーティング システムとコンパイラの設定に応じて固定されています。これは、スタック メモリの容量が制限されており、ローカル変数が多すぎるとスタック オーバーフローが発生する可能性があることを意味します。
-
後入れ先出し ( LIFO ) : スタック メモリは後入れ先出しの原則に従います。つまり、最後に割り当てられたローカル変数が最初に解放されます。
スタック メモリは通常、関数パラメータ、ローカル変数、および関数呼び出し情報を保存するために使用されます。関数呼び出しと戻り値の処理において重要な役割を果たします。
1.2 ヒープメモリ
ヒープ メモリは、プログラムの実行中にデータ構造とオブジェクトを格納するために動的に割り当てられるメモリ領域です。ヒープ メモリの機能は次のとおりです。
-
手動管理: スタックとは異なり、ヒープ メモリの割り当てと解放はプログラマが明示的に行う必要があります。C では、
malloc
ヒープ メモリは関数を使用して割り当てられますが、C++ ではnew
演算子を使用してこれを実現できます。ヒープ メモリを解放するには、メモリ リークを避けるために、対応する関数 (free
およびdelete
) を使用して手動でヒープ メモリを解放する必要があります。 -
柔軟なサイズ: ヒープ メモリのサイズに固定制限がないため、実行時にメモリを動的に割り当てることができます。これにより、ヒープ メモリは、動的配列や複雑なオブジェクトなど、不定サイズのデータ構造の格納に適したものになります。
-
速度が遅い: ヒープメモリの割り当てと解放はプログラマが明示的に操作する必要があるため、動作速度は比較的遅くなります。ヒープ メモリの割り当てと割り当て解除が頻繁に行われると、プログラムのパフォーマンスに影響を与える可能性があります。
-
メモリ リークに注意する: ヒープ メモリは手動で解放する必要があるため、メモリ ブロックが不要になったときに適切に解放されないとメモリ リークが発生する可能性があります。メモリ リークが発生すると、プログラムがより多くのメモリを消費し、最終的にはクラッシュにつながる可能性があります。
ヒープ メモリは通常、ライフ サイクルが長いデータ、サイズが不定のデータ、または動的に割り当てる必要があるデータを格納するために使用されます。
1.3 例
いくつかの例を見てみましょう。
Cの例
C++ の例
両者の違いは空間を開く方法と空間を解放する方法だけですが、基本的なロジックは似ています。!
2、C言語のメモリ管理方法
C 言語の動的メモリ管理関数 (
malloc
、calloc
、realloc
およびfree
)に関しては、その使用法と原理を理解することが非常に重要です。以下に、各機能の詳細な説明、使用例、および内部動作の構造を示します。
2.1 malloc関数
導入
malloc
この関数は、指定されたサイズのメモリ ブロックを割り当てるために使用され、割り当てられたメモリへのポインタを返します。割り当てられるメモリの初期値は未定義で、通常はガベージです。割り当てが失敗した場合に戻りますNULL
。
使用例
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr;
ptr = (int *)malloc(sizeof(int)); // 分配一个 int 大小的内存块
if (ptr != NULL) {
*ptr = 42;
printf("Value: %d\n", *ptr);
free(ptr); // 释放分配的内存
}
return 0;
}
原理分析
malloc
この関数は、要求されたサイズに応じてヒープ メモリ内の連続したメモリ領域を割り当てます。割り当てられたメモリ領域の開始アドレスへのポインタを返します。が呼び出されるとmalloc
、システムは空きメモリの適切なブロックを検索し、将来の割り当てが重複しないように、そのブロックを使用済みとしてマークします。
2.2 calloc関数
導入
calloc
この関数は、指定された数とサイズのメモリ ブロックを割り当て、。これは、必要なメモリ ブロックの数と各ブロック内のバイト数の2 つの引数を受け取り、割り当てられたメモリへのポインタを返します。割り当てが失敗した場合に戻りますNULL
。
使用例
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr;
ptr = (int *)calloc(1, sizeof(int)); // 分配一个 int 大小的内存块,初始化为零
if (ptr != NULL) {
printf("Value: %d\n", *ptr);
free(ptr); // 释放分配的内存
}
return 0;
}
原理分析
calloc
この関数は、必要なメモリ ブロックの数と各ブロックのバイト数を乗算したサイズの連続したメモリ領域をヒープ メモリに割り当てます。割り当てが完了すると、システムは割り当てられたメモリ領域のすべてのバイトを 0 に設定します。
2.3 再割り当て関数
導入
realloc
割り当てられたメモリのサイズを再割り当てするために使用される関数。割り当てられたメモリへのポインタと必要なメモリのバイト数を受け取り、再割り当てされたメモリへのポインタを返します。再割り当てが失敗した場合に返されますNULL
。を呼び出した後realloc
、元のポインタが解放される可能性があるため、返された新しいポインタを使用する必要があります。
説明する
- 関数プロトタイプ:
void *realloc(void *ptr, size_t size);
- パラメータ
ptr
: 、malloc
、によって以前calloc
にrealloc
割り当てられたメモリ ブロックへのポインタ。ptr
が null ポインター (つまり)の場合NULL
、 のrealloc
ように動作しますmalloc
。- パラメータ
size
:再割り当てするメモリ ブロックの新しいサイズ (バイト単位)。
効果
- 渡されたポインタが
ptr
の場合NULL
、 は のrealloc
ように動作しmalloc
、新しいメモリ ブロックを割り当て、このメモリへのポインタを返します。- 渡されたパラメータが
size
0 の場合、 のrealloc
動作は実装によって異なりますが、通常はptr
が指すメモリ ブロックが解放され、null ポインタが返されます。これを使用して、割り当てられたメモリ ブロックを解放できます。- 渡された
ptr
ポインタが ではなくNULL
、size
0 より大きい場合、 が指すメモリ ブロックのrealloc
再割り当てが試行され、ptr
サイズがバイトになるように試行されますsize
。これにより、次のいずれかの結果が生じる可能性があります。
- 元のメモリ ブロックのサイズが新しいサイズを収容できるほど大きい場合、メモリ ブロックはアドレスを変更せずに再割り当てされ、
realloc
元のポインタが返されます。- 元のメモリ ブロックのサイズが新しいサイズを収容するのに十分でない場合、
realloc
新しいサイズのデータを収容するのに十分な大きさのブロックをメモリ内で見つけて、元のデータを新しく割り当てられたメモリ ブロックにコピーします。元のメモリ ブロックが解放され、realloc
新しく割り当てられたメモリへのポインタが返されます。
realloc
メモリ ブロックの再割り当てプロセス中にデータのコピーとメモリ ブロックの移動が発生する可能性があるため、パフォーマンス重視のシナリオでは注意して使用する必要があることに注意してください。同時に、realloc
を使用した後は、元のポインタが無効になる可能性があるためrealloc
、再割り当てされたメモリにアクセスするには、返された のポインタを常に使用する必要があります。
使用例
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr;
ptr = (int *)malloc(sizeof(int)); // 分配一个 int 大小的内存块
if (ptr != NULL) {
*ptr = 42;
ptr = (int *)realloc(ptr, sizeof(int) * 2); // 重新分配为 2 个 int 大小的内存块
if (ptr != NULL) {
printf("Value: %d\n", *ptr);
free(ptr); // 释放分配的内存
}
}
return 0;
}
原理分析
realloc
この関数は、割り当てられたメモリのサイズを再割り当てします。新しいサイズが元のサイズより小さい場合、メモリ ブロックが縮小される可能性があります。新しいサイズが元のサイズより大きい場合、システムは元のメモリ ブロックの隣または他の適切な場所に大きなメモリ ブロックを再割り当てし、元のメモリ ブロックから新しいメモリ ブロックにデータをコピーします。再割り当てが失敗した場合、元のメモリ ブロックは変更されないままになります。
2.4 無料機能
導入
free
関数はmalloc
、 、calloc
またはによってrealloc
以前に割り当てられたメモリを解放するために使用されます。メモリを解放すると、そのメモリへのポインタは無効になるため、使用できなくなります。
使用例
#include <stdlib.h>
int main() {
int *ptr;
ptr = (int *)malloc(sizeof(int)); // 分配一个 int 大小的内存块
if (ptr != NULL) {
*ptr = 42;
free(ptr); // 释放分配的内存
}
return 0;
}
原理分析
free
この関数は、動的メモリ割り当て関数によって以前に割り当てられたメモリ ブロックを空きとしてマークし、今後の割り当てで再利用できるようにします。解放されたメモリはすぐにはオペレーティング システムに返されず、将来の呼び出しに備えてヒープ上に残されmalloc
ますcalloc
。
要約すると、動的メモリ管理関数 ( malloc
、calloc
、realloc
およびfree
) を使用すると、実行時にメモリの割り当てと割り当て解除を行うことができるため、より柔軟なメモリ使用が可能になります。ただし、メモリ リークやダングリング ポインタの問題を避けるために、適切なタイミングでメモリを解放するようにしてください。同時に、メモリの再割り当てによりデータの重複が発生し、パフォーマンスに影響を与える可能性があることに注意してください。
2.5 ハンギングポインタ
コンセプト
ダングリング ポインタは、解放されたメモリまたは無効なメモリへのポインタです。ポインタは、解放されたメモリ ブロック、または無効になったオブジェクトを指している場合、ダングリング ポインタになります。未解決のポインターにアクセスすると、プログラムのクラッシュ、未定義の動作、または予期しない結果が発生する可能性があります。
ポインターがダングリングする主な理由は 2 つあります。
1. 割り当てられたメモリを解放した後、ポインタが null にならない: メモリを使用した後free
、またはdelete
メモリを解放した後、ポインタが に設定されていない場合NULL
、ポインタは以前のアドレスを維持しますが、ポイントされたメモリは無効です。このようなポインタはダングリング ポインタです。
int *ptr = (int *)malloc(sizeof(int));
free(ptr); // 内存释放
// 这时 ptr 是一个悬挂指针
2. 関数がローカル変数へのポインタを返す:関数がローカル変数へのポインタを返し、関数が戻った後にそのポインタを使用しようとすると、ダングリング ポインタが返されます。これは、ローカル変数の有効期間が終了後に終了するためです。関数は を返します。
int *getLocalPtr() {
int x = 10;
return &x; // 返回局部变量的指针
}
int main() {
int *ptr = getLocalPtr(); // ptr 变成悬挂指针
// 后续对 ptr 的使用将导致未定义行为
return 0;
}
3. C++のメモリ管理方法
C++ プログラミングにおける動的メモリ管理に関しては、「新規」と「削除」の 2 つの非常に重要なキーワードです。これらは、プログラムの実行中にヒープ上のオブジェクトを作成および破棄するためにメモリの割り当てと解放に使用されます。「新規」と「削除」について詳しく説明します。
3.1 新しい演算子
「new」は、ヒープ上にメモリを動的に割り当てるために C++ で使用される演算子です。その基本的な構文は次のとおりです。
ここで、T
は割り当てられるメモリのデータ型で、pointer
は割り当てられたメモリへのポインタです。「new」演算子は次の手順を実行します。
T
型のオブジェクトを格納するのに十分なサイズのメモリを割り当てます。T
型のコンストラクターを呼び出して、新しいオブジェクトを初期化します。- 新しいオブジェクトへのポインタを返します。
例
3.2 削除演算子
「delete」は、「new」によって割り当てられたメモリを解放するために使用される演算子です。その構文は次のとおりです。
ここで、pointer
は「new」によって割り当てられたポインタです。「削除」演算子は次の手順を実行します。
- ポイントされたオブジェクトのデストラクターを呼び出して、リソースをクリーンアップします。
- オブジェクトが占有しているメモリを解放します。
- ポインタを無効にして、解放されたメモリへのアクセスを回避します。
例
3.3 注意事項
- メモリ リークを避けるために、各 "new" 呼び出しは対応する "delete" 操作で解放される必要があります。
- 同じポインターに対して「delete」を複数回呼び出さないでください。未定義の動作が発生する可能性があります。
- ポインタのダングリングを回避するには、メモリを解放した後にポインタを nullptr に設定します。
C++11 では、 "new" と "delete" の代替となる"new[]" と "delete[]"が導入されており、配列の割り当てと割り当て解除に使用されます。例えば:
ただし、最新の C++ でスマート ポインタ (例std::unique_ptr
: ) を使用してstd::shared_ptr
、手動メモリ管理の複雑さとリスクを軽減することをお勧めします。これらのスマート ポインターは、範囲外になると自動的にメモリを解放できます。
3.4 新規・削除操作組み込み型
「new」および「delete」演算子は、組み込み型 (整数、浮動小数点数など) を扱う場合、カスタム型と同様に機能します。以下は、組み込み型で「new」演算子と「delete」演算子を使用する方法の例です。
1. new を使用してメモリを割り当てます。
2. delete を使用してメモリを解放します。
メモリ リークを避けるために、「new」オペレータによって割り当てられたメモリは、対応する「delete」オペレータによって解放される必要があることに注意してください。また、メモリを割り当てた後に「delete」を呼び出すのを忘れると、メモリリークが発生します。
ただし、組み込み型を扱う場合は、スタック上の自動ストレージを使用することが一般的に推奨されます。これは、変数が存在するスコープが終了すると、変数が自動的に割り当て解除されることを意味します。これにより、手動メモリ管理の複雑さとリスクが回避されます。例えば:
3.5 新規および削除操作のカスタム タイプ
C++ では、「new」および「delete」演算子を使用して、ユーザー定義型、つまりユーザー定義クラス オブジェクトのメモリを動的に割り当てたり解放したりできます。カスタム タイプを処理するときに「new」演算子と「delete」演算子を使用する方法の例を次に示します。
class Person {
public:
Person(const std::string& name, int age) : name(name), age(age) {}
void DisplayInfo() const {
std::cout << "Name: " << name << ", Age: " << age << std::endl;
}
private:
std::string name;
int age;
};
1. new を使用してカスタム タイプ オブジェクトを割り当てます。
2. delete を使用してカスタム タイプ オブジェクトを解放します。
次の 2 点に注意してください。
- 「new」演算子は、クラスのコンストラクターを呼び出してオブジェクトを初期化します。
- 「削除」演算子は、クラスのデストラクターを呼び出して、リソースをクリーンアップし、メモリを解放します。
ただし、メモリ管理を向上させるために、最新の C++ では、明示的な「新規」および「削除」操作の代わりにスマート ポインターを使用することを推奨しています。std::unique_ptr
カスタム タイプ オブジェクトを処理するために使用する例を次に示します。
#include <memory>
// ...
std::unique_ptr<Person> personPtr = std::make_unique<Person>("Bob", 30);
// 使用 unique_ptr 分配一个 Person 对象
この場合、personPtr
関連付けられたメモリは範囲外になると自動的に解放されるため、手動で「delete」を呼び出す必要はありません。
複数のスマート ポインターがオブジェクトを共有する場合は、次を使用できますstd::shared_ptr
。
#include <memory>
// ...
std::shared_ptr<Person> sharedPersonPtr = std::make_shared<Person>("Charlie", 40);
// 使用 shared_ptr 分配一个 Person 对象
スマート ポインターを使用すると、手動メモリ管理の問題の多くが回避され、メモリの安全性が向上します。
第四に、新規と削除の実現原則
new
C++ で動的なメモリの割り当てと解放 ( and演算子など) を使用する場合、delete
実際には、メモリの割り当てと解放のために C++ 標準ライブラリで定義された一連の関数を呼び出します。これらの関数は「グローバルに構成された割り当て関数」と呼ばれ、<new>
ヘッダー ファイルをインクルードすることでアクセスできます。
4.1.operator new
およびoperator delete
:
この関数のペアは、単一オブジェクトのメモリ割り当てと割り当て解除に使用されます。それらのプロトタイプは次のとおりです。
operator new
: この関数は、指定されたサイズのメモリ ブロックをヒープ上に割り当てるために使用され、割り当てられたメモリへのポインタを返します。メモリの調整などの低レベルの詳細が考慮されます。割り当てが失敗した場合、例外がスローされる可能性がありますstd::bad_alloc
。
operator delete
: この関数は、 によって以前に割り当てられたメモリ ブロックを解放するために使用されますoperator new
。メモリ ブロックへのポインタをパラメータとして受け入れ、メモリを解放した後に必要なクリーンアップを実行できます。
使用例
4.2 operator new[]
およびoperator delete[]
:
この関数のペアは、配列メモリの割り当てと解放に使用されます。それらのプロトタイプは次のとおりです。
operator new[]
: この関数は、ヒープ上に指定されたサイズの配列メモリを割り当てるために使用され、割り当てられたメモリへのポインタを返します。これはoperator new
配列の割り当てに似ていますが、
operator delete[]
: この関数は、 によって以前に割り当てられた配列メモリを解放するために使用されますoperator new[]
。メモリ ブロックへのポインタをパラメータとして受け入れ、メモリを解放した後に必要なクリーンアップを実行できます。
使用例
これらの関数は通常、基礎となるメモリ アロケータ (オペレーティング システムによって提供されるメモリ アロケータなど) を呼び出して、実際の割り当てと割り当て解除を実行します。これらは通常、マルチスレッド環境における基本的なスレッドの安全性を保証します。
これらの関数はほとんどの場合に適切に機能しますが、特殊なケースでは、これらの関数をオーバーロードして、メモリ プールの実装やメモリ使用量の追跡などのカスタム メモリ管理ロジックを実装することを検討する場合があることに注意してください。
4.3 補足
C++ では、
new
anddelete
演算子は主にクラス型オブジェクトのメモリを動的に割り当てたり解放したりするために使用されます。もちろん、組み込みデータ型 (整数、浮動小数点数など) にメモリを割り当てたり解放したりするために使用することもできますが、ほとんどの場合、組み込みデータ型は通常、自動的に割り当てられ、解放されます。スタック。
1.new
クラス型の処理:
クラス型のオブジェクトがnew
演算子を使用して作成される場合、次の手順が実行されます。
- クラス型のオブジェクトを格納するのに十分なサイズのメモリを割り当てます。
- クラスのコンストラクターを呼び出して、新しいオブジェクトを初期化します。
- 新しいオブジェクトへのポインタを返します。
コード例
class MyClass {
public:
MyClass() { std::cout << "Constructor called." << std::endl; }
~MyClass() { std::cout << "Destructor called." << std::endl; }
};
MyClass* objPtr = new MyClass; // 使用 new 创建一个 MyClass 对象
delete objPtr; // 使用 delete 释放 MyClass 对象
2.delete
クラス型の処理:
delete
演算子を使用してクラス型のオブジェクトを解放する場合、次の手順が実行されます。
- クラスのデストラクターを呼び出してオブジェクト リソースをクリーンアップし、必要なクリーンアップを実行します。
- オブジェクトが占有しているメモリを解放します。
コード例
MyClass* objPtr = new MyClass; // 使用 new 创建一个 MyClass 对象
delete objPtr; // 使用 delete 释放 MyClass 对象
これらの手順により、メモリが動的に割り当てられる場合、オブジェクトが適切に構築され、適切なタイミングで破棄されることが保証されることに注意することが重要です。組み込みデータ型の場合、通常、スタック上で自動的に割り当ておよび解放されるため、new
および を明示的に使用する必要はありません。delete
new
orを使用する場合でもnew[]
、delete
orを使用する場合delete[]
でも、配列内のオブジェクトごとにコンストラクターとデストラクターが呼び出され、オブジェクトが正しいタイミングで構築および破棄されるようになります。