web-dev-qa-db-ja.com

delete []は配列であることをどのように知るのですか?

さて、私たちは皆、次のコードで何が起こるかは未定義であることに同意していると思います。

void deleteForMe(int* pointer)
{
     delete[] pointer;
}

ポインターはあらゆる種類のものである可能性があるため、無条件のdelete[]の実行は未定義です。ただし、実際に配列ポインタを渡していると仮定しましょう。

int main()
{
     int* arr = new int[5];
     deleteForMe(arr);
     return 0;
}

私の質問は、この場合、ポインタis配列で、これを知っているのは誰ですか?つまり、言語/コンパイラの観点からは、arrが配列ポインタか単一のintへのポインタかどうかはわかりません。ちなみに、arrが動的に作成されたかどうかすらわかりません。それでも、代わりに次のことを行うと、

int main()
{
     int* num = new int(1);
     deleteForMe(num);
     return 0;
}

OSは1つのintのみを削除し、そのポイントを超えて残りのメモリを削除することにより、ある種の「殺し屋」を実行しないほどスマートです(strlenと非\0-終了文字列-0に達するまで継続します。

これらのことを覚えるのは誰の仕事ですか? OSはバックグラウンドで何らかのタイプのレコードを保持しますか? (つまり、起こったことは未定義であると言ってこの投稿を始めたことに気付きましたが、実際には「殺し屋」のシナリオは起こらないので、実際の世界では someoneは記憶しています。)

133
GRB

コンパイラは、それが配列であることを知らず、プログラマを信頼しています。 delete []を持つ単一のintへのポインターを削除すると、未定義の動作が発生します。 2番目のmain()の例は、たとえすぐにクラッシュしなくても安全ではありません。

コンパイラは、なんとかして削除する必要があるオブジェクトの数を追跡する必要があります。これは、配列サイズを格納するのに十分な過剰割り当てによってこれを行う場合があります。詳細については、 C++ Super FAQ を参照してください。

98
Fred Larson

これまでに与えられた答えが対処していないように見える1つの質問:ランタイムライブラリ(実際にはOSではない)が配列内の数を追跡できる場合、なぜdelete[]構文が必要なのかまったく?単一のdeleteフォームを使用してすべての削除を処理できないのはなぜですか?

これに対する答えは、C互換言語としてのC++のルーツに遡ります(実際にはそうではありません)。Stroustrupの哲学は、プログラマが使用していない機能にお金を払う必要はないというものでした。配列を使用していない場合は、割り当てられたメモリチャンクごとにオブジェクト配列のコストを負担する必要はありません。

つまり、コードが単に

Foo* foo = new Foo;

fooに割り当てられるメモリスペースには、Fooの配列をサポートするために必要な余分なオーバーヘッドが含まれてはなりません。

追加の配列サイズ情報を運ぶために配列の割り当てのみが設定されているため、オブジェクトを削除するときにランタイムライブラリにその情報を探すように指示する必要があります。それが使用する必要がある理由です

delete[] bar;

ただの代わりに

delete bar;

barが配列へのポインターである場合。

私たちのほとんど(私自身も含む)にとって、数バイトの余分なメモリに関する大騒ぎは、最近では奇妙に思えます。しかし、非常に多くのメモリブロックになる可能性のあるものから数バイトを節約することが重要になる場合があります。

102
Dan Breslau

はい、OSは「バックグラウンド」でいくつかのものを保持します。たとえば、実行する場合

int* num = new int[5];

oSは追加の4バイトを割り当て、割り当てられたメモリの最初の4バイトに割り当てのサイズを格納し、オフセットポインターを返します(つまり、メモリスペース1000から1024を割り当てますが、ポインターは1004を指し、位置1000-割り当てのサイズを格納する1003)。次に、deleteが呼び出されると、ポインターが渡される前の4バイトを見て、割り当てのサイズを見つけることができます。

割り当てのサイズを追跡する他の方法があると確信していますが、それは1つのオプションです。

27
bsdfish

これは this 質問に非常に似ており、あなたが探している詳細の多くがあります。

ただし、これを追跡するのはOSの仕事ではありません。配列のサイズを追跡するのは、実際にはランタイムライブラリまたは基になるメモリマネージャーです。これは通常、事前に追加のメモリを割り当て、その場所に配列のサイズを保存することで行われます(ほとんどはヘッドノードを使用します)。

これは、次のコードを実行することにより、一部の実装で表示できます

int* pArray = new int[5];
int size = *(pArray-1);
13
JaredPar

deleteまたはdelete[]は、おそらく両方とも割り当てられたメモリを解放しますが(メモリが指す)、大きな違いは、配列のdeleteは配列の各要素のデストラクタを呼び出さないことです。

とにかく、new/new[]delete/delete[]の混合はおそらくUBです。

9
Benoît

配列であることはわかりません。そのため、通常の古いdeleteの代わりにdelete[]を指定する必要があります。

6
eduffy

これに似た質問がありました。 Cでは、malloc()(または別の同様の関数)でメモリを割り当て、free()でメモリを削除します。特定のバイト数を単に割り当てるmalloc()は1つだけです。 free()は1つしかありません。これは、パラメーターとしてポインターを受け取るだけです。

では、なぜCではポインタをfreeに渡すことができるのに、C++では配列か単一変数かを伝える必要があるのでしょうか?

私が学んだ答えは、クラスのデストラクタに関係しています。

クラスMyClassのインスタンスを割り当てる場合...

classes = new MyClass[3];

Deleteで削除すると、呼び出されたMyClassの最初のインスタンスのデストラクタのみを取得できます。 delete []を使用すると、配列内のすべてのインスタンスに対してデストラクタが確実に呼び出されます。

これが重要な違いです。単純に標準型(intなど)で作業している場合、この問題は実際には表示されません。さらに、new []でdeleteおよびnew []でdelete []を使用する場合の動作は未定義です。すべてのコンパイラ/システムで同じように動作するとは限らないことに注意してください。

5
ProdigySim

コンパイラのアプローチの1つは、少し多くのメモリを割り当て、要素の数をhead要素に格納することです。

実行方法の例:ここに

int* i = new int[4];

コンパイラは、sizeof(int)* 5バイトを割り当てます。

int *temp = malloc(sizeof(int)*5)

4を最初のsizeof(int)バイトに保存します

*temp = 4;

iを設定します

i = temp + 1;

したがって、iは5ではなく4つの要素の配列を指します。

そして

delete[] i;

次の方法で処理されます

int *temp = i - 1;
int numbers_of_element = *temp; // = 4
... call destructor for numbers_of_element elements if needed
... that are stored in temp + 1, temp + 2, ... temp + 4
free (temp)
3
Avt

Freeを使用して標準Cのmallocで作成された配列を削除できるのと同じように、メモリの割り当てを担当するのはランタイム次第です。各コンパイラは、それを別々に実装すると思います。一般的な方法の1つは、配列サイズに追加のセルを割り当てることです。

ただし、ランタイムは、それが配列またはポインターであるかどうかを検出するのに十分スマートではないため、通知する必要があります。間違えた場合、正しく削除できません(たとえば、配列の代わりにptr)、または最終的にはサイズに無関係な値を取り、重大な損傷を引き起こします。

3
Uri

意味的には、C++の両方のバージョンの削除演算子は、任意のポインターを「食べる」ことができます。ただし、単一のオブジェクトへのポインタがdelete[]に与えられた場合、UBが発生します。つまり、システムクラッシュなど、何も起こらない可能性があります。

C++では、プログラマは、割り当て解除の対象(配列または単一オブジェクト)に応じて、適切なバージョンの削除演算子を選択する必要があります。

コンパイラーが削除演算子に渡されたポインターがポインター配列であるかどうかを自動的に判断できた場合、C++には削除演算子が1つしかなく、どちらの場合でも十分です。

1
mloskot

コンパイラーが配列であるかどうかを知らないことに同意します。それはプログラマー次第です。

コンパイラは、配列サイズを格納するのに十分な量を割り当てて削除する必要があるオブジェクトの数を追跡することがありますが、必ずしも必要ではありません。

追加のストレージが割り当てられるときの完全な仕様については、C++ ABI(コンパイラの実装方法)を参照してください。 Itanium C++ ABI:Array Operator new Cookies

1
shibo

「未定義の振る舞い」とは、単に言語仕様が何が起こるかについて保証しないことを意味します。それは、何か悪いことが起こるということを意味しません。

これらのことを覚えるのは誰の仕事ですか? OSはバックグラウンドで何らかのタイプのレコードを保持しますか? (つまり、何が起こるかは未定義であると言ってこの投稿を始めたことに気付きましたが、実際には「殺し屋」のシナリオは起こらないので、実際の世界では誰かが覚えています。)

通常、ここには2つのレイヤーがあります。基礎となるメモリマネージャとC++実装。

一般的に、メモリマネージャは(とりわけ)割り当てられたメモリブロックのサイズを記憶します。これは、C++実装が要求したブロックよりも大きい場合があります。通常、メモリマネージャは、割り当てられたメモリブロックの前にメタデータを保存します。

通常、C++の実装では、配列のサイズを覚えるのは、通常は型に非トライバルデストラクタがあるため、独自の目的で行う必要がある場合のみです。

したがって、取るに足らないデストラクタを持つ型の場合、「delete」と「delete []」の実装は通常同じです。 C++実装は、ポインタを基になるメモリマネージャに渡すだけです。何かのようなもの

free(p)

一方、重要なデストラクタを持つタイプの場合、「delete」と「delete []」は異なる可能性があります。 「delete」は次のようになります(Tはポインターが指す型です)

p->~T();
free(p);

「delete []」は次のようになります。

size_t * pcount = ((size_t *)p)-1;
size_t count = *count;
for (size_t i=0;i<count;i++) {
  p[i].~T();
}
char * pmemblock = ((char *)p) - max(sizeof(size_t),alignof(T));
free(pmemblock);
0
plugwash

配列にはdeleteを使用できません。また、配列以外にはdelete []を使用できません。

0
ThundrbltMN