29

オブジェクトの実際のメモリ位置とは異なる基本クラスポインタが与えられたときに、削除演算子が解放する必要のあるメモリ位置をどのように把握するかを知りたいです。

この動作を自分のカスタムアロケータ/デアロケータに複製したいと思います。

次の階層について考えてみます。

struct A
{
    unsigned a;
    virtual ~A() { }
};

struct B
{
    unsigned b;
    virtual ~B() { }
};

struct C : public A, public B
{
    unsigned c;
};

タイプCのオブジェクトを割り当て、タイプBのポインターを介して削除したいと思います。これは、演算子deleteの有効な使用法であり、Linux/GCCで機能します。

C* c = new C;
B* b = c;

delete b;

興味深いのは、オブジェクトがメモリ内にどのように配置されているかにより、ポインタ「b」と「c」が実際には異なるアドレスを指していることと、削除演算子が正しいメモリ位置を見つけて解放する方法を「知っている」ことです。

一般に、基本クラスのポインターが与えられた場合、ポリモーフィックオブジェクトのサイズを見つけることはできないことを私は知っています。ポリモーフィックオブジェクトのサイズを見つけてください。オブジェクトの実際のメモリ位置を見つけることも一般的には不可能だと思います。

ノート:

4

7 に答える 7

14

これは明らかに実装固有です。実際には、物事を実装するための賢明な方法は比較的少数です。概念的には、ここにいくつかの問題があります。

  1. 最も派生したオブジェクト、つまり(概念的に)他のすべてのタイプを含むオブジェクトへのポインターを取得できる必要があります。

    dynamic_cast標準のC++では、 :を使用してこれを行うことができます。

    void *derrived = dynamic_cast<void*>(some_ptr);
    

    たとえば、次のようになりますC*B*

    #include <iostream>
    
    struct A
    {
        unsigned a;
        virtual ~A() { }
    };
    
    struct B
    {
        unsigned b;
        virtual ~B() { }
    };
    
    struct C : public A, public B
    {
        unsigned c;
    };
    
    int main() {
      C* c = new C;
      std::cout << static_cast<void*>(c) << "\n";
      B* b = c;
      std::cout << static_cast<void*>(b) << "\n";
      std::cout << dynamic_cast<void*>(b) << "\n";
    
      delete b;
    }
    

    私のシステムに以下を与えました

    0x912c008
    0x912c010
    0x912c008
    
  2. それが完了すると、それは標準のメモリ割り当て追跡の問題になります。通常、これは2つの方法のいずれかで行われます。a)割り当てられたメモリの直前に割り当てのサイズを記録し、サイズを見つけるのは単なるポインタの減算です。またはb)ある種のデータ構造で割り当てとメモリを解放します。詳細については、この質問を参照してください。

    glibcを使用すると、特定の割り当てのサイズをかなり賢明に照会できます。

    #include <iostream>
    #include <stdlib.h>
    #include <malloc.h>
    
    int main() {
      char *test = (char*)malloc(50);
      std::cout << malloc_usable_size(test) << "\n";
    }
    

    その情報は、同様に解放/削除するために利用可能であり、返されたメモリのチャンクをどう処理するかを理解するために使用されます。

の実装の正確な詳細は、malloc_useable_sizemalloc/malloc.cのlibcソースコードに記載されています。

(以下は、Colin Plumbによる軽く編集された説明を含みます。)

メモリのチャンクは、KnuthやStandishなどで説明されている「境界タグ」方式を使用して維持されます。(このような手法の調査については、Paul Wilson ftp://ftp.cs.utexas.edu/pub/garbage/allocsrv.psの論文を参照してください。)空きチャンクのサイズは、各チャンクの前と終わり。これにより、断片化されたチャンクをより大きなチャンクに非常に高速に統合できます。サイズフィールドには、チャンクが空きか使用中かを表すビットも含まれます。

割り当てられたチャンクは次のようになります。

    チャンク->+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -+-+-+-+-+-+-+-+-+
            | 割り当てられている場合、前のチャンクのサイズ| |
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+
            | チャンクのサイズ(バイト単位)| M | P |
      mem-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -+-+-+-+-+-+-+-+-+  
            | ユーザーデータはここから始まります...。  
            。。  
            。(malloc_usable_size()バイト)。  
            。|   
nextchunk-> +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ -+-+-+-+-+-+-+-+-+     
            | チャンクのサイズ|  
            +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+- +-+-+-+-+-+-+-+  
于 2012-07-31T17:24:33.083 に答える
10

基本クラスポインタを破棄するには、仮想デストラクタを実装している必要があります。そうしなかった場合、すべての賭けは無効になります。

呼び出される最初のデストラクタは、仮想メカニズム(vtable)によって決定される最も派生したオブジェクトのデストラクタになります。このデストラクタはオブジェクトのサイズを知っています!その情報をどこかでリスしたり、デストラクタのチェーンに渡したりすることができます。

于 2012-07-31T17:23:58.437 に答える
7

その実装は定義されていますが、一般的な実装手法の1つは、operator delete(が含まれるコードではなくdelete)デストラクタによって実際に呼び出され、呼び出されるかどうかを制御するデストラクタへの隠しパラメータがありますoperator delete

この実装では、デストラクタへのほとんどの呼び出し(すべての明示的なdtor呼び出し、自動変数と静的変数の呼び出し、および派生デストラクタからのベースデストラクタへの呼び出し)では、余分な非表示引数がfalseに設定されます(したがって、演算子deleteは呼び出されません)。ただし、削除式がある場合は、非表示の引数がtrueのオブジェクトの最上位デストラクタを呼び出します。あなたの例では、これはC ::〜C()になるので、オブジェクト全体のメモリを再利用することがわかります。

于 2012-07-31T17:26:03.633 に答える
1

通常の実装(理論的には他にもある可能性がありますが、実際にはあるとは思えません)は、ベースオブジェクトごとにvtableがあることです(そうでない場合、ベースオブジェクトは多形ではなく、削除に使用できません)。そのvtableには、仮想関数へのポインターだけでなく、現在のオブジェクトから最も派生したオブジェクトへのオフセットを含む、RTTI全体に必要なものも含まれています。

説明するために(実際の実装にはおそらく違いがあり、いくつかのエラーが発生した可能性があります)、実際に使用されるものは次のとおりです。

struct A_VTable_Desc {
   int offset;
   void* (destructor)();
} AVTable = { 0, A::~A };

struct A_impl {
   unsigned a;
   A_VTable_Desc* vptr;
};

struct B_VTable_Desc {
   int offset;
   void* (destructor)();
} BVtable = { 0, &B::~B };

struct B_impl {
   unsigned b;
   B_VTable_Desc* __vptr;
};

A_VTable_Desc CAVtable = { 0, &C::~C_as_A };
B_VTable_Desc CBVtable = { -8, &C::~C_as_B };

struct C {
   A_impl __aimpl;
   B_impl __bimpl;
   unsigned c;
};

そしてCのコンストラクターは暗黙のうちに次のようなことをします

this->__aimpl->__vptr = &CAVtable;
this->__bimpl->__vptr = &CBVtable;
于 2012-07-31T17:45:06.967 に答える
1

演算子をコンパイルするときdelete、コンパイラは、デストラクタの実行後に呼び出す「割り当て解除」関数を決定する必要があります。デストラクタは、割り当て解除の呼び出しとは直接関係がありませんが、コンパイラが割り当て解除関数を検索する方法に影響を与えることに注意してください。

通常の場合、オブジェクトのタイプ固有の割り当て解除関数はありません。この場合、グローバル割り当て解除関数が使用され、常に暗黙的に宣言されます(C ++ 03 3.7.3 / 2)。

void operator delete(void*) throw();

この関数はサイズ引数さえも取らないことに注意してください。ポインタの値だけに基づいて割り当てサイズを決定する必要があります。これは、アドレスの直前に割り当てのサイズを保存することで実行できます(他の方法でそれを行う実装はありますか?)。

ただし、その割り当て解除関数を使用することを決定する前に、コンパイラーは、タイプ固有の割り当て解除関数を使用する必要があるかどうかを確認するためにルックアップを実行します。その関数は、単一のパラメーター(a void*)または2つのパラメーター(avoid*とa size_t)のいずれかを持つことができます。

割り当て解除関数を検索するときに、オペランドとして使用されるポインターの静的タイプにdelete仮想デストラクタがある場合、(C ++ 03 12.5 / 4):

割り当て解除関数は、動的タイプの仮想デストラクタの定義でルックアップによって検出された関数です。

実際には、実際の関数は仮想デストラクタである必要がありますが、実際には、operator delete()割り当て解除関数は仮想デストラクタを持つタイプに対して仮想ですstatic(標準では12.5 / 7でこれに注意しています)。この場合、コンパイラはオブジェクトの動的型にアクセスできるため、必要に応じてオブジェクトのサイズを渡すことができます(オブジェクトポインタへの必要な調整は同じ方法で見つけることができます)。

オペランドの静的タイプがdelete静的である場合、割り当て解除関数のルックアップoperator delete()は通常のルールに従います。繰り返しになりますが、コンパイラがサイズパラメータを必要とする割り当て解除関数を選択した場合、コンパイル時にオブジェクトの静的タイプを認識しているため、これを実行できます。

最終的な状況は、未定義の動作をもたらす状況です。ポインターの静的型に仮想デストラクタがなく、派生型オブジェクトを指している場合、コンパイラは誤った割り当て解除関数を検索し、誤ったサイズを渡す可能性があります。しかし、それは未定義の動作の結果であるため、問題ではありません。

于 2012-07-31T18:54:25.423 に答える
0

これは、mallocと同じ方法で実行できます。一部のmallocは、オブジェクト自体の直前のサイズを記録します。最新のmallocのほとんどは、はるかに洗練されています。同じサイズのオブジェクトをページ上にまとめて保持する高速アロケータであるtcmallocを参照してください。これにより、ページの粒度でサイズ情報のみを保持する必要があります。

于 2012-07-31T17:27:10.830 に答える
0

ポリモーフィックオブジェクトへのポインタは、通常、オブジェクトと、オブジェクトの基になるクラスに関する情報を含む仮想テーブルへのポインタとして実装されます。deleteはこれらの実装の詳細を認識し、適切なデストラクタを見つけます

于 2012-07-31T17:24:53.733 に答える