ホームページ > バックエンド開発 > C#.Net チュートリアル > C++のメモリ管理の詳細な説明

C++のメモリ管理の詳細な説明

黄舟
リリース: 2016-12-16 09:50:57
オリジナル
1079 人が閲覧しました

1. 対応する new と delete は同じ形式である必要があります。次のステートメントのどこが間違っていますか?
string *stringarray = new string[100];
...
delete stringarray;

すべてが順調に見えます - 新しいは削除に対応します - しかし、大きな間違いが隠されています: プログラムの実行状況予測不能になるだろう。 stringarray が指す 100 個の文字列オブジェクトのうち少なくとも 99 個は、デストラクターが呼び出されないため、適切に破棄されません。

新しいものを使用すると 2 つのことが起こります。まず、メモリが割り当てられ (Operator new 関数を介して、詳細については項目 7-10 および項目 m8 を参照)、その後、割り当てられたメモリに対して 1 つ以上のコンストラクターが呼び出されます。 delete を使用すると、2 つの処理も行われます。まず、メモリを解放するために 1 つ以上のデストラクターが呼び出され、次にメモリが解放されます (オペレータの delete 関数を介して、詳細については項目 8 と m8 を参照)。削除については、非常に重要な質問があります。メモリ内のオブジェクトをいくつ削除する必要があるかということです。答えによって、呼び出されるデストラクターの数が決まります。

簡単に言うと、この質問は次のとおりです。削除されるポインタは単一のオブジェクトを指しているのか、それともオブジェクトの配列を指しているのか?削除できるのはあなただけです。括弧なしで delete を使用すると、delete は単一のオブジェクトを指しているとみなします。それ以外の場合は、

string *stringptr1 = new string;
string *stringptr2 = new string[100 ];
とみなします。 ..

delete stringptr1;//オブジェクトを削除します
delete [] stringptr2;//オブジェクトの配列を削除します

stringptr1 の前に「[]」を追加するとどうなりますか?答えは、「それは推測できません。stringptr2 の前に「[]」を追加しなかったらどうなるでしょうか?答えも「推測不可能」です。また、int のような固定型の場合、その型にデストラクターがない場合でも、結果は予測できません。したがって、この種の問題を解決するためのルールは単純です。new を呼び出すときに [] を使用する場合は、delete を呼び出すときにも [] を使用する必要があります。 new を呼び出すときに [] を使用しない場合は、delete を呼び出すときに [] を使用しないでください。

ポインター データ メンバーを含み、複数のコンストラクターを提供するクラスを作成する場合は、このルールに留意することが特に重要です。この場合、ポインター メンバーを初期化するすべてのコンストラクターで同じ新しい形式を使用する必要があるためです。それ以外の場合、デストラクターではどのような形式の削除が使用されますか?このトピックの詳細については、項目 11 を参照してください。

このルールは、typedef を使用する人にとっても非常に重要です。typedef を作成するプログラマーは、new を使用して typedef で定義された型のオブジェクトを削除するためにどのような形式の削除を使用する必要があるかを他の人に指示する必要があるためです。例:

typedef string addresslines[4]; //個人の住所、合計 4 行、1 行に 1 つの文字列
//addresslines は配列なので、 new:
string *pal = new addresslines; // Pay " new addresslines" は string* を返します。これは
// "new string[4]" が返すのと同じです。削除する場合は、配列の形式で対応する必要があります:
delete pal;// エラー!
delete [] pal;// 正解

混乱を避けるために、配列型に typedef を使用しないことが最善です。標準 C++ ライブラリ (項目 49 を参照) には文字列とベクトルのテンプレートが含まれており、それらを使用すると配列の必要性がほぼゼロになるため、これは実際には簡単です。たとえば、アドレス行は文字列のベクトルとして定義できます。つまり、アドレス行はベクトル型として定義できます。

2. デストラクター内のポインター メンバーに対して delete を呼び出します

ほとんどの場合、動的メモリ割り当てを実行するクラスは、new を使用してコンストラクター内でメモリを割り当て、その後、デストラクター内で delete を使用してメモリを解放します。このクラスを初めて作成するときに行うのは決して難しいことではありません。すべてのコンストラクターにメモリが割り当てられているすべてのメンバーに対して最後に delete を使用することを忘れないでください。

ただし、このクラスが維持され、アップグレードされた後は、状況は難しくなります。クラスのコードを変更したプログラマーが、必ずしもこのクラスを最初に作成した人ではないからです。ポインタ メンバの追加は、ほぼ次の作業を意味します:
· 各コンストラクタでポインタを初期化します。一部のコンストラクターでは、ポインターにメモリが割り当てられない場合、ポインターは 0 (つまり、ヌル ポインター) に初期化されます。
· 既存のメモリを削除し、代入演算子を介して新しいメモリをポインタに割り当てます。
・デストラクター内のポインターを削除します。

コンストラクターでポインターを初期化するのを忘れた場合、または代入操作中にポインターを処理するのを忘れた場合、問題はすぐに明らかになるため、実際には、これら 2 つの問題によってそれほど悩まされることはありません。ただし、ポインターがデストラクター内で削除されていない場合、明らかな外部症状は現れません。むしろ、小さなメモリ リークのように見えるだけで、アドレス空間を使い果たし、プログラムが停止するまで増加し続けます。この状況は気づかれないことが多いため、クラスにポインター メンバーを追加するたびに、この状況を明確に覚えておく必要があります。

また、null ポインターを削除しても安全です (何もしないため)。したがって、コンストラクター、代入演算子、またはその他のメンバー関数を作成する場合、クラスの各ポインター メンバーは有効なメモリを指すか、null を指すかのいずれかになります。その後、デストラクター内で、それらが新しいかどうかを気にせずに単に削除できます。

もちろん、これらの用語の使用は絶対的なものであってはなりません。たとえば、new で初期化されていないポインターを削除するために delete を使用することは絶対にありません。また、スマート ポインター オブジェクトを削除せずに使用するのと同じように、渡されたポインターを削除することもありません。つまり、クラス メンバーが最初に new を使用していない限り、デストラクターで delete を使用する必要はありません。

スマート ポインターについて言えば、ポインター メンバーを削除する必要を回避する方法があります。これは、これらのメンバーを C++ 標準ライブラリの auto_ptr などのスマート ポインター オブジェクトに置き換えることです。それがどのように機能するかを知るには、条項 m9 と m10 を見てください。

3. メモリ不足に備えて事前に準備してください
演算子 new は、メモリ割り当て要求を完了できない場合に例外をスローします (以前のメソッドは通常 0 を返しますが、一部の古いコンパイラはまだこれを行っています。コンパイラを使用することもできます)このように設定されています。このトピックの説明はこの記事の最後まで延期します)。メモリ不足が原因で発生した例外を処理することが実際には道徳的な行為であることは誰もが知っていますが、実際には首にナイフで刺されたような痛みを伴うことになります。それで、時々それを放っておくかもしれません、おそらくそれを気にしないかもしれません。しかし、心の中にはまだ深い罪悪感が隠されているはずです。「新品で本当に何か問題が起こったらどうしよう?」
この状況に対処する方法として、以前の方法に戻って前処理を使用する方法が自然に思い浮かびます。たとえば、C では、タイプに依存しないマクロを定義してメモリを割り当て、割り当てが成功したかどうかを確認するのが一般的です。 C++ の場合、このマクロは次のようになります。


#define new(ptr, type)
try { (ptr) = new type; }
catch (std::bad_alloc&) {assert(0) }

( 「遅いです! std::bad_alloc は何をするのですか?」という質問です。 bad_alloc は、演算子 new がメモリ割り当て要求を満たせない場合にスローされる例外タイプです。std は、bad_alloc が配置されている名前空間の名前です (項目 28 を参照)。 !" あなたは、「assert は何に使うのですか?」と尋ね続けます。標準 C ヘッダー ファイル (または、名前空間を使用した同等のファイル、項目 49 を参照) を見ると、assert がマクロであることがわかります。このマクロは、渡された式がゼロ以外の場合、エラー メッセージが発行され、標準マクロ ndebug が定義されていない場合、つまりデバッグ モードでのみこれが行われます。製品のリリース状態、つまり ndebug が定義されている場合、assert は何も行わず、空のステートメントと同等であるため、デバッグ中にアサーション (アサーション) を確認することしかできません。

新しいマクロは、公開されたプログラムで発生する可能性のあるステータスをassertを使用して確認する(ただし、メモリ不足はいつでも発生する可能性がある)という上記の一般的な問題を抱えているだけでなく、同時に、 C++ の別の関数 1 つの欠陥: new のさまざまな使用方法が考慮されていません。たとえば、型 t のオブジェクトを作成する場合、一般に 3 つの一般的な構文形式があり、各形式で発生する可能性のある例外を処理する必要があります:


new t;
new t(constrUCtor argument);
new。 t[size ];

ここで問題は大幅に単純化されています。人によっては演算子 new をカスタマイズ (オーバーロード) するため、プログラムには new を使用するあらゆる構文形式が含まれることになります。

それで、どうすればいいでしょうか?非常に単純なエラー処理方法を使用したい場合は、メモリ割り当て要求を満たせないときに、事前に指定したエラー処理関数を呼び出すことができます。このメソッドは規則に基づいています。つまり、演算子 new が要求を満たすことができない場合、例外をスローする前に顧客によって指定されたエラー処理関数 (一般に new-handler 関数と呼ばれます) が呼び出されます。 (演算子 new の実際の動作はより複雑です。詳細については、第 8 項を参照してください)

set_new_handler 関数は、エラー処理関数を指定するときに使用されます。ヘッダー ファイル内で次のように大まかに定義されます:


typedef void (*new_handler. ) ();
new_handler set_new_handler(new_handler p) throw();

ご覧のとおり、new_handler はカスタム関数ポインター型で、入力パラメーターも戻り値も持たない関数を指します。 set_new_handler は new_handler の型を入力して返す関数です。

set_new_handler の入力パラメータは、operator new がメモリの割り当てに失敗したときに呼び出されるエラー処理関数のポインタであり、戻り値は、set_new_handler が呼び出される前にすでに有効であった古いエラー処理関数のポインタです。

次のように set_new_handler を使用できます:


// 演算子 new が十分なメモリを割り当てられない場合に呼び出す関数
void nomorememory()
{
cerr << "メモリの要求を満たすことができません
";
abort();
}

int main()
{
set_new_handler(nomorememory);
int *pbigdataarray = new int[100000000];

...

}

演算子 new が 100,000,000 個の整数にスペースを割り当てることができない場合、nomorememory が呼び出され、プログラムはエラー メッセージを表示して終了します。これは、単にシステム カーネルにエラー メッセージを生成させてプログラムを終了させるよりも優れています。 (ところで、考えてみてください。エラー情報を書き込むプロセス中に cerr が動的にメモリを割り当てる必要がある場合はどうなるでしょうか...)

演算子 new がメモリ割り当て要求を満たせない場合、new-handler 関数は単にメモリ割り当て要求を満たさないだけではありません。十分なメモリが見つかるまで繰り返します。一度だけ呼び出されますが、継続的に呼び出されます。繰り返しの呼び出しを実装するコードは項目 8 にあります。ここでは説明的な言語を使用して説明しています。 適切に設計された new-handler 関数は、次の関数のいずれかを実装する必要があります。
· 利用可能なメモリをさらに生成します。これにより、operator new による次回のメモリ割り当ての試みが成功する可能性が高くなります。この戦略を実装する 1 つの方法は、プログラムの開始時に大きなメモリ ブロックを割り当て、最初に new-handler が呼び出されたときにそのブロックを解放することです。このリリースには、ユーザーに対するいくつかの警告メッセージが伴います。たとえば、メモリの量が少なすぎる場合、より多くの空き領域がないと次のリクエストが失敗する可能性があります。
· 別の new-handler 関数をインストールします。現在の new-handler 関数がより多くの利用可能なメモリを生成できない場合、別の new-handler 関数がより多くのリソースを提供できることを認識する可能性があります。この場合、現在の new-handler は、(set_new_handler を呼び出すことにより) 別の new-handler をインストールして置き換えることができます。次回、operator new が new-handler を呼び出すとき、最後にインストールされたものが使用されます。 (この戦略のもう 1 つのバリエーションは、new-handler が自身の実行動作を変更できるようにして、次回呼び出されたときに別の動作を行うようにすることです。これは、new-handler が自身に影響を与える静的変数を変更できるようにすることで行われます。動作またはグローバル データ)
· 新しいハンドラーを削除します。つまり、null ポインタを set_new_handler に渡します。 new-handler がインストールされていない場合、演算子 new はメモリの割り当てに失敗したときに標準の std::bad_alloc タイプの例外をスローします。
· std::bad_alloc または std::bad_alloc から継続して他の種類の例外をスローします。このような例外は演算子 new ではキャッチされないため、メモリ要求が最初に行われた場所に送信されます。 (異なるタイプの例外をスローすると、オペレーターの new 例外仕様に違反します。仕様のデフォルトの動作は、abort を呼び出すことであるため、 new-handler が例外をスローしたい場合は、それが std::bad_alloc から継続していることを確認する必要があります。例外仕様の詳細については、項目 m14 を参照してください。)
· 返品不可。一般的なアプローチは、abort または exit を呼び出すことです。 abort/exit は標準 C ライブラリ (および標準 C++ ライブラリ、項目 49 を参照) にあります。

上記のオプションを使用すると、new-handler 関数を非常に柔軟に実装できます。

メモリ割り当ての失敗を処理するときに取る方法は、割り当てられるオブジェクトのクラスによって異なります:


class x {
public:
static void

outofmemory();

...

};

class y {
public:
static void outofmemory();

...

};

x* p1 = new x; // 割り当てが成功したら、x::outofmemory
y* p2 = を呼び出します。 new y ; // 割り当てが失敗した場合は、 y::outofmemory を呼び出します

C++ はクラス専用の new-handler 関数をサポートしていないため、必要ありません。各クラスに独自のバージョンの set_new_handler と Operator new を提供することで、これを自分で実装できます。クラスの set_new_handler は、クラスの new-handler を指定できます (標準の set_new_handler がグローバル new-handler を指定するのと同じように)。クラスの演算子 new は、クラスのオブジェクトにメモリを割り当てるときに、グローバル new-handler の代わりにクラスの new-handler が使用されるようにします。

クラス x のメモリ割り当て失敗のケースを処理するとします。演算子 new は型 x のオブジェクトにメモリを割り当てることができないため、毎回エラー処理関数を呼び出す必要があるため、クラス内で new_handler 型の静的メンバーを宣言する必要があります。次に、クラス

クラスの静的メンバーをクラスの外で定義する必要があります。静的オブジェクトのデフォルトの初期化値0を借用したいため、x::currenthandler定義時に初期化はしませんでした。


new_handler x::currenthandler; //デフォルト設定の currenthandler は 0 (つまり null) です
クラス x の set_new_handler 関数は、渡されたポインターを保存し、呼び出す前に保存されたポインターを返します。これは、set_new_handler の標準バージョンが行うこととまったく同じです:


new_handler x::set_new_handler(new_handler p)
{
new_handler oldhandler = currenthandler;
currenthandler = p;
return oldhandler;
}

最後に、次の演算子を見てみましょう。 x の機能:
1. 標準の set_new_handler 関数を呼び出します。入力パラメーターは x のエラー処理関数です。これにより、x の new-handler 関数がグローバルな new-handler 関数になります。次のコードでは、「::」記号を使用して std 空間を明示的に参照していることに注意してください (標準の set_new_handler 関数は std 空間に存在します)。

2. グローバル演算子 new を呼び出してメモリを割り当てます。最初の割り当てが失敗した場合、グローバル演算子 new は、グローバル new ハンドラーとしてインストールされたばかりであるため、x の new ハンドラーを呼び出します (1. を参照)。グローバル演算子 new が最終的にメモリの割り当てに失敗すると、std::bad_alloc 例外がスローされ、x の演算子 new がそれをキャッチします。 x の演算子 new は、最初に置き換えられたグローバル new ハンドラー関数を復元し、最後に例外をスローして戻ります。

3. グローバル演算子 new が型 x のオブジェクトにメモリを正常に割り当てたと仮定すると、x の演算子 new は標準の set_new_handler を再度呼び出して、元のグローバル エラー処理関数を復元します。最後に、正常に割り当てられたメモリへのポインタが返されます。
C++ はこれを行います:


void * x::operator new(size_t size)
{
new_handler globalhandler = // x
std::set_new_handler(currenthandler)の new_handler をインストールします;

void *memory;

try { // メモリの割り当てを試みます
memory = ::operator new(size);
}

catch (std::bad_alloc&) { // 古い new_handler を復元します
std::set_new_handler(globalhandler); // Throws; anException
}
std::set_new_handler(globalhandler); // 古い new_handler を復元します
return メモリ;
}

上記の std::set_new_handler の繰り返し呼び出しが気に入らない場合は、削除する項目 m9 を参照してください。彼ら。

クラス x のメモリ割り当て処理関数は、おおよそ次のとおりです:


void nomorememory();// Set nomorememory のオブジェクトが
// グローバルの新規処理関数に呼び出されるときに呼び出される new_handler 関数の宣言new-handling function

x::set_new_handler(0);
// x の new-handling 関数を空に設定します

x *px2 = new x;
// メモリ割り当てに失敗した場合、例外がスローされますすぐに
// (クラスに新規処理関数はありません項目 41 で説明しているように、継続とテンプレートを使用して再利用可能なコードを設計できます。ここでは、要件を満たすために 2 つの方法を組み合わせます。

「ミックスインスタイル」基本クラスを作成するだけで済みます。これにより、サブクラスがその特定の機能を継続できるようになります。ここでは、クラスの新しいハンドラーを作成する機能を指します。基本クラスが設計される理由は、すべてのサブクラスが set_new_handler 関数と Operator new 関数を継続できるようにするためであり、テンプレートは、各サブクラスが異なる currenthandler データ メンバーを持つように設計されています。これは複雑に聞こえますが、コードは実際には非常に馴染みのあるものであることがわかります。唯一の違いは、どのクラスでも再利用できることです。


template // クラス set_new_handler のサポートを提供します
class newhandlersupport { // 「混合スタイル」の基本クラス
public:
static new_handler set_new_handler(new_handler p);

static void *operator new(size_t size);

private :
static new_handler currenthandler;
};

template
new_handler newhandlersupport::set_new_handler(new_handler p)
{
new_handler oldhandler = currenthandler;
currenthandler = p;
return oldhandler;
}

template
void *ラーサポート::operator new(size_t size)
{
new_handler globalhandler =
std::set_new_handler(currenthandler);
void *memory;
try {
memory = ::operator new(size);
}
catch (std:: bad_alloc&) {
std::set_new_handler(globalhandler);
throw;
}

std::set_new_handler(globalhandler);
return Memory;
}
// これにより、各 currenthandler が 0 に設定されます

template
new_handler newhandlersupport:: currenthandler;
このテンプレート クラスでは、set_new_handler 関数をクラス Why
// ここではプライベート継承の方が望ましいかもしれません。)
class x: public newhandlersupport {

... // 前と同様ですが、
の宣言はありません。 // set_new_handler または演算子 new


は、x を使用する場合でも動作します。舞台裏で何が行われているかを心配する必要はありません。古いコードは引き続き動作します。これはいい!無視することが多いものは、多くの場合、最も信頼できるものです。

set_new_handler を使用すると、メモリ不足に対処する便利で簡単な方法です。これは確かに、新しいものを try モジュールでラップするよりもはるかに優れています。さらに、newhandlersupport のようなテンプレートを使用すると、特定の new-handler を任意のクラスに簡単に追加できます。 「混合スタイル」の継続により、必然的にトピックが複数の継続につながります。このトピックに進む前に項目 43 を読む必要があります。

1993 年以前は、C++ ではメモリ割り当てが失敗した場合に、常に演算子 new が 0 を返す必要がありました。現在では、演算子 new が std::bad_alloc 例外をスローする必要があります。多くの C++ プログラムは、コンパイラーが新しい仕様のサポートを開始する前に作成されました。 C++ 標準委員会は、return-0 仕様に従った既存のコードを放棄したくなかったため、return-0 機能を提供し続ける代替形式の演算子 new (および演算子 new[] - 項目 8 を参照) を提供しました。これらのフォームは throw を使用せず、 new を使用してエントリ ポイントで nothrow オブジェクトを使用するため、「throwless」と呼ばれます。

widget *pw1 = new widget; Failure throws std::bad_alloc if

if (pw1 == 0) ... // このチェックは失敗する必要があります
widget *pw2 = new (nothrow) widget; // 割り当てが失敗した場合は、0を返します

if (pw2 = = 0) ... // このチェックは成功する可能性があります

new の「通常の」(つまり、例外をスローする) 形式を使用するか、new の「スローしない」形式を使用するかに関係なく、重要なことは、次のことを行う必要があることです。メモリ割り当ての失敗に備えてください。最も簡単な方法は、どちらの形式でも機能する set_new_handler を使用することです。

4. 演算子 new と演算子 delete を記述するときは規則に従います

演算子 new を自分で書き換える場合 (項目 10 でオーバーライドが必要になる理由を説明します)、関数によって提供される動作がシステムのデフォルトと一致していることが重要です。演算子 new は一貫しています。実際の実装は次のとおりです: 正しい戻り値を持ち、使用可能なメモリが十分でない場合はエラー処理関数を呼び出します (項目 7 を参照)。また、new の標準形式を誤って非表示にしないようにしてください。ただし、それは項目 9 のトピックです。

戻り値に関する部分は簡単です。メモリ割り当て要求が成功すると、メモリへのポインタが返され、失敗すると、項目 7 で指定されているように std::bad_alloc 型の例外がスローされます。

しかし、物事はそれほど単純ではありません。演算子 new は実際にはメモリの割り当てを複数回試行するため、失敗するたびにエラー処理関数を呼び出す必要があり、また、エラー処理関数が他の場所でメモリを解放する方法を見つけることも期待されます。演算子 new は、エラー処理関数へのポインタが null の場合にのみ例外をスローします。

さらに、C++ 標準では、0 バイトのメモリの割り当てを要求する場合でも、演算子 new は正当なポインタを返さなければなりません。 (実際、この奇妙に聞こえる要件は、C++ 言語の他の部分に単純さをもたらします)

このようにして、非クラス メンバーの形式の演算子 new の疑似コードは次のようになります:
void * operande new (size_t size) // 演算子 new には他のパラメータも含まれる場合があります
{

if (size == 0) { // 0 バイトのリクエストを処理する場合、
size = 1; // 1 バイトのリクエストとして扱います。
}
while (1) {
size バイトのメモリを割り当てる;

if (割り当て成功)
return (メモリへのポインタ);

//割り当てが失敗した場合は、現在のエラー処理関数を確認する
new_handler globalhandler = set_new_handler (0);
set_new_handler(globalhandler);

if (globalhandler) (*globalhandler)();
else throw std::bad_alloc();
}
}

ゼロバイトリクエストを処理するコツは、次のとおりです。処理するバイトを要求するものとして扱います。これは奇妙に思えるかもしれませんが、シンプルかつ合法的で効果的な方法です。また、ゼロバイトリクエストに遭遇する頻度はどれくらいですか?

なぜ上記の疑似コードでエラー処理関数が 0 に設定され、すぐに復元されるのか疑問に思われるかもしれません。これは、エラー処理関数へのポインタを直接取得する方法がないため、set_new_handler を呼び出してポインタを見つける必要があるためです。この方法は愚かですが効果的です。


項目 7 では、演算子 new が内部に無限ループを含むと述べましたが、上記のコードはこれを明確に示しています - while (1) は無限ループを引き起こします。ループから抜け出す唯一の方法は、メモリの割り当てが成功するか、エラー ハンドラが項目 7 で説明されているイベントの 1 つを完了することです。より多くの利用可能なメモリが取得され、新しい新しいハンドラ (エラー ハンドラ) がインストールされます。ハンドラーが std::bad_alloc またはその派生型の例外をスローしたか、失敗を返しました。これで、new-handler がこれらのタスクのいずれかを実行しなければならない理由がわかりました。これを行わないと、operator new のループは終了しません。

多くの人が気づいていないことの 1 つは、演算子 new はサブクラスによって継承されることが多いということです。これにより、いくつかの複雑な問題が発生します。上記の疑似コードでは、関数は size バイトのメモリを割り当てます (size が 0 でない場合)。サイズは関数に渡されるパラメータであるため重要です。しかし、クラス用に新しく書かれたほとんどの演算子 (項目 10 のものを含む) は、すべてのクラスやそのすべてのサブクラスではなく、特定のクラスのみを対象として設計されています。これは、クラスの new 演算子に対して、ただし、基本クラスの演算子 new が存在するため、サブクラス オブジェクトにメモリを割り当てるために呼び出される場合があります:
class Base {
public:
static void *operator new(size_t size);
...
};

class派生: public base // 派生クラスは演算子 new
{ ... } //

derived *p = newderived // Base::operator new

を呼び出します。基本クラスの演算子 new がこの状況に特別に対処する手間をかけたくない場合 (これは発生する可能性は低いですが)、最も簡単な方法は、この「間違った」数のメモリ割り当て要求を標準演算子 Process に転送することです。 、次のようになります:
void * Base::operator new(size_t size)
{
if (size != sizeof(base)) // 数量が「間違っている」場合は、標準の演算子 new
return ::operator new を使用します( size); // このリクエストを処理しに行く
//

... // それ以外の場合はこのリクエストを処理する
}

「無理だけど可能なメソッドをチェックするのを忘れた!」と叫んでいるのが聞こえました。発生する状況 - サイズがゼロになる可能性があります!」はい、確認しませんでしたが、次回叫ぶときはあまり堅苦しくならないでください。 :) しかし、実際にはチェックはまだ行われていますが、size != sizeof(base) ステートメントに統合されています。 C++ 標準は奇妙です。その 1 つは、すべての独立したクラスのサイズが 0 以外であることです。そのため、sizeof(base) をゼロにすることはできません (基本クラスにメンバーがない場合でも)。リクエストは ::operator new に送られ、適切な方法でリクエストが処理されます。 (興味深いことに、base が独立したクラスではない場合、sizeof(base) はゼロになる可能性があります。詳細については、「オブジェクトのカウントに関する私の記事」を参照してください。)

クラスベースの配列のメモリ割り当てを制御したい場合は、operator new の配列形式、operator new[] を実装する必要があります (この関数は、「operator new」を考えることができないため、「array new」と呼ばれることがよくあります) []")。 発音の仕方)。演算子 new[] を記述するときは、「生の」メモリを扱っているため、配列内にまだ存在しないオブジェクトに対しては操作を実行できないことに注意してください。実際、各オブジェクトの大きさがわからないため、配列内にオブジェクトがいくつあるかさえわかりません。基本クラスの演算子 new[] は、継続を通じてサブクラス オブジェクトの配列にメモリを割り当てるために使用され、サブクラス オブジェクトは多くの場合、基本クラスよりも大きくなります。したがって、base::operator new[] 内の各オブジェクトのサイズが sizeof(base) であることは当然のこととは言えません。言い換えれば、配列内のオブジェクトの数は必ずしも (要求されたバイト数)/sizeof であるとは限りません。 (ベース)。演算子 new[] の詳細については、m8 節を参照してください。

演算子 new (および演算子 new[]) をオーバーライドするときに従うべき規則はこれですべてです。 delete 演算子 (およびそのメイト演算子 delete[]) の場合、状況はより単純です。覚えておく必要があるのは、C++ では null ポインターを削除しても常に安全であることが保証されているため、この保証を最大限に活用する必要があるということだけです。以下は、クラス以外のメンバーの形式での演算子 delete の疑似コードです。 , return
//

rawmemory;

return;
}

この関数のクラスメンバーバージョンも単純ですが、削除されたオブジェクトのサイズもチェックする必要があります。クラスの演算子 new が「間違った」サイズの割り当てリクエストを ::operator new に転送すると仮定すると、「間違った」サイズの削除リクエストも ::operator delete:

class base { / / 前と同じですが、ここで宣言されています
public: //operator delete
static void *operator new(size_t size);
static void deleteoperator(void *rawmemory, size_t size);
...
};

voidbase::operator delete (void *rawmemory, size_t size)
{
if (rawmemory == 0) return; // null ポインタを確認します

if (size != sizeof(base)) { // if sizeは「間違っています」、
::operator delete(rawmemory); // 標準のオペレーターにリクエストを処理させます
return;
}

rawmemory;

return;
}

ご覧のとおり、演算子 new と演算子 delete (およびそれらの配列形式) があります。ルールはそれほど面倒ではありません。重要なのはそれらに従うことです。メモリ アロケータが new-handler 関数をサポートし、ゼロ メモリ リクエストを正しく処理する限り、それだけです。メモリ デアロケータが null ポインタを処理する場合は、他に何もする必要はありません。関数のクラスメンバーバージョンへの継続サポートの追加については、間もなく行われる予定です。
5. new の標準形式を隠さないようにします
内部スコープで宣言された名前は外部スコープでも同じ名前を隠すため、クラス内およびグローバルで宣言された同じ名前を持つ 2 つの関数 f については、クラスのメンバー関数がグローバル関数を隠す:
void f(); // グローバル関数
class x {
public:
void f(); // メンバー関数
};

x x;

f(); // x.f(); // グローバル関数とメンバー関数の呼び出しでは常に異なる

構文が使用されるため、x::f

の呼び出しは驚くべきことではなく、混乱することもありません。ただし、複数のパラメーターを持つ演算子 new 関数をクラスに追加すると、予期しない結果が生じる可能性があります。

class x {
public:
void f();

// 演算子 new のパラメータは、
// new-hander (new のエラー処理) 関数を指定します
static void * Operator new (size_t size, new_handler p) ;
};

voidspecialerrorhandler(); // 他の場所で定義されています

x *px1 =
new (specialerrorhandler) x; // x::operator を呼び出します new

x *px2 = new x; // エラー!

クラスで「operator new」という関数を定義すると、標準 new へのアクセスが誤ってブロックされてしまいます。項目 50 では、このような理由が説明されていますが、ここでさらに関心があるのは、この問題を回避する方法を見つける方法です。

1 つの方法は、標準の new 呼び出しメソッドをサポートするクラスに演算子 new を記述することです。これは、標準の new と同じことを行います

。これは効率的なインライン関数を使用して実装できます。

class x {
public:
void f();

static void *演算子new(size_t size, new_handler p);

static void *演算子new(size_t size)
{ return ::operator new(size) ; }
};

x *px1 =
new (specialerrorhandler) x; // x::operator
を呼び出す// new(size_t, new_handler)

x* // x::operatorを呼び出す
// new(size_t)

もう 1 つのアプローチは、演算子 new に追加される各パラメーターのデフォルト値を提供することです (項目 24 を参照):

class x {
public:
void f();

static
void *演算子 new(size_t size, // p のデフォルト値は 0
new_handler p = 0); //
};

x *px1 = new (specialerrorhandler) x // 正しい

x* px2 = new x; // こちらも正解です

どの方法を使用しても、将来 new の「標準」形式に合わせて新しい関数をカスタマイズしたい場合は、この関数を書き直すだけで済みます。

呼び出し元は、再コンパイルしてリンクした後、新しい関数を使用できるようになります。

6. 演算子 new を記述する場合は、演算子 delete も記述する必要があります

この基本的な質問に戻って考えてみましょう: なぜ独自の演算子 new と演算子 delete を記述する必要があるのでしょうか?

答えは通常、「効率のため」です。デフォルトの演算子 new と演算子 delete は非常に多用途であり、その柔軟性により特定の状況でのパフォーマンスをさらに向上させることもできます。これは、多数の小さなオブジェクトを動的に割り当てる必要があるアプリケーションに特に当てはまります。

たとえば、飛行機を表すクラスがあります: クラス飛行機には、飛行機オブジェクトの実際の記述を指すポインタのみが含まれます (この手法については項目 34 で説明します):

class plainrep { ... }; // 飛行機オブジェクトを意味します
//
classplane {
public:
...
private:
airplanerep *rep; // 実際の説明へのポインタ
};

飛行機オブジェクトは大きくはなく、含まれているだけですポインター (節 14 と m24 にあるように、航空機クラスが仮想関数を宣言する場合、2 番目のポインターが暗黙的に含まれることを示しています)。ただし、operator new を呼び出して航空機オブジェクトを割り当てると、ポインター (またはポインターのペア) を格納するのに必要な以上のメモリが得られる可能性があります。この一見奇妙な動作が発生する理由は、演算子 new と演算子 delete が相互に情報を渡す必要があるためです。

演算子 new のデフォルト バージョンは汎用メモリ アロケータであるため、任意のサイズのメモリ ブロックを割り当てることができなければなりません。同様に、オペレータ delete も任意のサイズのメモリ ブロックを解放できなければなりません。 delete 演算子が解放したいメモリ量を知りたい場合は、new 演算子によって最初に割り当てられたメモリ量を知る必要があります。オペレータ new がオペレータ delete に最初に割り当てたメモリのサイズを通知し、割り当てられたメモリ ブロックのサイズを示す追加情報を返されるメモリに事前に含める一般的な方法があります。つまり、次のステートメントを作成すると、

airplane *pa = new airplane;

次のようなメモリ ブロックは取得されません:

pa --> 代わりに飛行機オブジェクト

のメモリ次のようにメモリ ブロックを取得します:

pa——> メモリ ブロック サイズ データ + 飛行機オブジェクトのメモリ

飛行機のような小さなオブジェクトの場合、これらの追加データ情報によりオブジェクトを動的に割り当てる必要があります。メモリ サイズは 2 倍になります。特にクラスに仮想関数がない場合)。

メモリが貴重な環境でソフトウェアが実行されている場合、そのような贅沢なメモリ割り当てスキームを使用する余裕はありません。航空機クラス専用に新しい演算子を作成すると、割り当てられた各メモリ ブロックに追加情報を追加することなく、各航空機のサイズが等しいという事実を利用できます。

具体的には、カスタム オペレーター new を実装する方法があります。まず、デフォルトのオペレーター new に生のメモリの大きなブロックを割り当てさせます。各ブロックは、多くの航空機オブジェクトを収容するのに十分な大きさです。飛行機オブジェクトのメモリ ブロックは、これらの大きなメモリ ブロックから取得されます。現在使用されていないメモリ ブロックは、航空機による将来の使用に備えて、フリー リンク リストと呼ばれるリンク リストに編成されます。各オブジェクトは(リンクリストをサポートするために)次のフィールドのオーバーヘッドを負担する必要があるように聞こえますが、そうではありません。repフィールドのスペースは、次のポインターを格納するためにも使用されます(使用されるメモリブロックにのみ必要であるため)航空機オブジェクトの rep ポインタとして、同様に、航空機オブジェクトとして使用されないメモリ ブロックのみが次のポインタを必要とします)。これは、union を使用して実装できます。


特定の実装中に、カスタマイズされたメモリ管理をサポートするために航空機の定義を変更する必要があります。これを行うことができます:

class plain { // 変更されたクラス - カスタマイズされたメモリ管理をサポート
public: //

static void *operator new(size_t size);

...

private:
union {
plainrep *rep; // 使用されているオブジェクトの場合
airplane *next; // 未使用のオブジェクトの場合 (自由リンク リスト内)
};

// 大きなメモリ ブロックを指定する
// 飛行機オブジェクトの数後で初期化されますか?
static const int block_size;

staticplane *headoffreelist;

};

上記のコードは、演算子 new 関数、共用体 (rep フィールドと next フィールドが同じスペースを占めるようにするため)、定数 (大きなメモリ ブロックのサイズを指定)、および静的ポインター (空きリンク リストの先頭を追跡)。航空機オブジェクトごとではなく、クラス全体に対して空きリンク リストが 1 つだけ存在するため、ヘッダー ポインターを静的メンバーとして宣言することが重要です。

オペレーター new 関数を作成します:

void *plane::operator new(size_t size)
{
// 「間違った」サイズのリクエストを処理のために ::operator new() に転送します;
// を参照してください。詳細 条項 8
if (size != sizeof(airplane))
return ::operator new(size);

airplane *p = // p はフリーリンクリストの先頭を指します
headoffreelist; //

/ / p 正当な場合は、リストの先頭を次の要素に移動します
//
if (p)
headoffreelist = p->next;

else {
//自由なリンクされたリストが空の場合、大きなメモリ ブロック,
// block_size の航空機オブジェクトを収容可能
airplane *newblock =
static_cast(::operator new(block_size *
sizeof(airplane)));

// それぞれの小さなメモリ ブロックをリンクして、新しい空きリンクを形成しますlist
// 0 番目の要素は演算子の呼び出し元に返されるためスキップします new
//
for (int i = 1; i newblock[i].next = &newblock [i+1];

// null ポインタでリンクされたリストを終了します
newblock[block_size-1].next = 0;

// p はテーブルの先頭に設定され、
// メモリheadoffreelist が指すブロックが続きます then
p = newblock;
headoffreelist = &newblock[1];
}

return p;
}

項目 8 を読むと、演算子 new がメモリ割り当て要求を満たすことができないときがわかるでしょう。 、新しいハンドラー関数と例外に関連する一連のルーチン アクション。上記のコードにはこれらの手順がありません。これは、operator new によって管理されるメモリが from::operator new から割り当てられるためです。これは、::operator new が失敗した場合にのみ、operator new が失敗することを意味します。そして、::operator new が失敗した場合は、new-handler のアクションが実行される (例外をスローして終了する可能性がある) ため、航空機オペレーター new がそれを処理する必要もありません。言い換えれば、new-handler のアクションは実際にはまだ存在しており、目に見えないだけで、::operator new に隠されています。

演算子 new を使用して、次に行うことは、航空機の静的データ メンバーを定義することです:

airplane *airplane::headoffreelist;

const int airplane::block_size = 512;

明示的に追加する必要はありません。デフォルトでは静的メンバーの初期値が0に設定されているため、headoffreelistにはnullポインタが設定されます。 block_size は、::operator new から取得されるメモリ ブロックの大きさを決定します。

このバージョンの Operator new は非常にうまく機能します。これは、デフォルトの Operator new よりも航空機オブジェクトに割り当てるメモリが少なく、より高速 (おそらく 2 倍) で実行されます。これは驚くべきことではありません。汎用のデフォルト演算子 new は、さまざまなサイズのメモリ要求や内部および外部の断片化を処理する必要がありますが、演算子 new はリンクされたリスト内のポインターのペアに対してのみ動作します。多くの場合、柔軟性を優先して速度を犠牲にするのは簡単です。

次に、オペレーター削除について説明します。オペレーター削除を覚えていますか?この記事はオペレーター削除についてです。しかしこれまで、航空機クラスはオペレーターの新規を宣言するだけで、オペレーターの削除を宣言していませんでした。次のコードを書いたらどうなるかを考えてみましょう:

airplane *pa = newplane; // Call
// plain::operator new
...

delete pa; // Call::operator delete

このコードを読むときに耳を澄ますと、飛行機が墜落して燃える音が聞こえ、プログラマーが泣いているのが聞こえます。問題は、演算子 new (飛行機で定義されているもの) はヘッダー情報なしでメモリへのポインタを返すのに対し、演算子 delete (デフォルトのもの) は渡されたメモリにヘッダー情報が含まれていると想定していることです。これが悲劇の原因だ。

この例は、一般原則を示しています。異なる仮定が発生しないように、演算子 new と演算子 delete は同時に記述する必要があります。独自のメモリ割り当てプログラムを作成する場合は、解放プログラムも作成する必要があります。 (このルールに従わなければならない別の理由については、オブジェクトの数に関する記事の配置セクションのサイドバーを参照してください)

したがって、引き続き次のように飛行機クラスを設計します:

classplane { // 追加された点を除き、前と同じです。 a
public: // 演算子の宣言 delete
...

static void Operator delete(void *deadobject,
size_t size);

};

// 演算子 delete に渡されるのはメモリ ブロックです。
// そのサイズは正しいので、空きメモリブロックリストの先頭に追加します
//
voidplane::operator delete(void *deadobject,

size_t size)
{
if (deadobject == 0) return ; // 8 項を参照

if (size != sizeof(airplane)) { // 8 節を参照
::operator delete(deadobject);
return;
}

airplane *carcass =
static_cast(deadobject);

carcass->next = headoffreelist;
headoffreelist = carcass;
}

「間違った」サイズのリクエストが、operator new のグローバル オペレーター new に転送されたため (項目 8 を参照)、「間違った」サイズのオブジェクトもグローバル オペレーターに渡される必要があります。ここで演算子を削除して処理します。これを行わないと、以前に回避しようと努めた問題、つまり new と delete の間の構文の不一致が再現されてしまいます。

興味深いことに、削除されるオブジェクトが仮想デストラクターのないクラスから継承されている場合、operator delete に渡される size_t 値が正しくない可能性があります。これが、基底クラスが仮想デストラクターを持たなければならない理由であり、項目 14 には 2 つ目の、より説得力のある理由がリストされています。ここで、基本クラスが仮想コンストラクターを省略した場合、演算子の削除が正しく機能しない可能性があることを覚えておいてください。

すべて順調ですが、あなたの眉をひそめている様子から、メモリ リークを心配していることがわかります。開発経験が豊富であれば、航空機の Operator new がメモリの大部分を取得するために ::operator new を呼び出しますが、航空機の Operator delete ではメモリが解放されないことに必ず気づくでしょう。メモリーリーク!メモリーリーク!あなたの頭の中で警鐘が鳴っているのがはっきりと聞こえます。

しかし、私の答えを注意深く聞いてください、ここにはメモリリークはありません!

メモリリークの原因は、メモリ割り当て後にメモリを指すポインタが失われることです。ガベージ処理や言語外のその他のメカニズムがなければ、このメモリは再利用されません。ただし、上記の設計ではメモリ ポインタが失われることがないため、メモリ リークは発生しません。各大きなメモリ ブロックはまず航空機サイズのチャンクに分割され、次にこれらのチャンクがフリー リンク リストに配置されます。クライアントが plain::operator new を呼び出すと、小さいブロックが自由リンク リストから削除され、クライアントは小さいブロックへのポインターを取得します。クライアントがオペレータ削除を呼び出すと、小さなブロックは空きリストに戻されます。この設計では、すべてのメモリ ブロックは航空機オブジェクトによって使用されるか (この場合、メモリ リークを回避するのはクライアントの責任です)、またはメモリ ブロックは空きリンク リスト上にあります (この場合、メモリ ブロックはポインタを持ちます)。したがって、ここではメモリリークは発生しません。

ただし、::operator new によって返されたメモリ ブロックが、aircraft::operator delete によって解放されたことがないことは事実です。このメモリ ブロックには、メモリ プールと呼ばれる名前が付いています。ただし、メモリ リークとメモリ プールの間には重要な違いがあります。クライアントが正常に動作する場合でも、メモリ リークは無限に増加する可能性があります。メモリ プールのサイズがクライアントによって要求された最大メモリ量を超えることはありません。

::operator new によって返されたメモリ ブロックが使用されないときに自動的に解放されるように航空機のメモリ管理プログラムを変更することは難しくありませんが、ここではこれを行いません。これには 2 つの理由があります。1 つ目の理由。そして、ガバナンスの本来の意図に関連するカスタムの記憶。メモリ管理をカスタマイズする理由はたくさんありますが、その最も基本的な理由は、デフォルトの演算子 new および演算子 delete がメモリを多量に使用する、または実行が遅いと確信していることです。これらの大きなメモリ ブロックを追跡して解放するために追加のバイトが書き込まれるたびに、また追加のステートメントが書き込まれるたびに、メモリ プール戦略を使用する場合よりもソフトウェアの実行が遅くなり、より多くのメモリが使用されます。高いパフォーマンス要件を備えたライブラリまたはプログラムを設計する場合、メモリ プールのサイズが妥当な範囲内に収まると予想される場合は、メモリ プール方式を使用するのが最善です。

2 番目の理由は、いくつかの不合理なプログラム動作への対処に関連しています。航空機のメモリ管理プログラムが変更されていると仮定すると、航空機のオペレータがオブジェクトを削除すると、オブジェクトが存在しない大きなメモリ チャンクが解放される可能性があります。次に、次のプログラムを見てください: int main()
{
airplane *pa = new airplane; // 最初の割り当て: 大きなメモリ ブロックを取得します。
// 空きリンク リストなどを生成します。

delete pa; / メモリ ブロックは空です ;
// 解放してください

pa = 新しい飛行機; // 大きなメモリ ブロックを再度取得します、
// 空きリンク リストを生成します、お待ちください

delete pa; // メモリ ブロックはまた空です、
//

を解放してください... // アイデアがあります...

return 0;
}

このひどい小さなプログラムは、デフォルトの演算子 new と演算子 delete で書かれたプログラムよりも実行が遅くなります。より多くのメモリを消費し、より多くのメモリを使用します。メモリ プールを使用して作成されたプログラムと比較しないでください。

もちろん、この不合理な状況に対処する方法はありますが、異常な状況を考慮すればするほど、メモリ管理機能を再実装する必要が生じる可能性が高くなります。最終的に何が得られるでしょうか?メモリ プールはメモリ管理の問題をすべて解決できるわけではありませんが、多くの場合に適しています。

実際の開発では、多くの場合、さまざまなクラスに対してメモリ プール ベースの関数を実装する必要があります。 「この固定サイズのメモリ アロケータを便利に使用できるようにカプセル化する何らかの方法があるに違いない」と考えるでしょう。はい、方法はあります。私はこの条項について長い間こだわってきましたが、それでも簡単に紹介し、具体的な実装については読者の演習として残しておきたいと思います。

以下は、プール クラスのシンプルで最小限のインターフェイスです (項目 18 を参照)。プール クラスの各オブジェクトは、特定のタイプのオブジェクト (そのサイズはプールのコンストラクターで指定されます) のメモリ アロケーターです。

class pool {
public:
pool(size_t n); // サイズ n のオブジェクトの
を作成します。// アロケータ


void * alloc(size_t n); // オブジェクトに十分なメモリを割り当てます
// 項目 8 の演算子の新しい規則に従います

void free( void *p, size_t n); p が指すメモリはメモリ プールに返されます
// 第 8 項の演算子削除規則に従います

~pool() // メモリ プール内のすべてのメモリを解放します

};プール オブジェクトの作成と実行 操作の割り当てと割り当て解除、および破棄。プール オブジェクトが破棄されると、そのオブジェクトによって割り当てられたすべてのメモリが解放されます。これは、航空機の機能によって示されるメモリ リークのような動作を回避する方法が存在することを意味します。ただし、これは、プールのデストラクターの呼び出しが早すぎる場合 (メモリ プールを使用しているすべてのオブジェクトが破棄されるわけではありません)、一部のオブジェクトは、使用しているメモリが突然なくなったことに気づくことも意味します。多くの場合、結果は予測できません。

このプール クラスを使用すると、Java プログラマでも独自のメモリ管理関数を飛行機クラスに簡単に追加できます:

classplane {
public:

... // 通常の飛行機関数

static void *operator new(size_t size);
static void Operator delete(void *p, size_t size);


private:
airplanerep *rep; // 実際の記述へのポインタ
static pool mempool // 飛行機のメモリ Pool

};

inline void *plane::operator new(size_t size)
{ return mempool.alloc(size); }

inline void plain::operator delete(void *p,
size_t size)
{ mempool.free(p, size); }

// 航空機オブジェクトのメモリ プールを作成します。
//
pool plain::mempool(sizeof(airplane)) を実装します。

この設計は、前の設計よりもはるかに明確でクリーンです。航空機クラスが航空機以外のコードと混ざることはなくなりました。共用体、空きリンク リストのヘッド ポインタ、および元のメモリ ブロックのサイズを定義する定数はすべて、プール クラス内の本来あるべき場所に隠されています。メモリ管理の詳細については、プールを作成するプログラマに任せてください。あなたの仕事は、航空機クラスを適切に動作させることだけです。

カスタム メモリ管理プログラムによってプログラムのパフォーマンスが大幅に向上し、プールなどのクラスにカプセル化できることが理解できたはずです。ただし、重要な点を忘れないでください。演算子 new と演算子削除は同時に動作する必要があります。そのため、演算子 new を作成する場合は、演算子 delete も記述する必要があります。

上記は C++ メモリ管理の詳細な説明です。その他の関連記事については、PHP 中国語 Web サイト (www.php.cn) に注目してください。

関連ラベル:
ソース:php.cn
このウェブサイトの声明
この記事の内容はネチズンが自主的に寄稿したものであり、著作権は原著者に帰属します。このサイトは、それに相当する法的責任を負いません。盗作または侵害の疑いのあるコンテンツを見つけた場合は、admin@php.cn までご連絡ください。
最新の問題
人気のチュートリアル
詳細>
最新のダウンロード
詳細>
ウェブエフェクト
公式サイト
サイト素材
フロントエンドテンプレート