4

deleteここでは、あるケースでは例外が発生するのに、他のケースでは例外が発生しないのはなぜだろうか。

特例なし

#include <iostream>
using namespace std;

class A
{
public:
    ~A() { cout << "A dtor" << endl; }
};

class B : public A
{
public:
    int x;
    ~B() { cout << "B dtor" << endl; }
};


A* f() { return new B; }

int _tmain(int argc, _TCHAR* argv[])
{
    cout << sizeof(B) << " " << sizeof(A) << endl;
    A* bptr= f();
    delete bptr;
}

ここでの出力は です。これは4 1 .. A dtor、A の ID が 1 バイトであり、B の ID が 4 であるためですint x

例外ケース

#include <iostream>
using namespace std;

class A
{
public:
    ~A() { cout << "A dtor" << endl; }
};

class B : public A
{
public:
    virtual ~B() { cout << "B dtor" << endl; }
};


A* f() { return new B; }

int _tmain(int argc, _TCHAR* argv[])
{
    cout << sizeof(B) << " " << sizeof(A) << endl;
    A* bptr= f();
    delete bptr;
}

ここで、出力は です。これは4 1 .. A dtor、A の ID が 1 バイトであり、B がvptr仮想デストラクタに必要なため 4 バイトであるためです。 しかし、デバッグ アサーションはdeletecall( _BLOCK_TYPE_IS_VALID)内で失敗します。

環境

Visual Studio 2010 SP1Rel で Windows 7 を実行しています。

4

2 に答える 2

3

この投稿を見る

簡単な要約:

  • A のインスタンスを削除するようにマシンに指示しています
  • これはポインター/参照を介して呼び出すクラスなので、仮想テーブル (VT) を使用する必要がありますか?
  • A には仮想メンバーがないため、VT は使用されません。
  • A の標準デストラクタを呼び出します…</li>
  • バン!クラス A を削除しようとしていますが、ポインタによって、A が知らなかった VT を含む B のオブジェクトが表示されることがあります。sizeof(A) は 1 (私の知る限り、サイズを 0 にすることは合法ではないため) であり、sizeof(B) は 4 (VT が存在するため) です。1 バイトを削除したいのですが、4 バイトのブロックがあります。DEBUG ヒープ監視により、エラーがキャッチされました。

Aもちろん、解決策は基本クラス ( )dtorを宣言することです。virtualしたがって、は常に呼び出されます。Bdtor

編集:最初のケースでは、標準が言わなければならないことは次のとおりです。

§5.3 最初の選択肢 (オブジェクトの削除) で、削除するオブジェクトの静的型が動的型と異なる場合、静的型は削除するオブジェクトの動的型と静的型の基底クラスでなければならないは仮想デストラクタを持たなければなりません。そうしないと、動作が未定義になります。2 番目の選択肢 (配列の削除) では、削除するオブジェクトの動的な型がその静的な型と異なる場合、動作は未定義です。

したがって、どちらの場合も、もちろん実装ごとに異なる未定義の動作の領域につながります。しかし、ほとんどの実装では、難解なアンチパターンである 2 番目のケースよりも、最初のケースの方が扱いやすく、少なくとも熟考しやすいのは当然のことです。

于 2013-02-16T12:52:52.410 に答える
1

他の人が指摘しているように、静的型が動的型とは異なるオブジェクトを削除しています。静的型には仮想デストラクタがないため、未定義の動作が発生します。これには、ご覧のように動作する場合と動作しない場合があるという動作が含まれます。ただし、特定のコンパイラで何が起こっているのかをもう少し深く理解することに興味があると思います。

クラスAにはメンバーがまったくないため、そのデータ レイアウトは次のようになります。

struct A {
};

classは classBから派生するためA、 classはAB に埋め込まれます。 classBに仮想関数がない場合、レイアウトは次のようになります。

struct B {
  A __a_part;
  int x;
};

コンパイラは、あたかもコンパイラが次のような関数を持っているかのように、のアドレスを取得するだけでaB*を anに変換できます。A*__a_part

A* convertToAPointer(B* bp) { return &bp->__a_part; }

__a_partは の最初のメンバーなのでBB*A*は同じアドレスを指します。

次のようなコード:

A* bptr = new B;
delete bptr;

効果的に次のようなことをしています:

// Allocate a new B
void* vp1 = allocateMemory(sizeof(B));
B* bp = static_cast<B*>(vp1);
bp->B(); // assume for a second that this was a legal way to construct

// Convert the B* to an A*
A* bptr = &bp->__a_part;

// Deallocate the A*
void* vp2 = ap;
deallocateMemory(vp2);

この場合、vp2vp1は同じです。システムは同じメモリ アドレスの割り当てと割り当て解除を行っているため、プログラムはエラーなしで実行されます。

クラスBに仮想メンバー関数 (この場合はデストラクタ) がある場合。コンパイラは仮想テーブル ポインターを追加するため、クラス B は次のようになります。

struct B {
  B_vtable* __vptr;
  A __a_part;
};

ここでの問題は__a_part、それが最初のメンバーではなくなり、convertToAPointer操作によってポインターのアドレスが変更されるためvp2vp1同じアドレスを指しなくなることです。割り当てられたメモリ位置とは異なるメモリ位置が割り当て解除されているため、エラーが発生します。

于 2013-02-17T16:21:22.510 に答える