delete []はどのようにしてそれが配列であることを認識しますか?


136

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

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

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

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

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

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

OSは、intを1つだけ削除し、そのポイントを超えて残りのメモリを削除することで、ある種の「強制終了」を行わないほどスマートです(それとstrlen\0終端されていない文字列とは対照的です-それまでは続きます)ヒット0)。

では、これらのことを覚えておくことは誰の仕事なのでしょうか。OSは何らかのタイプの記録をバックグラウンドで保持しますか?(つまり、何が起きるかは未定義であると言ってこの投稿を始めたことを私は理解していますが、実際、「殺害酒宴」シナリオは発生しないため、実際の世界では誰かが覚えています。)



6
削除後の角括弧からわかる
JoelFan

「ポインタは配列です」。いいえ、ポインターは配列にはなりません。多くの場合、配列の最初の要素を指しますが、それは別のことです。
Aaron McDaid

回答:


99

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

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


14
実際、delete []を使用してnewで作成されたものを削除することは悪用可能です。taossa.com/index.php/2007/01/03/…–
ロドリゴ

23
あなたのコメント内のリンクが壊れている@Rodrigo、ありがたいウェイバックマシンは、そのコピーがあるreplay.web.archive.org/20080703153358/http://taossa.com/...を
デビッド・ガードナー

103

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

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

つまり、コードが単純に

Foo* foo = new Foo;

そのため、割り当てられるメモリ空間には、のfoo配列をサポートするために必要となる余分なオーバーヘッドを含めないでくださいFoo

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

delete[] bar;

ただの代わりに

delete bar;

barが配列へのポインタの場合。

ほとんどの人(私も含めて)にとって、数バイトのメモリの余分な煩わしさは最近では風変わりなようです。しかし、数バイト(非常に多くのメモリブロックになる可能性があるものから)のバイト数を節約することが重要になる状況がまだいくつかあります。


20
「最近の数バイトのメモリの乱れは古臭いようです」。幸いなことに、そのような人々にとって、裸の配列も古風に見え始めているので、ベクトルまたはboost :: arrayを使用するだけで、delete []を永久に忘れることができます:-)
Steve Jessop

28

はい、OSはいくつかのことを「バックグラウンド」に保ちます。たとえば、実行すると

int* num = new int[5];

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

割り当てのサイズを追跡する方法は他にもあると思いますが、それは1つの選択肢です。


26
+1-通常、言語ランタイムがOSではなくこのメタデータを格納することを除いて、有効なポイント。
シャープトゥース2009

配列のサイズまたは配列が定義されているオブジェクトのサイズはどうなりますか?そのオブジェクトでsizeofを実行すると、追加の4バイトが表示されますか?
Shree

3
いいえ、sizeofは配列のサイズのみを示します。 ランタイムが、私が説明した方法で実装することを選択した場合、それは厳密に実装の詳細であり、ユーザーの観点からは、マスクする必要があります。ポインタの前のメモリはユーザーに「属して」おらず、カウントされません。
bsdfish

2
さらに重要なことに、sizeofは、動的に割り当てられた配列の実際のサイズを返すことはありません。コンパイル時に既知のサイズのみを返すことができます。
bdonlan 2011

このメタデータをforループで使用して、配列を正確にループすることはできますか?例 for(int i = 0; i < *(arrayPointer - 1); i++){ }
Sam

13

これはこの質問とよく似ており、探している詳細の多くが含まれています。

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

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

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

これは機能しますか?WindowsとLinuxでは、これは機能しませんでした。
2012

1
size_t size = *(reinterpret_cast<size_t *>(pArray) - 1)代わりに試してください

9

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

とにかく、ミキシングnew/new[]delete/delete[]はおそらくUBです。


1
明確で短く、最も役立つ答え!
GntS 2017年

6

それが配列であることを認識していないためdelete[]、通常のoldではなく指定する必要がありますdelete


5

これと同じような質問がありました。Cでは、malloc()(または他の同様の関数)を使用してメモリを割り当て、free()を使用してそれを削除します。malloc()は1つだけあり、特定のバイト数を割り当てるだけです。free()は1つだけあり、パラメーターとしてポインターを使用します。

では、なぜCではポインタを解放するだけでよいのに、C ++ではそれが配列か単一変数かを通知する必要があるのはなぜですか。

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

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

classes = new MyClass[3];

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

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


3

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

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


3

コンパイラのアプローチの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)

1

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

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

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


1

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

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

追加のストレージが割り当てられる場合の完全な仕様については、C ++ ABI(コンパイラーの実装方法)を参照してください:Itanium C ++ ABI:配列演算子の新しいCookie


私は、すべてのコンパイラーが文書化されたC ++用のABIをいくつか監視したことを望みます。以前にアクセスしたリンクの+1。ありがとう。
Don Wakefield、


0

「未定義の動作」とは、言語仕様が何が起こるかを保証しないことを意味します。それは本質的に何か悪いことが起こるという意味ではありません。

では、これらのことを覚えておくことは誰の仕事なのでしょうか。OSは何らかのタイプの記録をバックグラウンドで保持しますか?(つまり、何が起こるかは未定義であると言ってこの投稿を始めたことを私は理解していますが、実際、「殺害酒宴」シナリオは発生しないため、実際の世界では誰かが覚えています。)

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

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

C ++の実装は通常、配列のサイズを記憶するだけです。それは、それ自体の目的のためにそうする必要がある場合のみです。これは、通常、型に非自明なデストラクタがあるためです。

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

free(p)

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

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

一方、「[]を削除」は次のようになります。

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);

-1

オブジェクトの配列を反復処理し、オブジェクトごとにデストラクタを呼び出します。私はこの簡単なコード魔女を作成し、new []およびdelete []式をオーバーロードし、メモリの割り当てを解除するテンプレート関数を提供し、必要に応じて各オブジェクトのデストラクタを呼び出します。

// overloaded new expression 
void* operator new[]( size_t size )
{
    // allocate 4 bytes more see comment below 
    int* ptr = (int*)malloc( size + 4 );

    // set value stored at address to 0 
    // and shift pointer by 4 bytes to avoid situation that
    // might arise where two memory blocks 
    // are adjacent and non-zero
    *ptr = 0;
    ++ptr; 

    return ptr;
}
//////////////////////////////////////////

// overloaded delete expression 
void static operator delete[]( void* ptr )
{
    // decrement value of pointer to get the
    // "Real Pointer Value"
    int* realPtr = (int*)ptr;
    --realPtr;

    free( realPtr );
}
//////////////////////////////////////////

// Template used to call destructor if needed 
// and call appropriate delete 
template<class T>
void Deallocate( T* ptr )
{
    int* instanceCount = (int*)ptr;
    --instanceCount;

    if(*instanceCount > 0) // if larger than 0 array is being deleted
    {
        // call destructor for each object
        for(int i = 0; i < *instanceCount; i++)
        {
            ptr[i].~T();
        }
        // call delete passing instance count witch points
        // to begin of array memory 
        ::operator delete[]( instanceCount );
    }
    else
    {
        // single instance deleted call destructor
        // and delete passing ptr
        ptr->~T();
        ::operator delete[]( ptr );
    }
}

// Replace calls to new and delete
#define MyNew ::new
#define MyDelete(ptr) Deallocate(ptr)

// structure with constructor/ destructor
struct StructureOne
{
    StructureOne():
    someInt(0)
    {}
    ~StructureOne() 
    {
        someInt = 0;
    }

    int someInt;
};
//////////////////////////////

// structure without constructor/ destructor
struct StructureTwo
{
    int someInt;
};
//////////////////////////////


void main(void)
{
    const unsigned int numElements = 30;

    StructureOne* structOne = nullptr;
    StructureTwo* structTwo = nullptr;
    int* basicType = nullptr;
    size_t ArraySize = 0;

/**********************************************************************/
    // basic type array 

    // place break point here and in new expression
    // check size and compare it with size passed 
    // in to new expression size will be the same
    ArraySize = sizeof( int ) * numElements;

    // this will be treated as size rather than object array as there is no 
    // constructor and destructor. value assigned to basicType pointer
    // will be the same as value of "++ptr" in new expression
    basicType = MyNew int[numElements];

    // Place break point in template function to see the behavior
    // destructors will not be called and it will be treated as 
    // single instance of size equal to "sizeof( int ) * numElements"
    MyDelete( basicType );

/**********************************************************************/
    // structure without constructor and destructor array 

    // behavior will be the same as with basic type 

    // place break point here and in new expression
    // check size and compare it with size passed 
    // in to new expression size will be the same
    ArraySize = sizeof( StructureTwo ) * numElements;

    // this will be treated as size rather than object array as there is no 
    // constructor and destructor value assigned to structTwo pointer
    // will be the same as value of "++ptr" in new expression
    structTwo = MyNew StructureTwo[numElements]; 

    // Place break point in template function to see the behavior
    // destructors will not be called and it will be treated as 
    // single instance of size equal to "sizeof( StructureTwo ) * numElements"
    MyDelete( structTwo );

/**********************************************************************/
    // structure with constructor and destructor array 

    // place break point check size and compare it with size passed in
    // new expression size in expression will be larger by 4 bytes
    ArraySize = sizeof( StructureOne ) * numElements;

    // value assigned to "structOne pointer" will be different 
    // of "++ptr" in new expression  "shifted by another 4 bytes"
    structOne = MyNew StructureOne[numElements];

    // Place break point in template function to see the behavior
    // destructors will be called for each array object 
    MyDelete( structOne );
}
///////////////////////////////////////////

-2

クラス内でデストラクタを定義し、両方の構文でコードを実行するだけです

delete pointer

delete [] pointer

出力に応じて、uは解を見つけることができます


配列型を新規作成する場合は、delete []を使用してください。たとえば、int * a = new int; int * b = new int [5]; 削除a; delete [] b;
Lineesh K Mohan 2017

-3

答え:

int * pArray = new int [5];

intサイズ= *(pArray-1);

上記の投稿は正しくなく、無効な値を生成します。"-1"は要素をカウントします64ビットWindows OSでは、正しいバッファーサイズはPtr-4バイトのアドレスにあります。

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.