C++マルチステートの実現原理とメモリモデル

10630 ワード

マルチステートはC++において重要な概念であり,虚関数メカニズムによりプログラム実行時に呼び出し対象に基づいて具体的にどの関数を呼び出すかを判断することを実現した.
具体的には、親クラスのポインタ(または参照)は、その子クラスのインスタンスを指し、親クラスのポインタ(または参照)によって実際の子クラスのメンバー関数を呼び出します.虚関数を含むクラスの各オブジェクトの一番前(このオブジェクトオブジェクトのメモリレイアウトの一番前)には、虚関数ポインタ(vptr)と呼ばれるものが虚関数テーブル(vtbl)を指しています.この虚関数テーブル(ここでは最も簡単な単一継承の場合のみを議論し,多重継承であれば複数の虚関数テーブルが存在する可能性がある)には,このクラスのすべての虚関数のポインタが格納されており,中の関数を呼び出すときにこの虚関数テーブルを検索することで対応する虚関数を見つけることが虚関数の実現原理である.ベースクラスにvptrが挿入されている場合、派生クラスはvptrを継承して再利用します.vptr(一般的には、オブジェクトメモリモデルの上部)は、オブジェクトタイプの変化に伴って、その値が現在のオブジェクトの実際のタイプと一致することを保証するために、その指向を絶えず変更する必要があります.
以上の概念はC++プログラマーによく知られていますが、具体的な例を挙げて理解を強化してみましょう.
1. 
#include<iostream>
using namespace std;

class IRectangle
{
public:
    virtual ~IRectangle() {}
    virtual void Draw() = 0;
};

class Rectangle: public IRectangle
{
public:
    virtual ~Rectangle() {}
    virtual void Draw(int scale)
    {
        cout << "Rectangle::Draw(int)" << endl;
    }
    virtual void Draw()
    {
        cout << "Rectangle::Draw()" << endl;
    }
};

int main(void)
{
    IRectangle *pI = new Rectangle;
    pI->Draw();
    pI->Draw(200);
    delete pI;
    return 0;
}

セグメントコードのコンパイルに失敗しました:
C:\Users\zhuyp\Desktop>g++ -Wall test.cpp -o test -g
test.cpp: In function 'int main()':test.cpp:29:17: error: no matching function for call to 'IRectangle::Draw(int)' pI->Draw(200); ^test.cpp:29:17: note: candidate is:test.cpp:8:18: note: virtual void IRectangle::Draw() virtual void Draw() = 0; ^test.cpp:8:18: note: candidate expects 0 arguments, 1 provided
C:\Users\zhuyp\Desktop>
以上の情報は,親IrectangleにDraw(int)という関数がないことを示している.確かに、親Irectangleにはこのような署名の関数はありませんが、多態ではないでしょうか、newのは子類Rectangleではないでしょうか.ポインタpIはサブクラスを指すが、それ自体が親Irectangleタイプであるため、pI->draw(200)を実行するときに親vtableを検索し、親vtableにはDraw(int)タイプの関数がないためコンパイルエラーが発生する.
pI->draw(200)という文を変更し、pIをdown castにするとコンパイルは正常、dynamic_cast(pI)->draw(200); このとき呼び出されるのはサブクラスのポインタで、draw(int)と署名された関数があるサブクラスのvtableを検索しますので、問題はありません.
2.
#include <iostream>
using namespace std;

class Base
{
public:
    ~Base()
    {
        cout << "~Base()" << endl;
    }
    void fun()
    {
        cout << "Base::fun()"  << endl;
    }
};

class Derived : public Base
{
public:
    ~Derived()
    {
        cout << "~Derived()" << endl;
    }
    virtual void fun()
    {
        cout << "Derived::fun()"  << endl;
    }
};

int main()
{
    Derived *dp = new Derived;
    Base *p = dp;
    p->fun();
    cout << sizeof(Base) << endl;
    cout << sizeof(Derived) << endl;
    cout << (void *)dp << endl;
    cout << (void *)p << endl;
    delete p;
    p = NULL;

    return 0;
}

プログラムのコンパイルと実行:
C:\Users\zhuyp\Desktop>test.exeBase::fun()180x3856a00x3856a0~Base()
コンパイラはgcc 4を使用する.8.1 pとpbの値は同じであることがわかるので,現代のC++コンパイラでは性能の問題のためにvptrポインタをクラスメモリモデルの先頭に置いていないと結論できる.
3.
#include<iostream>
using namespace std;

class B
{
    int b;
public:
    virtual ~B()
    {
        cout << "B::~B()" << endl;
    }
};

class D: public B
{
    int i;
    int j;
public:
    virtual ~D()
    {
        cout << "D::~D()" << endl;
    }
};

int main(void)
{
    cout << "sizeB:" << sizeof(B) << " sizeD:" << sizeof(D) << endl;
    char *ch = NULL;
    
    B *pb = new D[2];

    cout<<"size *pb "<<sizeof(pb)<<"\tend"<<endl;
    
    delete [] pb;

    return 0;
}

プログラムの実行エラーは、pbのサイズを出力した後に発生します.delete[]pbで問題が発生したことがわかります.
申請の配列空間を解放するときにdelete[]を使用する必要があることを知っていますが、deleteはどのようにしてどれだけのメモリを解放するか知っていますか?delete[]の実装には、ポインタの算術演算が含まれており、各ポインタが指す要素の構造関数を順次呼び出し、配列要素全体のメモリを解放する必要があります.
C++のマルチステートが存在するため、親ポインタはサブクラスのメモリ領域を指す可能性があります.上記の例ではdelete[]がマルチステート配列の空間を解放しているため、delete[]計算空間はBクラスの大きさで計算され、オフセット呼び出し毎の解析関数はBクラスで行われるが、この配列は実際にはDクラスのポインタ解放の大きさが間違っている(sizeof(B)!=sizeof(D),)だから崩壊する.
C:\Users\zhuyp\Desktop>test.exesizeB:16 sizeD:24size *pb 8 end
注意:本コードは64 bit環境で実行するので*pbは8.