[C ++詳細分析] 36. C ++オブジェクトモデル(以下:継承時のメモリ分散、仮想関数テーブル)

前回のブログでは、クラスと構造体は同じメモリ配置規則に従うと述べました。クラスのメンバー関数とクラス変数は別々に格納されます。sizeofはメンバー変数のメモリサイズを解決するために使用されます。メンバー関数は含まれていません。メンバー関数はコードセグメントにありますで。

継承後のサブクラスのサイズを計算するにはどうすればよいですか?

1継承されたオブジェクトモデル

  • サブクラスは、新しいメンバーの親クラスメンバーのサブクラスの重ね合わせ GET

ここに画像の説明を挿入
サブクラスのサイズは、親クラスのメンバー変数の後にサブクラスの新しく追加されたメンバー変数が続き、次にサイズが計算されます。以下では、プログラミング実験によって証明します

プログラミング実験:継承されたオブジェクトモデル

// 36-1.cpp
#include<iostream>
using namespace std;
class Demo
{
protected:
    int mi;
    int mj;
public:
    void print()
    {
        cout << "mi = " << mi << ", "
            << "mj = " << mj << endl;
    }
};
class Derived : public Demo
{
public:
    Derived(int i, int j, int k)
    {
        mi = i;
        mj = j;
        mk = k;
    }
    void print()
    {
        cout << "mi = " << mi << ", "
            << "mj = " << mj << ", "
            << "mk = " << mk << endl;
    }
private:
    int mk;
};
struct Test
{
    int mi;
    int mj;
    int mk;
};
int main()
{
    cout << "sizeof(Demo) = " << sizeof(Demo) << endl;
    cout << "sizeof(Derived) = " << sizeof(Derived) << endl;
    Derived d(1, 2, 3);
    d.print();
    Test *p = reinterpret_cast<Test*>(&d);
    p->mi = 10;
    p->mj = 20;
    p->mk = 30;
    d.print();
    return 0;
}
  • サブクラスは、親クラスのメンバーからサブクラスの新しいメンバーを重ね合わせることによって取得されるため、DerivedはクラスのDemoメンバーに基づいてmkを重ね合わせます。クラスオブジェクトのサイズを計算すると、メンバー変数のサイズが計算され、メンバー関数はオブジェクトのサイズをカウントせずにコードセグメントに保存されます。したがって、Demoのサイズは8バイトで、Derivedのサイズは12バイトです。
  • クラスDerivedとstruct Testのメモリ分布は同じです。Testポインタを使用してDerivedオブジェクトをポイントできるため、このメモリの一部が再解釈され、クラスオブジェクトの変数を変更できます。

コンパイルして実行:

$ g++ 36-1.cpp -o 36-1
$ ./36-1
sizeof(Demo) = 8
sizeof(Derived) = 12
mi = 1, mj = 2, mk = 3
mi = 10, mj = 20, mk = 30

2ポリモーフィックオブジェクトモデル(仮想関数)

2.1 C ++ポリモーフィズムの実現原理

  • 仮想関数を宣言すると、コンパイラはクラスに仮想関数テーブル(メンバー関数のアドレスを格納するデータ構造)を生成します
  • クラスで仮想関数が定義されている場合、各オブジェクトには仮想関数テーブルへのポインターがあります
  • 仮想関数テーブルは、コンパイラーによって自動的に生成および保守されます

次の図に示すように、仮想関数を定義すると、メンバー変数に加​​えて、オブジェクトにはメンバー関数へのポインターを格納する仮想関数テーブルへのポインターも含まれます。

注:仮想関数テーブルのポインターは、オブジェクトインスタンスの最前面にあります。

ここに画像の説明を挿入

2.2仮想関数テーブル

一般的な継承(仮想関数カバレッジなし)
では、継承中に仮想関数テーブルを見てみましょう。次に示すような継承関係があると想定します。
ここに画像の説明を挿入
メンバー関数はすべて仮想関数です。この継承関係では、サブクラスは親クラスをオーバーロードしません機能。派生クラスの仮想関数テーブルは次のとおりです
ここに画像の説明を挿入

  • 仮想関数宣言されたテーブルに配置されます
  • 親クラスの仮想関数が子クラスの仮想関数に先行する

一般的な継承(仮想関数カバレッジあり)
親クラスの仮想関数をオーバーロードするサブクラスに仮想関数がある場合、仮想関数テーブルはどのようになりますか?次のような継承関係があります。
ここに画像の説明を挿入
次のように継承した後、クラスカバーのみの親クラスの関数f()、派生クラスの仮想関数テーブルの一例を効果を比較する:
ここに画像の説明を挿入
参照を

  • オーバーレイされたf()関数は、元の親仮想関数の仮想テーブルに配置されます
  • カバーされていない機能はまだあります

オブジェクトに格納された仮想関数テーブルへのポインターはメモリの前にあり、次の実験はオブジェクト内の仮想関数テーブルへのポインターの存在と位置を証明します。

プログラミング実験:オブジェクトの仮想関数テーブルへのポインター

上記のコードを変更し、デモでprint()関数を仮想関数として定義し、構造体struct Testの前にポインターを追加します。クラスDerivedのメモリが構造体Testのメモリと同じであることを証明します。

// 36-1.cpp
#include<iostream>
using namespace std;
class Demo
{
protected:
    int mi;
    int mj;
public:
    virtual void print()		// 虚函数,类对象中将有一个指针,指向虚函数表
    {
        cout << "mi = " << mi << ", "
            << "mj = " << mj << endl;
    }
};
class Derived : public Demo
{
public:
    Derived(int i, int j, int k)
    {
        mi = i;
        mj = j;
        mk = k;
    }
    void print()
    {
        cout << "mi = " << mi << ", "
            << "mj = " << mj << ", "
            << "mk = " << mk << endl;
    }
private:
    int mk;
};
struct Test				// 结构体中指针在最前面,和Derived内存布局相同
{
    void* p;
    int mi;
    int mj;
    int mk;
};
int main()
{
    cout << "sizeof(Demo) = " << sizeof(Demo) << endl;
    cout << "sizeof(Derived) = " << sizeof(Derived) << endl;
    Derived d(1, 2, 3);
    d.print();
    Test *p = reinterpret_cast<Test*>(&d);
    p->mi = 10;
    p->mj = 20;
    p->mk = 30;
    d.print();
    return 0;
}

まず、親クラスのデモとサブクラスのクラスDerivedのサイズを計算します。オブジェクトには仮想関数テーブルへのポインターが含まれ、クラスサイズは8バイトなので、デモサイズは16、派生サイズは24です。

クラスオブジェクト内の仮想関数テーブルへのポインターは先頭にあり、メモリレイアウトはstruct Testと同じです。構造体のポインターを使用して、クラスのメンバー変数を変更できます。

コンパイルして実行:

$ g++ 36-1.cpp -o 36-1
$ ./36-1
sizeof(Demo) = 16
sizeof(Derived) = 24
mi = 1, mj = 2, mk = 3
mi = 10, mj = 20, mk = 30

Demoのサイズは16バイトで、Derivedのサイズは24バイトです。これは、クラスオブジェクト内のポインターの存在を証明します。クラスオブジェクトのデータは、構造体ポインターを介して変更できます。これは、クラスオブジェクト内のメンバーのメモリ分布が構造体Testとまったく同じであることを証明します。

2.3仮想関数を呼び出すプロセス

仮想関数を呼び出すプロセスを次の図に示します

クラスオブジェクトは、ポインタに従って仮想関数テーブルを検索し、仮想関数テーブルには仮想関数を指す関数ポインタがあり、関数ポインタに従って関数を見つけることができます。
ここに画像の説明を挿入
仮想関数でない場合、コンパイラーは呼び出されたメンバー関数のアドレスを直接判別できます。したがって、仮想関数は通常のメンバー関数よりも効率的ではありません

3 C言語はポリモーフィズムを実現します

この部分は難しく、多態性の実装プロセスを理解するために使用されます。

// Polymorphism1.h
#ifndef _POLYYMORPHISM_H_
#define _POLYYMORPHISM_H_
typedef void Demo;
typedef void Derived;

Demo* Demo_Create(int i, int j);
int Demo_GetI(Demo* pThis);
int Demo_GetJ(Demo* pThis);
int Demo_Add(Demo* pThis, int value);
void Demo_Free(Demo* pThis);

Derived* Derived_Create(int i, int j, int k);
int Derived_GetK(Derived* pThis);
int Derived_Add(Derived* pThis, int value);
#endif
// Polymorphism1.c
#include "Polymorphism.h"
#include "malloc.h"

static int Demo_Virtual_Add(Demo* pThis, int value);
static int Derived_Virtual_Add(Demo* pThis, int value);

struct VTable     // 2. 定义虚函数表数据结构
{
    int (*pAdd)(void*, int);   // 3. 虚函数表里面存储什么???
};
struct ClassDemo
{
    struct VTable* vptr;     // 1. 定义虚函数表指针  ==》 虚函数表指针类型???
    int mi;
    int mj;
};
struct ClassDerived
{
    struct ClassDemo d;
    int mk;
};

static struct VTable g_Demo_vtbl = 
{
    Demo_Virtual_Add
};
static struct VTable g_Derived_vtbl = 
{
    Derived_Virtual_Add
};

Demo* Demo_Create(int i, int j)
{
    struct ClassDemo* ret = (struct ClassDemo*)malloc(sizeof(struct ClassDemo)); 
    if( ret != NULL )
    {
        ret->vptr = &g_Demo_vtbl;   // 4. 关联对象和虚函数表
        ret->mi = i;
        ret->mj = j;
    }
    return ret;
}

int Demo_GetI(Demo* pThis)
{
     struct ClassDemo* obj = (struct ClassDemo*)pThis;    
     return obj->mi;
}

int Demo_GetJ(Demo* pThis)
{
    struct ClassDemo* obj = (struct ClassDemo*)pThis;
    return obj->mj;
}

// 6. 定义虚函数表中指针所指向的具体函数
static int Demo_Virtual_Add(Demo* pThis, int value)
{
    struct ClassDemo* obj = (struct ClassDemo*)pThis;
    return obj->mi + obj->mj + value;
}

// 5. 分析具体的虚函数!!!!
int Demo_Add(Demo* pThis, int value)
{
    struct ClassDemo* obj = (struct ClassDemo*)pThis;
    return obj->vptr->pAdd(pThis, value);
}

void Demo_Free(Demo* pThis)
{
    free(pThis);
}

Derived* Derived_Create(int i, int j, int k)
{
    struct ClassDerived* ret = (struct ClassDerived*)malloc(sizeof(struct ClassDerived));   
    if( ret != NULL )
    {
        ret->d.vptr = &g_Derived_vtbl;
        ret->d.mi = i;
        ret->d.mj = j;
        ret->mk = k;
    }   
    return ret;
}

int Derived_GetK(Derived* pThis)
{
    struct ClassDerived* obj = (struct ClassDerived*)pThis;
    return obj->mk;
}

static int Derived_Virtual_Add(Demo* pThis, int value)
{
    struct ClassDerived* obj = (struct ClassDerived*)pThis; 
    return obj->mk + value;
}

int Derived_Add(Derived* pThis, int value)
{   
    struct ClassDerived* obj = (struct ClassDerived*)pThis;
    return obj->d.vptr->pAdd(pThis, value);
}
// 36-2.c
#include<iostream>
#include"Polymorphism.h"
using namespace std;
void run(Demo* p, int v)
{
	int r = Demo_Add(p, v);
	printf("r = %d\n", r);
}
int main()
{
    Demo* pb = Demo_Create(1, 2);
    Derived* pd = Derived_Create(1, 22, 333);
    
    printf("pb->add(3) = %d\n", Demo_Add(pb, 3));
    printf("pd->add(3) = %d\n", Derived_Add(pd, 3));
    
    run(pb, 3);
    run(pd, 3);
    Demo_Free(pb);
    Demo_Free(pd);
    return 0;
}
$ g++ 36-2.c Polymorphism1.c -o 36-2
$ ./36-2
pb->add(3) = 6
pd->add(3) = 336
r = 6
r = 336

4まとめ

1.継承の本質は、父親と息子の間のメンバー変数の重ね合わせです
。2. C ++ポリモーフィズムは、仮想関数テーブル(コンパイラーによって自動的に維持される)によって実現されます。3
.仮想関数の効率は、通常のメンバー関数の効率よりも低くなります。

298件のオリジナル記事を公開 181 件を賞賛 100,000回以上の閲覧

おすすめ

転載: blog.csdn.net/happyjacob/article/details/104411231