web-dev-qa-db-ja.com

動的Cスタイル配列のサイズの取得と、delete []の使用の比較。矛盾?

C++では、そのメモリのチャンクを指すポインターから動的配列のサイズを取得することは不可能であるということをどこでも読みました。

動的配列のサイズをポインターだけから取得する方法がないこと、また、ポインターなしでdelete []を使用して割り当てられたすべてのメモリを解放することはできますか?配列サイズを指定する必要がありますか?

delete []は配列のサイズを知っている必要がありますよね?したがって、この情報はどこかに存在している必要があります。そうじゃないの?

私の推論の何が問題になっていますか?

36

TL; DR演算子delete[]はオブジェクトを破棄し、メモリの割り当てを解除します。破壊には情報N(「要素数」)が必要です。割り当て解除には、情報S(「割り当てられたメモリのサイズ」)が必要です。 Sは常に格納され、コンパイラー拡張機能によって照会できます。 Nは、オブジェクトの破棄にデストラクタの呼び出しが必要な場合にのみ格納されます。 Nが格納される場合、格納される場所は実装に依存します。


演算子delete []は、次の2つのことを行う必要があります。

a)オブジェクトを破棄する(必要に応じてデストラクタを呼び出す)および

b)メモリの割り当てを解除します。

まず、(de)allocationについて説明しましょう。これは、多くのコンパイラーによってC関数mallocおよびfreeに委任されています( GCCのように)。関数mallocは、割り当てられるバイト数をパラメーターとして取り、ポインターを返します。関数freeはポインターのみを受け取ります。バイト数は必要ありません。つまり、メモリ割り当て関数は、割り当てられたバイト数を追跡​​する必要があります。割り当てられているバイト数を照会する関数がある可能性があります(Linuxでは malloc_usable_size で、Windowsでは _msize で実行できます) 。 これはあなたが望むものではありませんこれはしないがあなたにサイズを教えてくれるからです配列の割り当てられたメモリの量。 mallocは必ずしも要求されたメモリを提供するわけではないため、malloc_usable_sizeの結果から配列サイズを計算することはできません。

#include <iostream>
#include <malloc.h>

int main()
{
    std::cout << malloc_usable_size(malloc(42)) << std::endl;
}

この例では、42ではなく56になります。 http://cpp.sh/2wdm4

newの結果にmalloc_usable_size(または_msize)を適用すると、未定義の動作になることに注意してください。

それでは、オブジェクトのconstructiondestructionについて説明しましょう。ここでは、delete(単一オブジェクトの場合)とdelete[](配列の場合)の2つの削除方法があります。 C++の非常に古いバージョンでは、配列のサイズをdelete[]- operatorに渡す必要がありました。ご存知のように、今日ではそうではありません。コンパイラはこの情報を追跡します。 GCCは配列の先頭の前に小さなフィールドを追加します。配列のサイズは、デストラクタを呼び出す必要がある頻度がわかるように格納されています。あなたはそれをクエリするかもしれません:

#include <iostream>

struct foo {
    char a;
    ~foo() {}
};

int main()
{
    foo * ptr = new foo[42];
    std::cout << *(((std::size_t*)ptr)-1) << std::endl;
}

このコードは42を提供します: http://cpp.sh/7mbqq

プロトコルのみ:これは未定義の動作ですが、GCCの現在のバージョンでは動作します。

したがって、この情報を照会する機能がない理由を自問するかもしれません。その答えは、GCCが常にこの情報を保存するわけではないということです。オブジェクトの破棄が操作なしである場合があります(そしてコンパイラーはそれを理解することができます)。次の例を考えてみます。

#include <iostream>

struct foo {
    char a;
    //~foo() {}
};

int main()
{
    foo * ptr = new foo[42];
    std::cout << *(((std::size_t*)ptr)-1) << std::endl;
}

ここでは、答えはではありません42これ以上: http://cpp.sh/2rzfb

答えは単なるごみです-コードは再び未定義の動作でした。

どうして?コンパイラはデストラクタを呼び出す必要がないため、情報を格納する必要はありません。そして、はい、この場合、コンパイラーは、作成されたオブジェクトの数を追跡するコードを追加しません。割り当てられたバイト数(56になる可能性がある、上記を参照)のみが既知です。

29
Handy999

アロケータ、またはその背後にある実装の詳細によって、ブロックのサイズが正確にわかります。

ただし、その情報は、ユーザーまたはプログラムの「コードレイヤー」には提供されません。

言語はこれを行うように設計されているのでしょうか?承知しました!これはおそらく「使用しないものにお金を払わない」というケースです—この情報を覚えておくのはあなたの責任です。結局、あなたはあなたがどれだけのメモリを求めたか知っています!多くの場合、人々は、ほとんどの場合、それが必要とされないときに、コールスタックに渡される数値のコストを望まないでしょう。

そこにはaremalloc_usable_size Linuxおよび _msize Windowsでは、これらは、アロケータがmallocを使用し、割り当てられたブロックのサイズを最下位レベルに拡張する可能性のある他のマジックを実行しなかったと想定しています。本当に必要な場合は、自分で追跡するほうがよいと思います。または、ベクトルを使用します。

その理由は、3つの要素が合流したためだと思います。

  1. C++には「使用した分だけ支払う」という文化があります
  2. C++はCのプリプロセッサとしての生活を始めたため、Cが提供するものの上に構築する必要がありました。
  3. C++は、最も広く移植されている言語の1つです。既存のポートの運用を困難にする機能が追加されることはほとんどありません。

Cでは、プログラマーが解放するメモリブロックのサイズを指定せずにメモリブロックを解放できますが、プログラマーには、割り当てのサイズにアクセスするための標準的な方法はありません。さらに、割り当てられるメモリの実際の量は、プログラマが要求した量よりもはるかに大きくなる可能性があります。

「使用した分だけ支払う」という原則に従い、C++の実装では、型ごとに異なるnew[]を実装しています。通常、型には重要なデストラクタがあるため、必要な場合にのみサイズが格納されます。

したがって、はい、メモリブロックを解放するのに十分な情報が保存されていますが、その情報にアクセスするための健全で移植可能なAPIを定義することは非常に困難です。データ型とプラットフォームに応じて、実際に要求されたサイズが利用できる場合があります(C++実装がそれを格納する必要がある型の場合)、実際に割り当てられたサイズのみが利用できる場合があります(C++実装がそれを格納する必要がない型の場合)基盤となるメモリマネージャーに割り当てられたサイズを取得するための拡張機能があるプラットフォーム)、またはサイズがまったく使用できない場合があります(C++実装が、そこからの情報へのアクセスを提供しないプラットフォームに保存する必要がないタイプの場合)基になるメモリマネージャー)。

6
plugwash

この回答は、Microsoft Visual Studioにのみ適用されます。

_msizeと呼ばれる関数があり、malloced/calloced/reallocedされたポインターのサイズを返します。

これはmalloc.hヘッダーにあり、パラメーターは次のとおりです。

size_t _msize(
   void *memblock
);

Gccに同等のものがあるかどうかはわかりません。おそらくあるはずです。

2
Owl