C ++ new [] into base class pointer crash on array access
-
06-07-2019 - |
質問
単一のオブジェクトを割り当てると、このコードは正常に機能します。配列構文を追加しようとすると、セグメンテーション違反が発生します。どうしてこれなの?ここでの私の目標は、クラスcが内部でbオブジェクトを使用しているという事実を外部から隠すことです。このプログラムをコードパッドに投稿して、一緒にプレイできるようにしました。
#include <iostream>
using namespace std;
// file 1
class a
{
public:
virtual void m() { }
virtual ~a() { }
};
// file 2
class b : public a
{
int x;
public:
void m() { cout << "b!\n"; }
};
// file 3
class c : public a
{
a *s;
public:
// PROBLEMATIC SECTION
c() { s = new b[10]; } // s = new b;
void m() { for(int i = 0; i < 10; i++) s[i].m(); } // s->m();
~c() { delete[] s; } // delete s;
// END PROBLEMATIC SECTION
};
// file 4
int main(void)
{
c o;
o.m();
return 0;
}
解決
1つの問題は、式 s [i]
がポインター演算を使用して目的のオブジェクトのアドレスを計算することです。 s
は a
へのポインターとして定義されているため、結果は a
sの配列に対しては正しく、 bの配列に対しては正しくありません。
s。継承によって提供される動的バインディングは、メソッドに対してのみ機能し、他には何も機能しません(たとえば、仮想データメンバーや仮想 sizeof
など)。したがって、メソッド s [i] .m()
を呼び出すと、 this
ポインターは i
th aに設定されます配列内の
オブジェクト。しかし、実際には配列は b
sの1つであるため、(場合によっては)オブジェクトの中央のどこかを指し、セグメンテーション違反が発生します(おそらくプログラムがオブジェクトのvtableにアクセスしようとすると)。 operator []()
を仮想化してオーバーロードすることで問題を修正できる場合があります。 (ただし、実際に機能するかどうかを確認することはできませんでした。)
別の問題は、デストラクタの delete
です。同様の理由によります。仮想化してオーバーロードできる場合もあります。 (繰り返しますが、私の頭に浮かんだランダムなアイデアです。うまくいかないかもしれません。)
もちろん、(他の人が提案したように)キャストも機能します。
他のヒント
new
を使用して10個の b
の配列を作成し、そのアドレスを a *
に割り当てるだけで問題が発生します。
配列を多態的に扱わないでください。
詳細については、 ARR39-CPP。セクション 06。 CERTの配列とSTL(ARR) C ++セキュアコーディング標準。
&quot; b&quot;型の配列がありますタイプ「a」ではない;そして、それをタイプaのポインターに割り当てています。多態性は動的配列に転送されません。
a* s
へ
b* s
これが機能し始めます。
まだバインドされていないポインターのみが多態的に処理できます。考えてみてください
a* s = new B(); // works
//a* is a holder for an address
a* s = new B[10]
//a* is a holder for an address
//at that address are a contiguos block of 10 B objects like so
// [B0][B2]...[B10] (memory layout)
sを使用して配列を反復処理するときは、何が使用されるかを考えてください
s[i]
//s[i] uses the ith B object from memory. Its of type B. It has no polymorphism.
// Thats why you use the . notation to call m() not the -> notation
今持っていた配列に変換する前に
a* s = new B();
s->m();
ここでのsは単なるアドレスであり、s [i]のような静的オブジェクトではありません。アドレスsのみを動的にバインドできます。なに?知るか?アドレスs。
の何かCスタイルの配列がどのように配置されるかという点でこれが意味をなさない理由の詳細については、以下の Ari の優れた回答を参照してください。
Bの各インスタンスには、Xデータメンバーと「vptr」の両方が含まれます。 (仮想テーブルへのポインター)。
Aの各インスタンスには&quot; vptr&quot;のみが含まれます
したがって、sizeof(a)!= sizeof(b)。
これを行うと、&quot; S = new b [10]&quot; rawにbの10個のインスタンスをメモリに置くと、S(a *のタイプを持つ)はデータのrawの先頭を取得しています。
C :: m()メソッドでは、コンパイラに&quot; a&quot;の配列を反復処理するように指示します。 (sのタイプはa *であるため)、 BUT 、sは実際には「b」の配列を指します。 s [i]を呼び出すと、コンパイラが実際に行うことは&quot; s + i * sizeof(a)&quot;です。 、コンパイラは「a」の単位でジャンプします。 「b」の単位ではなく、また、aとbのサイズが同じではないため、多くのマンボジャンボが得られます。
回答に基づいて回避策を見つけました。インダイレクションのレイヤーを使用して、実装の詳細を隠すことができます。また、配列内のオブジェクトを組み合わせて一致させることもできます。ありがとう!
#include <iostream>
using namespace std;
// file 1
class a
{
public:
virtual void m() { }
virtual ~a() { }
};
// file 2
class b : public a
{
int x;
public:
void m() { cout << "b!\n"; }
};
// file 3
class c : public a
{
a **s;
public:
// PROBLEMATIC SECTION
c() { s = new a* [10]; for(int i = 0; i < 10; i++) s[i] = new b(); }
void m() { for(int i = 0; i < 10; i++) s[i]->m(); }
~c() { for(int i = 0; i < 10; i++) delete s[i]; delete[] s; }
// END PROBLEMATIC SECTION
};
// file 4
int main(void)
{
c o;
o.m();
return 0;
}