C++ オブジェクトのメモリ レイアウト

C++ オブジェクトのメモリ レイアウトに影響を与える可能性のある要因は次のとおりです。

  1. オブジェクトデータのメンバー変数
  2. オブジェクトの一般的なメンバー関数
  3. オブジェクトの仮想メンバー関数
  4. オブジェクトの継承 - 親クラスには仮想関数が含まれていません
  5. オブジェクトの継承 - 親クラスには仮想関数が含まれています
  6. オブジェクトの多重継承 - 親クラスには仮想関数が含まれていません
  7. オブジェクトの多重継承 - 親クラスに仮想関数が含まれる
  8. オブジェクトの仮想継承
  9. 動的型情報 (ここでは説明しません)
  10. アセンブリ コードを通じて仮想関数の呼び出しプロセスを確認する

これらの機能の実装は、オペレーティング システム プラットフォームやコンパイラ/ABI によって異なることが多く、ここでの分析は主に Linux x86_64 プラットフォーム上の GCC 11.2.0/G++ 11.2.0 の動作に基づいています。

一般的な C++ オブジェクトのメモリ レイアウト

一般に、C++ オブジェクトは、少数のデータ メンバー変数のみを持つ C++ オブジェクトを指し、C++ で最も単純なクラス オブジェクトです。キーワードを使用して定義されたクラスclassは、メンバー変数のデフォルトの可視性を除いて、C 言語の構造体とまったく同じです。

ここでは、一般的な C++ クラスを定義し、オブジェクトをインスタンス化し、オブジェクトのアドレスと各データ メンバー変数のアドレスを確認します。

class Object;
void print_field_address(Object *obj);

class Object {
public:
  int32_t data_1 = 0;
  int16_t data_2 = 0;
  int64_t data_3 = 0;
  int32_t data_4 = 0;
private:
  int32_t data_5 = 0;
  int32_t data_6 = 0;

  friend void print_field_address(Object *obj);
};

void print_field_address(Object *obj) {
  printf("Address of obj %p, object size %zu bytes\n", obj, sizeof(*obj));
  printf("Address of Field data_1 %p\n", &obj->data_1);
  printf("Address of Field data_2 %p\n", &obj->data_2);
  printf("Address of Field data_3 %p\n", &obj->data_3);
  printf("Address of Field data_4 %p\n", &obj->data_4);
  printf("Address of Field data_5 %p\n", &obj->data_5);
  printf("Address of Field data_6 %p\n", &obj->data_6);
}

int main(int argc, char *argv[]) {
  Object obj;
  print_field_address(&obj);

  return 0;
}

上記のコードの出力は次のようになります。

Address of obj 0x7fff3c605fa0, object size 32 bytes
Address of Field data_1 0x7fff3c605fa0
Address of Field data_2 0x7fff3c605fa4
Address of Field data_3 0x7fff3c605fa8
Address of Field data_4 0x7fff3c605fb0
Address of Field data_5 0x7fff3c605fb4
Address of Field data_6 0x7fff3c605fb8

異なるマシンで実行した場合、または同じマシンで複数回実行した場合、各実行の出力は異なる場合がありますが、オブジェクトと各メンバー変数の特定のメモリ アドレスの違いは分析の結論に影響しません。

上記のアドレス出力からわかります。

  • 一般に、C++ オブジェクトのアドレスは、その最初のメンバー変数のアドレスと同じです。
  • 一般に、C++ オブジェクトの各データ メンバーは、宣言された順序でメモリ内に 1 つずつ配置されます。
  • メンバー変数のデータ型により、個々のデータ メンバー間にパディングが存在する場合があります。
  • オブジェクトのサイズは、パディングにより、そのメンバーのサイズの実際の合計よりも大きくなります。サンプル オブジェクトでは、パディングが 2 か所で発生します。

メンバー関数を含む一般的な C++ オブジェクトのメモリ レイアウト

ここでは、上記の一般的な C++ クラスにいくつかのメンバー関数を追加し、そのクラス オブジェクトのメモリ レイアウトを確認します。サンプルコードは次のとおりです。

class Object;
void print_field_address(Object *obj);

class Object {
public:
  Object();
  ~Object();
  void funcA();
  void funcB();
  int32_t data_1 = 0;
  int16_t data_2 = 0;
  int64_t data_3 = 0;
  void funcC();
  int32_t data_4 = 0;
private:
  void funcD();
  int32_t data_5 = 0;
  void funcE();
  int32_t data_6 = 0;

  friend void print_field_address(Object *obj);
};

Object::Object() {}
Object::~Object() {}
void Object::funcA() {}
void Object::funcB() {}
void Object::funcC() {}
void Object::funcD() {}
void Object::funcE() {}

void print_field_address(Object *obj) {
  printf("Address of obj %p, object size %zu bytes\n", obj, sizeof(*obj));
  printf("Address of Field data_1 %p\n", &obj->data_1);
  printf("Address of Field data_2 %p\n", &obj->data_2);
  printf("Address of Field data_3 %p\n", &obj->data_3);
  printf("Address of Field data_4 %p\n", &obj->data_4);
  printf("Address of Field data_5 %p\n", &obj->data_5);
  printf("Address of Field data_6 %p\n", &obj->data_6);
}

int main(int argc, char *argv[]) {
  Object obj;
  print_field_address(&obj);

  return 0;
}

上記のコードの出力は次のようになります。

Address of obj 0x7ffe6d94ba00, object size 32 bytes
Address of Field data_1 0x7ffe6d94ba00
Address of Field data_2 0x7ffe6d94ba04
Address of Field data_3 0x7ffe6d94ba08
Address of Field data_4 0x7ffe6d94ba10
Address of Field data_5 0x7ffe6d94ba14
Address of Field data_6 0x7ffe6d94ba18

クラス メンバー関数はクラス オブジェクトではなくクラスに属しているため、メンバー関数を追加してもクラス オブジェクトのメモリ レイアウトには影響しません。それは、出力されたクラス オブジェクトのアドレスと、クラス オブジェクトの各メンバー変数のアドレスからわかります。

仮想関数を使用した C++ クラス オブジェクトのメモリ レイアウト

ここでは、上記のメンバー関数を持つクラスのメンバー関数の一部を仮想関数に変更し、そのクラス オブジェクトのメモリ レイアウトを確認します。仮想関数を持つクラスには、オブジェクト内に仮想関数テーブルへのポインタが暗黙的に含まれます。仮想関数テーブル ポインタの位置を決定するために、ここでは 2 つのデータ メンバー変数を含むクラスを追加し、仮想関数を含むクラスのオブジェクトを 2 つのメンバー変数の間に埋め込みます。サンプルコードは次のとおりです。

class Object;
void print_field_address(Object *obj);

class Object {
public:
  Object();
  virtual ~Object();
  void funcA();
  virtual void funcB();
  int32_t data_1 = 0;
  int16_t data_2 = 0;
  int64_t data_3 = 0;
  void funcC();
  int32_t data_4 = 0;
private:
  virtual void funcD();
  int32_t data_5 = 0;
  void funcE();
  int64_t data_6 = 0;

  friend void print_field_address(Object *obj);
};

Object::Object() {}
Object::~Object() {}
void Object::funcA() {}
void Object::funcB() {}
void Object::funcC() {}
void Object::funcD() {}
void Object::funcE() {}

void print_field_address(Object *obj) {
  printf("Address of obj %p, object size %zu bytes\n", obj, sizeof(*obj));
  printf("Address of Field data_1 %p\n", &obj->data_1);
  printf("Address of Field data_2 %p\n", &obj->data_2);
  printf("Address of Field data_3 %p\n", &obj->data_3);
  printf("Address of Field data_4 %p\n", &obj->data_4);
  printf("Address of Field data_5 %p\n", &obj->data_5);
  printf("Address of Field data_6 %p\n", &obj->data_6);
}

class Object1 {
public:
  int64_t data_10 = 0;
  int64_t data_11 = 0;
  Object obj;
  int32_t data_12 = 0;
};

int main(int argc, char *argv[]) {
  Object1 obj;
  printf("Pointer size %zu bytes\n", sizeof(void *));
  printf("Address of Field data_10 %p\n", &obj.data_10);
  printf("Address of Field data_11 %p\n", &obj.data_11);
  print_field_address(&obj.obj);
  printf("Address of Field data_12 %p\n", &obj.data_12);


  return 0;
}

上記のコードの出力は次のようになります。

Pointer size 8 bytes
Address of Field data_10 0x7ffca60e3cf0
Address of Field data_11 0x7ffca60e3cf8
Address of obj 0x7ffca60e3d00, object size 40 bytes
Address of Field data_1 0x7ffca60e3d08
Address of Field data_2 0x7ffca60e3d0c
Address of Field data_3 0x7ffca60e3d10
Address of Field data_4 0x7ffca60e3d18
Address of Field data_5 0x7ffca60e3d1c
Address of Field data_6 0x7ffca60e3d20
Address of Field data_12 0x7ffca60e3d28

私の 64 ビット マシンでは、ポインター変数の長さは 8 バイトです。仮想関数を持つクラスのオブジェクトの場合、そのオブジェクトのアドレスと最初のデータ メンバー変数のアドレスの差は、ポインター変数の長さである 8 です。オブジェクトの開始アドレスとデータ メンバー変数の間にギャップがあってはなりません。前のデータ オブジェクト 最後のデータ メンバーとそれに続くデータの間にギャップがあってはなりません。

仮想関数を含む C++ クラス オブジェクトの場合、メモリ レイアウトは次のようになります。

  • 仮想関数テーブル ポインタはクラス オブジェクトの先頭に配置され、仮想関数テーブル ポインタのアドレスはクラス オブジェクトのアドレスと同じです。
  • 仮想関数の数、仮想関数宣言の場所などは、クラス オブジェクトのメモリ レイアウトに影響を与えません。
  • 仮想関数テーブル ポインタの後には他のデータ メンバー変数が続き、通常の C++ クラス オブジェクトのように配置されます。

単一継承におけるサブクラスオブジェクトのメモリレイアウト

ここでは、メンバー関数とデータ メンバー変数を持つ一般的な C++ クラスを継承するクラスを設計し、そのクラス オブジェクトのメモリ レイアウトを表示します。サンプルコードは次のとおりです。

class Object;
void print_field_address(Object *obj);

class Object {
public:
  Object();
  ~Object();
  void funcA();
  void funcB();
  int32_t data_1 = 0;
  int16_t data_2 = 0;
  int64_t data_3 = 0;
  void funcC();
  int32_t data_4 = 0;
private:
  void funcD();
  int32_t data_5 = 0;
  void funcE();
  int64_t data_6 = 0;

  friend void print_field_address(Object *obj);
};

Object::Object() {}
Object::~Object() {}
void Object::funcA() {}
void Object::funcB() {}
void Object::funcC() {}
void Object::funcD() {}
void Object::funcE() {}

void print_field_address(Object *obj) {
  printf("Address of obj %p, object size %zu bytes\n", obj, sizeof(*obj));
  printf("Address of Field data_1 %p\n", &obj->data_1);
  printf("Address of Field data_2 %p\n", &obj->data_2);
  printf("Address of Field data_3 %p\n", &obj->data_3);
  printf("Address of Field data_4 %p\n", &obj->data_4);
  printf("Address of Field data_5 %p\n", &obj->data_5);
  printf("Address of Field data_6 %p\n", &obj->data_6);
}

class Object1 : public Object {
public:
  int64_t data_10 = 0;
  int64_t data_11 = 0;
  int32_t data_12 = 0;
};

int main(int argc, char *argv[]) {
  Object1 obj;
  printf("Pointer size %zu bytes, obj1 address %p, obj1 size %zu bytes\n",
      sizeof(void *), &obj, sizeof(obj));
  print_field_address(&obj);
  printf("Address of Field data_10 %p\n", &obj.data_10);
  printf("Address of Field data_11 %p\n", &obj.data_11);
  printf("Address of Field data_12 %p\n", &obj.data_12);

  return 0;
}

上記のコードの出力は次のようになります。

Pointer size 8 bytes, obj1 address 0x7ffff0033c90, obj1 size 56 bytes
Address of obj 0x7ffff0033c90, object size 32 bytes
Address of Field data_1 0x7ffff0033c90
Address of Field data_2 0x7ffff0033c94
Address of Field data_3 0x7ffff0033c98
Address of Field data_4 0x7ffff0033ca0
Address of Field data_5 0x7ffff0033ca4
Address of Field data_6 0x7ffff0033ca8
Address of Field data_10 0x7ffff0033cb0
Address of Field data_11 0x7ffff0033cb8
Address of Field data_12 0x7ffff0033cc0

単一継承では、サブクラス オブジェクトのメモリ レイアウトは次のようになります。

  • サブクラス オブジェクトの開始位置により、親クラス オブジェクトの内容が保存されます。
  • サブクラス独自のデータ メンバーは親クラスのすべてのコンテンツの後に配置され、間にパディングが存在する場合があります。
  • サブクラスのデータ メンバーは、宣言された順序でメモリ内に配置されます。

親クラスが仮想関数を持つ場合、単一継承の基本的なロジックは同じです。つまり、親クラスのオブジェクトの内容が子クラスのオブジェクトの先頭に保存されますが、このとき、親クラスのオブジェクトの内容は親クラスの仮想関数テーブル ポインタが含まれます。サンプルコードは次のとおりです。

class Object;
void print_field_address(Object *obj);

class Object {
public:
  Object();
  virtual ~Object();
  virtual void funcA();
  void funcB();
  int32_t data_1 = 0;
  int16_t data_2 = 0;
  int64_t data_3 = 0;
  virtual void funcC();
  int32_t data_4 = 0;
private:
  void funcD();
  int32_t data_5 = 0;
  void funcE();
  int64_t data_6 = 0;

  friend void print_field_address(Object *obj);
};

Object::Object() {}
Object::~Object() {}
void Object::funcA() {}
void Object::funcB() {}
void Object::funcC() {}
void Object::funcD() {}
void Object::funcE() {}

void print_field_address(Object *obj) {
  printf("Address of obj %p, object size %zu bytes\n", obj, sizeof(*obj));
  printf("Address of Field data_1 %p\n", &obj->data_1);
  printf("Address of Field data_2 %p\n", &obj->data_2);
  printf("Address of Field data_3 %p\n", &obj->data_3);
  printf("Address of Field data_4 %p\n", &obj->data_4);
  printf("Address of Field data_5 %p\n", &obj->data_5);
  printf("Address of Field data_6 %p\n", &obj->data_6);
}

class Object1 : public Object {
public:
  void funcA() override;
  void funcC() override;
  int64_t data_10 = 0;
  int64_t data_11 = 0;
  int32_t data_12 = 0;
};

void Object1::funcA() {}
void Object1::funcC() {}

int main(int argc, char *argv[]) {
  Object1 obj;
  printf("Pointer size %zu bytes, obj1 address %p, obj1 size %zu bytes\n",
      sizeof(void *), &obj, sizeof(obj));
  print_field_address(&obj);
  printf("Address of Field data_10 %p\n", &obj.data_10);
  printf("Address of Field data_11 %p\n", &obj.data_11);
  printf("Address of Field data_12 %p\n", &obj.data_12);

  return 0;
}

上記のコードの出力は次のようになります。

Pointer size 8 bytes, obj1 address 0x7ffd42bad690, obj1 size 64 bytes
Address of obj 0x7ffd42bad690, object size 40 bytes
Address of Field data_1 0x7ffd42bad698
Address of Field data_2 0x7ffd42bad69c
Address of Field data_3 0x7ffd42bad6a0
Address of Field data_4 0x7ffd42bad6a8
Address of Field data_5 0x7ffd42bad6ac
Address of Field data_6 0x7ffd42bad6b0
Address of Field data_10 0x7ffd42bad6b8
Address of Field data_11 0x7ffd42bad6c0
Address of Field data_12 0x7ffd42bad6c8

サブクラスが親クラスの仮想関数をオーバーライドするかどうか、および親クラスのオーバーライドされた仮想関数の数は、サブクラス オブジェクトのメモリ レイアウトに影響を与えません。

多重継承におけるサブクラスオブジェクトのメモリレイアウト

ここでは、2 つの親クラスを継承するクラスを設計します。怠惰になって、2 つの親クラスの定義を基本的に同じにしてください。サンプルコードは次のとおりです。

template<typename Object>
void print_field_address(Object *obj);

class Object {
public:
  Object();
  ~Object();
  void funcA();
  void funcB();
  int32_t data_1 = 0;
  int16_t data_2 = 0;
  int64_t data_3 = 0;
  void funcC();
  int32_t data_4 = 0;
private:
  void funcD();
  int32_t data_5 = 0;
  void funcE();
  int64_t data_6 = 0;

  friend void print_field_address<Object>(Object *obj);
};

Object::Object() {}
Object::~Object() {}
void Object::funcA() {}
void Object::funcB() {}
void Object::funcC() {}
void Object::funcD() {}
void Object::funcE() {}

class Object1 {
public:
  Object1();
  ~Object1();
  void funcA();
  void funcB();
  int32_t data_1 = 0;
  int16_t data_2 = 0;
  int64_t data_3 = 0;
  void funcC();
  int32_t data_4 = 0;
private:
  void funcD();
  int32_t data_5 = 0;
  void funcE();
  int64_t data_6 = 0;

  friend void print_field_address<Object1>(Object1 *obj);
};

Object1::Object1() {}
Object1::~Object1() {}
void Object1::funcA() {}
void Object1::funcB() {}
void Object1::funcC() {}
void Object1::funcD() {}
void Object1::funcE() {}

template<typename Object>
void print_field_address(Object *obj) {
  printf("Address of obj %p, object size %zu bytes\n", obj, sizeof(*obj));
  printf("Address of Field data_1 %p\n", &obj->data_1);
  printf("Address of Field data_2 %p\n", &obj->data_2);
  printf("Address of Field data_3 %p\n", &obj->data_3);
  printf("Address of Field data_4 %p\n", &obj->data_4);
  printf("Address of Field data_5 %p\n", &obj->data_5);
  printf("Address of Field data_6 %p\n", &obj->data_6);
}

class Object2: public Object, public Object1 {
public:
  int64_t data_10 = 0;
  int64_t data_11 = 0;
  int32_t data_12 = 0;
};

int main(int argc, char *argv[]) {
  Object2 obj2;
  printf("obj2 address %p, obj2 size %zu bytes\n", &obj2, sizeof(obj2));
  print_field_address<Object>(&obj2);
  print_field_address<Object1>(&obj2);
  printf("Address of Field data_10 %p\n", &obj2.data_10);
  printf("Address of Field data_11 %p\n", &obj2.data_11);
  printf("Address of Field data_12 %p\n", &obj2.data_12);

  return 0;
}

上記のコードの出力は次のようになります。

obj2 address 0x7fff4a92dea0, obj2 size 88 bytes
Address of obj 0x7fff4a92dea0, object size 32 bytes
Address of Field data_1 0x7fff4a92dea0
Address of Field data_2 0x7fff4a92dea4
Address of Field data_3 0x7fff4a92dea8
Address of Field data_4 0x7fff4a92deb0
Address of Field data_5 0x7fff4a92deb4
Address of Field data_6 0x7fff4a92deb8
Address of obj 0x7fff4a92dec0, object size 32 bytes
Address of Field data_1 0x7fff4a92dec0
Address of Field data_2 0x7fff4a92dec4
Address of Field data_3 0x7fff4a92dec8
Address of Field data_4 0x7fff4a92ded0
Address of Field data_5 0x7fff4a92ded4
Address of Field data_6 0x7fff4a92ded8
Address of Field data_10 0x7fff4a92dee0
Address of Field data_11 0x7fff4a92dee8
Address of Field data_12 0x7fff4a92def0

多重継承では、サブクラス オブジェクトのメモリ レイアウトは次のようになります。

  • 各親クラス オブジェクトの内容は、サブクラス オブジェクトの先頭に順番に配置されます。各親クラス オブジェクトの内容は、宣言順に配置されます。各親クラス オブジェクトの内容の間にパディングが存在する場合があります。
  • サブクラス独自のデータ メンバーはすべての親クラスのすべての内容の後に配置され、間にパディングが存在する場合があります。
  • サブクラスのデータ メンバーは、宣言された順序でメモリ内に配置されます。

ここでは、多重継承で親クラスのオブジェクト ポインタが子クラスのオブジェクト ポインタに変換されるときのstatic_cast reinterpret_castさまざまな動作を確認できます。サンプルコードは次のとおりです。

int main(int argc, char *argv[]) {
  Object2 obj2;
  Object1 *obj1p = &obj2;
  Object2 *obj2p = static_cast<Object2 *>(obj1p);
  Object2 *obj2p2 = reinterpret_cast<Object2 *>(obj1p);
  printf("obj1p %p, obj2p %p, obj2p2 %p\n", obj1p, obj2p, obj2p2);

  return 0;
}

上記のコードの出力は次のようになります。

obj1p 0x7ffc763ca260, obj2p 0x7ffc763ca240, obj2p2 0x7ffc763ca260

static_castポインタの型を変換するときに、ポインタの値が型情報に従って調整されることがわかりますreinterpret_castが、ポインタの型を変換するときに、あるポインタの型が意図せずに別の型に強制され、ポインタの値が変化します。タイプ情報に従って調整されない タイプ調整。

この多重継承のコード例では、どちらの親クラスにも仮想関数がありません。2 つの親クラスが仮想関数を持つ場合、2 つの親クラスの仮想関数の関数シグネチャが異なる限り、親クラス オブジェクトのコンテンツを除き、子クラス オブジェクトのメモリ レイアウトに影響はありません。ポインタには、親クラスの仮想関数テーブルが含まれます。サンプルコードは次のとおりです。

template<typename Object>
void print_field_address(Object *obj);

class Object {
public:
  Object();
  virtual ~Object();
  void funcA();
  virtual void funcB();
  int32_t data_1 = 0;
  int16_t data_2 = 0;
  int64_t data_3 = 0;
  void funcC();
  int32_t data_4 = 0;
private:
  void funcD();
  int32_t data_5 = 0;
  void funcE();
  int64_t data_6 = 0;

  friend void print_field_address<Object>(Object *obj);
};

Object::Object() {}
Object::~Object() {}
void Object::funcA() {}
void Object::funcB() {}
void Object::funcC() {}
void Object::funcD() {}
void Object::funcE() {}

class Object1 {
public:
  Object1();
  virtual ~Object1();
  void funcA();
  void funcB();
  int32_t data_1 = 0;
  int16_t data_2 = 0;
  int64_t data_3 = 0;
  virtual void funcC();
  int32_t data_4 = 0;
private:
  void funcD();
  int32_t data_5 = 0;
  void funcE();
  int64_t data_6 = 0;

  friend void print_field_address<Object1>(Object1 *obj);
};

Object1::Object1() {}
Object1::~Object1() {}
void Object1::funcA() {}
void Object1::funcB() {}
void Object1::funcC() {}
void Object1::funcD() {}
void Object1::funcE() {}

template<typename Object>
void print_field_address(Object *obj) {
  printf("Address of obj %p, object size %zu bytes\n", obj, sizeof(*obj));
  printf("Address of Field data_1 %p\n", &obj->data_1);
  printf("Address of Field data_2 %p\n", &obj->data_2);
  printf("Address of Field data_3 %p\n", &obj->data_3);
  printf("Address of Field data_4 %p\n", &obj->data_4);
  printf("Address of Field data_5 %p\n", &obj->data_5);
  printf("Address of Field data_6 %p\n", &obj->data_6);
}

class Object2: public Object, public Object1 {
public:
  virtual ~Object2();
  virtual void funcF();
  int64_t data_10 = 0;
  int64_t data_11 = 0;
  int32_t data_12 = 0;
};

Object2::~Object2() {}

void Object2::funcF(){}

int main(int argc, char *argv[]) {
  Object2 obj2;
  printf("obj2 address %p, obj2 size %zu bytes\n", &obj2, sizeof(obj2));
  print_field_address<Object>(&obj2);
  print_field_address<Object1>(&obj2);
  printf("Address of Field data_10 %p\n", &obj2.data_10);
  printf("Address of Field data_11 %p\n", &obj2.data_11);
  printf("Address of Field data_12 %p\n", &obj2.data_12);

  return 0;
}

クラス オブジェクトのメモリ レイアウトに対する仮想関数の宣言の影響をさらに理解するために、2 つの親クラスに対していくつかの仮想関数を宣言することに加えて、追加の仮想関数もサブクラスに対して特別に宣言します。上記のコードの出力は次のようになります。

obj2 address 0x7ffe732ac990, obj2 size 104 bytes
Address of obj 0x7ffe732ac990, object size 40 bytes
Address of Field data_1 0x7ffe732ac998
Address of Field data_2 0x7ffe732ac99c
Address of Field data_3 0x7ffe732ac9a0
Address of Field data_4 0x7ffe732ac9a8
Address of Field data_5 0x7ffe732ac9ac
Address of Field data_6 0x7ffe732ac9b0
Address of obj 0x7ffe732ac9b8, object size 40 bytes
Address of Field data_1 0x7ffe732ac9c0
Address of Field data_2 0x7ffe732ac9c4
Address of Field data_3 0x7ffe732ac9c8
Address of Field data_4 0x7ffe732ac9d0
Address of Field data_5 0x7ffe732ac9d4
Address of Field data_6 0x7ffe732ac9d8
Address of Field data_10 0x7ffe732ac9e0
Address of Field data_11 0x7ffe732ac9e8
Address of Field data_12 0x7ffe732ac9f0

このサブクラス オブジェクトのサイズは、継承システム全体に仮想関数が含まれていない以前のバージョンよりも 16 バイトだけ増加しており、これは 2 つのポインターのサイズです。これは、サブクラスには専用の vtable ポインターがないことも意味します。

このときのObject2クラスオブジェクトのメモリ配置は下図のようになります。

 Low   |                                  |          
   |   |----------------------------------| <------ Object2 class object memory layout
   |   |           Object::vptr.          |---------|
   |   |----------------------------------|         |---------> |----------------------------|
   |   |     int32_t Object:: data_1      |                     |           . . .            |
  \|/  |----------------------------------|                     |----------------------------|
       |     int32_t Object:: data_2      |                     |           . . .            |
       |----------------------------------|                     |----------------------------|
       |     int32_t Object:: data_3      |
       |----------------------------------|
       |     int32_t Object:: data_4      | 
       |----------------------------------|
       |     int32_t Object:: data_5      |
       |----------------------------------|
       |     int32_t Object:: data_6      | 
       |----------------------------------|
       |          Object1::vptr           |---------|
       |----------------------------------|         |
       |     int32_t Object1:: data_1     |         |---------> |----------------------------|
       |----------------------------------|                     |           . . .            |
       |     int32_t Object1:: data_2     |                     |----------------------------|
       |----------------------------------|                     |           . . .            |
       |     int32_t Object1:: data_3     |                     |----------------------------|
       |----------------------------------|
       |     int32_t Object1:: data_4     |
       |----------------------------------|
       |     int32_t Object1:: data_5     |
       |----------------------------------|
       |     int32_t Object1:: data_6     |
       |----------------------------------|
       |    int64_t Object2:: data_10     |
       |----------------------------------|
       |    int64_t Object2:: data_11     |
       |----------------------------------|
       |    int64_t Object2:: data_12     |
-------|----------------------------------|------------
       |                o                 |
       |                o                 |
       |                o                 |
       |                                  |
       |                                  |

C++ 仮想関数呼び出しと仮想関数テーブルの構造

C++ クラス オブジェクト内の仮想関数テーブル ポインターの位置を理解した後は、仮想関数テーブル ポインターを使用して仮想関数の呼び出しをシミュレートすることは難しくありません。サンプル コードは次のとおりです。

template<typename Object>
void print_field_address(Object *obj);

class Object {
public:
  Object();
  virtual ~Object();
  void funcA();
  virtual void funcB();
  int32_t data_1 = 0;
  int16_t data_2 = 0;
  int64_t data_3 = 0;
  void funcC();
  int32_t data_4 = 0;
private:
  void funcD();
  int32_t data_5 = 0;
  void funcE();
  int64_t data_6 = 0;

  friend void print_field_address<Object>(Object *obj);
};

Object::Object() { printf("Object::ctor()\n"); }
Object::~Object() { printf("Object::~dtor()\n"); }
void Object::funcA() { printf("Object::funcA()\n"); }
void Object::funcB() { printf("Object::funcB()\n"); }
void Object::funcC() { printf("Object::funcC()\n"); }
void Object::funcD() { printf("Object::funcD()\n"); }
void Object::funcE() { printf("Object::funcE()\n"); }

class Object1 {
public:
  Object1();
  virtual ~Object1();
  void funcA();
  void funcB();
  int32_t data_1 = 0;
  int16_t data_2 = 0;
  int64_t data_3 = 0;
  virtual void funcC();
  int32_t data_4 = 0;
private:
  void funcD();
  int32_t data_5 = 0;
  void funcE();
  int64_t data_6 = 0;

  friend void print_field_address<Object1>(Object1 *obj);
};

Object1::Object1() { printf("Object1::ctor()\n"); }
Object1::~Object1() { printf("Object1::~dtor()\n"); }
void Object1::funcA() { printf("Object1::funcA()\n"); }
void Object1::funcB() { printf("Object1::funcB()\n"); }
void Object1::funcC() { printf("Object1::funcC()\n"); }
void Object1::funcD() { printf("Object1::funcD()\n"); }
void Object1::funcE() { printf("Object1::funcE()\n"); }

class Object2: public Object, public Object1 {
public:
  Object2();
  virtual ~Object2();
  virtual void funcF();
  int64_t data_10 = 0;
  int64_t data_11 = 0;
  int32_t data_12 = 0;
};

Object2::Object2() { printf("Object2::ctor()\n"); }
Object2::~Object2() { printf("Object2::~dtor()\n"); }

void Object2::funcF(){ printf("Object2::funcF()\n"); }

int main(int argc, char *argv[]) {
  Object2 obj2;
  Object2 obj2_01;

  printf("obj2 address %p, obj2 size %zu bytes\n", &obj2, sizeof(obj2));

  printf("Object vtable address %p, Object1 vtable address %p\n",
      *reinterpret_cast<void **>(&obj2),
      *reinterpret_cast<void **>(static_cast<Object1 *>(&obj2)));

  printf("Object vtable address %p, Object1 vtable address %p\n",
        *reinterpret_cast<void **>(&obj2_01),
        *reinterpret_cast<void **>(static_cast<Object1 *>(&obj2_01)));

  void ** vtable = (void**)(*reinterpret_cast<void **>(&obj2));
  void ** child_vtable = (void**)(*reinterpret_cast<void **>(static_cast<Object1 *>(&obj2)));

  printf("vtable[0] %p, vtable[1] %p, child_vtable[0] %p, child_vtable[1] %p\n",
      vtable[0], vtable[1], child_vtable[0], child_vtable[1]);

  for (int i = 2; i < 10; ++i) {
    void (*vmem_func)(Object *) = reinterpret_cast<void (*)(Object *)>(vtable[i]);
    printf("vmem_func %p start %d\n", vmem_func, i);
    vmem_func(&obj2);
    printf("vmem_func end\n");
  }

  return 0;
}

上記のコードでは、まず、クラス オブジェクトのアドレスである仮想関数テーブル ポインタのアドレスを取得し、クラス オブジェクトのアドレスをポインタへのポインタにキャストし、それを逆参照することで、仮想関数テーブル ポインタのアドレスを取得します。 function テーブルのアドレス。仮想関数テーブルは関数ポインタの配列、つまりポインタの配列であるため、仮想関数テーブルを指すポインタはポインタの配列にキャストされます。指す関数のプロトタイプ最初のパラメータを除く、仮想関数テーブル内の各関数ポインタによるクラス オブジェクト ポインタである最初のパラメータを除き、他のパラメータはクラス仮想関数宣言内のパラメータです。ここで、各仮想関数の関数プロトタイプは同様に、多少の手間は省けます。

上記のコードの出力は次のようになります。

Object::ctor()
Object1::ctor()
Object2::ctor()
Object::ctor()
Object1::ctor()
Object2::ctor()
obj2 address 0x7ffd921d19e0, obj2 size 104 bytes
Object vtable address 0x558b593eec80, Object1 vtable address 0x558b593eecb0
Object vtable address 0x558b593eec80, Object1 vtable address 0x558b593eecb0
vtable[0] 0x558b593ec5c2, vtable[1] 0x558b593ec628, child_vtable[0] 0x558b593ec61d, child_vtable[1] 0x558b593ec657
vmem_func 0x558b593ec300 start 2
Object::funcB()
vmem_func end
vmem_func 0x558b593ec662 start 3
Object2::funcF()
vmem_func end
vmem_func 0xffffffffffffffd8 start 4

上記のメソッドを通じて、各 C++ クラス オブジェクトの仮想関数テーブルで指定されている各関数を呼び出すと、仮想関数テーブルの構造を確認できます。

  • 仮想関数テーブルはクラス オブジェクトではなくクラスに属します。各クラス オブジェクトは 1 つ以上の仮想関数テーブル ポインタを持つことができますが、対応する仮想関数テーブル ポインタは、同じクラスの異なるインスタンス間の同じポインタを指します。仮想関数テーブル;
  • 仮想関数テーブルの最初の 2 つの要素は、クラス オブジェクトのデストラクターを指します。2 番目のデストラクターはメモリを解放するバージョンです。
  • 2 つの親クラスの仮想関数テーブルでポイントされるデストラクターはまったく同じではありませんが、これらのデストラクターによって実行されるコードはほぼ同一です。
  • サブクラスには個別の仮想関数テーブルはありませんが、最初の親クラスと仮想関数テーブルを共有します。
  • サブクラスの最初の親クラスの仮想関数テーブルの構造は次のとおりです。最初の親クラスの仮想関数、その後にサブクラスに固有の仮想関数が続きます。
  • 最初の親クラスの vtable のアドレスは、2 番目の親クラスの vtable のアドレスに非常に近く、まるで同一であるかのように見えます。

仮想関数テーブルの内部構造を補足すると、Object2 クラスオブジェクトのメモリレイアウトは下図のようになります。

 Low   |                                  |          
   |   |----------------------------------| <------ Object2 class object memory layout
   |   |           Object::vptr.          |---------|
   |   |----------------------------------|         |---------> |----------------------------|
   |   |     int32_t Object:: data_1      |                     |    Object2::~Object2()     |
  \|/  |----------------------------------|                     |----------------------------|
       |     int32_t Object:: data_2      |                     |    Object2::~Object2()     |
       |----------------------------------|                     |----------------------------|
       |     int32_t Object:: data_3      |                     |      Object::funcB()       |
       |----------------------------------|                     |----------------------------|
       |     int32_t Object:: data_4      |                     |      Object2::funcF()      |
       |----------------------------------|                     |----------------------------|
       |     int32_t Object:: data_5      |
       |----------------------------------|
       |     int32_t Object:: data_6      |
       |----------------------------------|
       |          Object1::vptr           |---------|
       |----------------------------------|         |
       |     int32_t Object1:: data_1     |         |---------> |----------------------------|
       |----------------------------------|                     |    Object2::~Object2()     |
       |     int32_t Object1:: data_2     |                     |----------------------------|
       |----------------------------------|                     |    Object2::~Object2()     |
       |     int32_t Object1:: data_3     |                     |----------------------------|
       |----------------------------------|                     |      Object1::funcC()      |
       |     int32_t Object1:: data_4     |                     |----------------------------|
       |----------------------------------|
       |     int32_t Object1:: data_5     |
       |----------------------------------|
       |     int32_t Object1:: data_6     |
       |----------------------------------|
       |    int64_t Object2:: data_10     |
       |----------------------------------|
       |    int64_t Object2:: data_11     |
       |----------------------------------|         |
       |    int64_t Object2:: data_12     |
-------|----------------------------------|------------
       |                o                 |
       |                o                 |
       |                o                 |
       |                                  |
       |                                  |

C++ Prismatic 継承と仮想継承

複雑な多重継承の継承階層では、プリズマティックな継承が偶発的に発生する可能性があります。つまり、特定のサブクラスが複数の親クラスを継承し、これらの複数の親クラスが同じ親クラスを継承します。次の例のように:

template<typename Object>
void print_field_address(Object *obj);

class ObjectBase {
public:
  ObjectBase();
  virtual ~ObjectBase();

  void funcA();
  void funcH();

  virtual void funcG();

  int64_t data_31 = 0;
};

ObjectBase::ObjectBase() {}
ObjectBase::~ObjectBase() {}
void ObjectBase::funcA() {}
void ObjectBase::funcG() {}
void ObjectBase::funcH() {}

class Object : public ObjectBase{
public:
  Object();
  virtual ~Object();
  void funcA();
  virtual void funcB(int a);
  int32_t data_1 = 0;
  int16_t data_2 = 0;
  int64_t data_3 = 0;
  virtual void funcC();
  int32_t data_4 = 0;
private:
  void funcD();
  int32_t data_5 = 0;
  void funcE();
  int64_t data_6 = 0;

  void funcG() override;

  friend void print_field_address<Object>(Object *obj);
};

Object::Object() { printf("Object::ctor()\n"); }
Object::~Object() { printf("Object::~dtor()\n"); }
void Object::funcA() { printf("Object::funcA()\n"); }
void Object::funcB(int a) { printf("Object::funcB()\n"); }
void Object::funcC() { printf("Object::funcC()\n"); }
void Object::funcD() { printf("Object::funcD()\n"); }
void Object::funcE() { printf("Object::funcE()\n"); }
void Object::funcG() { printf("Object::funcG()\n"); }

class Object1 : public ObjectBase{
public:
  Object1();
  virtual ~Object1();
  void funcA();
  virtual void funcB();
  int32_t data_1 = 0;
  int16_t data_2 = 0;
  int64_t data_3 = 0;
  virtual void funcC();
  int32_t data_4 = 0;
private:
  void funcD();
  int32_t data_5 = 0;
  void funcE();
  int64_t data_6 = 0;
  void funcG() override;

  friend void print_field_address<Object1>(Object1 *obj);
};

Object1::Object1() { printf("Object1::ctor()\n"); }
Object1::~Object1() { printf("Object1::~dtor()\n"); }
void Object1::funcA() { printf("Object1::funcA()\n"); }
void Object1::funcB() { printf("Object1::funcB()\n"); }
void Object1::funcC() { printf("Object1::funcC()\n"); }
void Object1::funcD() { printf("Object1::funcD()\n"); }
void Object1::funcE() { printf("Object1::funcE()\n"); }
void Object1::funcG() { printf("Object1::funcG()\n"); }

template<typename Object>
void print_field_address(Object *obj) {
  printf("Address of obj %p, object size %zu bytes\n", obj, sizeof(*obj));
  printf("Address of Field data_31 %p\n", &obj->data_31);
  printf("Address of Field data_1 %p\n", &obj->data_1);
  printf("Address of Field data_2 %p\n", &obj->data_2);
  printf("Address of Field data_3 %p\n", &obj->data_3);
  printf("Address of Field data_4 %p\n", &obj->data_4);
  printf("Address of Field data_5 %p\n", &obj->data_5);
  printf("Address of Field data_6 %p\n", &obj->data_6);
}

class Object2: public Object, public Object1 {
public:
  Object2();
  virtual ~Object2();
  virtual void funcF();
  void funcG() override;
  int64_t data_10 = 0;
  int64_t data_11 = 0;
  int32_t data_12 = 0;
};

Object2::Object2() { printf("Object2::ctor()\n"); }
Object2::~Object2() { printf("Object2::~dtor()\n"); }

void Object2::funcF(){ printf("Object2::funcF()\n"); }
void Object2::funcG(){ printf("Object2::funcG()\n"); }

int case1(int argc, char *argv[]) {
  Object2 obj2;
  printf("obj2 address %p, obj2 size %zu bytes\n", &obj2, sizeof(obj2));
  print_field_address<Object>(&obj2);
  print_field_address<Object1>(&obj2);
  obj2.funcG();
//  obj2.funcH();
//  obj2.data_31 = 1;

  return 0;
}

Object2この例では、 -> Object-> ObjectBaseObject2-> Object1->の 2 つの継承チェーンがありObjectBaseと のObject22 つの親クラスは両方とも同じ を継承するため、プリズマティックな継承が形成されます。ObjectObject1ObjectBase

この継承階層には、最上位レベルにObjectBase2 つの仮想関数 (destructor と destructor )、データ メンバーと一般メンバー関数があります。およびすべてはの仮想関数をオーバーライドしますfuncG()ObjectObject1Object2ObjectBasefuncG()

上記のコードは問題なくコンパイルおよび実行され、実行結果は次のようになります。

Object::ctor()
Object1::ctor()
Object2::ctor()
obj2 address 0x7fff11beed50, obj2 size 120 bytes
Address of obj 0x7fff11beed50, object size 48 bytes
Address of Field data_31 0x7fff11beed58
Address of Field data_1 0x7fff11beed60
Address of Field data_2 0x7fff11beed64
Address of Field data_3 0x7fff11beed68
Address of Field data_4 0x7fff11beed70
Address of Field data_5 0x7fff11beed74
Address of Field data_6 0x7fff11beed78
Address of obj 0x7fff11beed80, object size 48 bytes
Address of Field data_31 0x7fff11beed88
Address of Field data_1 0x7fff11beed90
Address of Field data_2 0x7fff11beed94
Address of Field data_3 0x7fff11beed98
Address of Field data_4 0x7fff11beeda0
Address of Field data_5 0x7fff11beeda4
Address of Field data_6 0x7fff11beeda8
Object2::funcG()
Object2::~dtor()
Object1::~dtor()
Object::~dtor()

上記のサンプル コードを使用すると、次のことがわかります。

  • 継承の有無Object比較するとオブジェクトのサイズは2 つのデータ メンバーのサイズだけ増加します。仮想関数テーブルはオブジェクト内で追加の領域を占有しません。実際、特別な仮想関数テーブル自体はありません。オブジェクトに関連付けられた関数テーブル2 つの仮想関数テーブルがあり、1 つは ( + + )の仮想関数にアクセスするために使用され、もう 1 つは ( + )の仮想関数にアクセスするために使用されます。Object1ObjectBaseObject2ObjectBaseObjectBaseObject2Object2ObjectBaseObjectObject2ObjectBaseObject1
  • 最上位の基本クラスのメンバー (メンバー関数、メンバー変数、および仮想メンバー関数) にアクセスしない場合、コードを正常にコンパイルするのに問題はなく、コードの実行に問題はありません。
  • 仮想関数のみをオーバーライドしObjectオーバーライドしない場合、その関数がobject を通じて呼び出されるとき、コンパイラは次のようなエラーを報告します。Object1ObjectBasefuncG()Object2Object2funcG()
./src/linux_audio.cpp: In function ‘int case1(int, char**)’:
../src/linux_audio.cpp:588:8: error: request for member ‘funcG’ is ambiguous
  588 |   obj2.funcG();
      |        ^~~~~
../src/linux_audio.cpp:490:6: note: candidates are: ‘virtual void ObjectBase::funcG()’
  490 | void ObjectBase::funcG() {}
      |      ^~~~~~~~~~
../src/linux_audio.cpp:552:6: note:                 ‘virtual void Object1::funcG()’
  552 | void Object1::funcG() { printf("Object1::funcG()\n"); }
      |      ^~~~~~~
../src/linux_audio.cpp:522:6: note:                 ‘virtual void Object::funcG()’
  522 | void Object::funcG() { printf("Object::funcG()\n"); }
      |      ^~~~~~
make: *** [src/subdir.mk:26:src/linux_audio.o] 错误 1
"make all" terminated with exit code 2. Build might be incomplete.
  • ObjectObject1およびObject2すべてはObjectBaseの仮想関数をオーバーライドしますfuncG()。関数がObject2オブジェクトを通じて呼び出される場合funcG()、コンパイルまたは実行のどちらでもコードに問題はありません。
  • 継承階層の最上位にある親クラスのデータ メンバーの各サブクラス オブジェクトには 1 つのコピーがあり、実際には継承階層の最下位にあるクラス オブジェクトには複数のコピーがあります。
  • Object2クラスのオブジェクトを通じて、ObjectBaseのメンバー関数にアクセスします。コンパイラは、クラス オブジェクトの一部をメンバー関数に渡す必要があるかどうかを判断できないためObject2コンパイル中に次のようなエラーが発生しますObjectObject1ObjectBase
../src/linux_audio.cpp:589:8: error: request for member ‘funcH’ is ambiguous
  589 |   obj2.funcH();
      |        ^~~~~
../src/linux_audio.cpp:491:6: note: candidates are: ‘void ObjectBase::funcH()’
  491 | void ObjectBase::funcH() {}
      |      ^~~~~~~~~~
../src/linux_audio.cpp:491:6: note:                 ‘void ObjectBase::funcH()’
make: *** [src/subdir.mk:26:src/linux_audio.o] 错误 1
"make all" terminated with exit code 2. Build might be incomplete.

C++ の仮想継承メカニズムは、継承階層の最下位クラスのオブジェクトを介して継承階層の最上位クラスのデータ メンバーにアクセスする際の、シンボル解析におけるあいまいさとコンパイル エラーの問題を解決するために使用されます。仮想的Object1継承させます:ObjectObjectBase

template<typename Object>
void print_field_address(Object *obj);

class ObjectBase {
public:
  ObjectBase();
  virtual ~ObjectBase();

  void funcA();
  void funcH();

  virtual void funcG();

  int64_t data_31 = 0;
  int64_t data_32 = 0;
};

ObjectBase::ObjectBase() {}
ObjectBase::~ObjectBase() {}
void ObjectBase::funcA() {}
void ObjectBase::funcG() {}
void ObjectBase::funcH() {}

class Object : virtual public ObjectBase{
public:
  Object();
  virtual ~Object();
  void funcA();
  virtual void funcB(int a);
  int32_t data_1 = 0;
  int16_t data_2 = 0;
  int64_t data_3 = 0;
  virtual void funcC();
  int32_t data_4 = 0;
private:
  void funcD();
  int32_t data_5 = 0;
  void funcE();
  int64_t data_6 = 0;

  void funcG() override;

  friend void print_field_address<Object>(Object *obj);
};

Object::Object() { printf("Object::ctor()\n"); }
Object::~Object() { printf("Object::~dtor()\n"); }
void Object::funcA() { printf("Object::funcA()\n"); }
void Object::funcB(int a) { printf("Object::funcB()\n"); }
void Object::funcC() { printf("Object::funcC()\n"); }
void Object::funcD() { printf("Object::funcD()\n"); }
void Object::funcE() { printf("Object::funcE()\n"); }
void Object::funcG() { printf("Object::funcG()\n"); }

class Object1 : virtual public ObjectBase{
public:
  Object1();
  virtual ~Object1();
  void funcA();
  void funcB();
  int32_t data_1 = 0;
  int16_t data_2 = 0;
  int64_t data_3 = 0;
  void funcC();
  int32_t data_4 = 0;
private:
  virtual void funcD();
  int32_t data_5 = 0;
  virtual void funcE();
  int64_t data_6 = 0;
  void funcG() override;

  friend void print_field_address<Object1>(Object1 *obj);
};

Object1::Object1() { printf("Object1::ctor()\n"); }
Object1::~Object1() { printf("Object1::~dtor()\n"); }
void Object1::funcA() { printf("Object1::funcA()\n"); }
void Object1::funcB() { printf("Object1::funcB()\n"); }
void Object1::funcC() { printf("Object1::funcC()\n"); }
void Object1::funcD() { printf("Object1::funcD()\n"); }
void Object1::funcE() { printf("Object1::funcE()\n"); }
void Object1::funcG() { printf("Object1::funcG()\n"); }

template<typename Object>
void print_field_address(Object *obj) {
  printf("Address of obj %p, object size %zu bytes\n", obj, sizeof(*obj));

  printf("Address of Field data_1 %p\n", &obj->data_1);
  printf("Address of Field data_2 %p\n", &obj->data_2);
  printf("Address of Field data_3 %p\n", &obj->data_3);
  printf("Address of Field data_4 %p\n", &obj->data_4);
  printf("Address of Field data_5 %p\n", &obj->data_5);
  printf("Address of Field data_6 %p\n", &obj->data_6);

  ObjectBase *base_obj = obj;
  printf("Address of base obj %p, object size %zu bytes\n", base_obj, sizeof(*base_obj));
  printf("Address of Field data_31 %p\n", &obj->data_31);
  printf("Address of Field data_32 %p\n", &obj->data_32);
}

class Object2: public Object, public Object1 {
public:
  Object2();
  virtual ~Object2();
  virtual void funcF();
  void funcG() override;
  int64_t data_10 = 0;
  int64_t data_11 = 0;
  int32_t data_12 = 0;
};

Object2::Object2() { printf("Object2::ctor()\n"); }
Object2::~Object2() { printf("Object2::~dtor()\n"); }

void Object2::funcF(){ printf("Object2::funcF()\n"); }
void Object2::funcG(){ printf("Object2::funcG()\n"); }

int case1(int argc, char *argv[]) {
  Object2 obj2;
  printf("obj2 address %p, obj2 size %zu bytes\n", &obj2, sizeof(obj2));
  print_field_address<Object>(&obj2);
  print_field_address<Object1>(&obj2);
  printf("Address of Field data_10 %p\n", &obj2.data_10);
  printf("Address of Field data_11 %p\n", &obj2.data_11);
  printf("Address of Field data_12 %p\n", &obj2.data_12);
  obj2.funcG();
//  obj2.funcH();
  obj2.data_31 = 1;

  void ** vtable = (void**)(*reinterpret_cast<void **>(static_cast<ObjectBase *>(&obj2)));
  for (int i = 2; i < 10; ++i) {
    void (*vmem_func)(ObjectBase *) = reinterpret_cast<void (*)(ObjectBase *)>(vtable[i]);
    printf("vmem_func %p start %d\n", vmem_func, i);
    vmem_func(&obj2);
    printf("vmem_func end\n");
  }
  return 0;
}

上記のコードの出力は次のようになります。

Object::ctor()
Object1::ctor()
Object2::ctor()
obj2 address 0x7ffe8b59eca0, obj2 size 128 bytes
Address of obj 0x7ffe8b59eca0, object size 64 bytes
Address of Field data_1 0x7ffe8b59eca8
Address of Field data_2 0x7ffe8b59ecac
Address of Field data_3 0x7ffe8b59ecb0
Address of Field data_4 0x7ffe8b59ecb8
Address of Field data_5 0x7ffe8b59ecbc
Address of Field data_6 0x7ffe8b59ecc0
Address of base obj 0x7ffe8b59ed08, object size 24 bytes
Address of Field data_31 0x7ffe8b59ed10
Address of Field data_32 0x7ffe8b59ed18
Address of obj 0x7ffe8b59ecc8, object size 64 bytes
Address of Field data_1 0x7ffe8b59ecd0
Address of Field data_2 0x7ffe8b59ecd4
Address of Field data_3 0x7ffe8b59ecd8
Address of Field data_4 0x7ffe8b59ece0
Address of Field data_5 0x7ffe8b59ece4
Address of Field data_6 0x7ffe8b59ece8
Address of base obj 0x7ffe8b59ed08, object size 24 bytes
Address of Field data_31 0x7ffe8b59ed10
Address of Field data_32 0x7ffe8b59ed18
Address of Field data_10 0x7ffe8b59ecf0
Address of Field data_11 0x7ffe8b59ecf8
Address of Field data_12 0x7ffe8b59ed00
Object2::funcG()
vmem_func 0x55b311c2c96b start 2
Object2::funcG()
vmem_func end
vmem_func 0x55b311c317c8 start 3

この出力からわかるように、次のようになります。

  • 仮想継承を使用しない前のコードと比較すると、継承階層の最下位クラスのオブジェクト サイズは、Object2その 2 つの直接のサブクラスのサイズの合計に等しく、Object2その独自のメンバー変数は占有していないかのように扱われます。仮想継承の最上位の親クラスの内容は、それを継承する各クラス オブジェクトのサイズにカウントされますが、計算する必要があるのは、最下位レベルのクラス オブジェクトのサイズを計算するときに 1 回だけです。
  • Object2クラス オブジェクトのメモリ レイアウトは、最初のサブクラスのすべての内容 (仮想継承クラスの内容を除き、この親クラスの仮想関数テーブル ポインターとデータ メンバー変数のみ) -> 2 番目のサブクラスのすべての内容が続きます。 (仮想継承クラスの内容を除き、親の仮想関数テーブル ポインターとデータ メンバー変数のみを含む) -> 継承階層の最下位サブクラスのデータ メンバー変数 (その仮想関数テーブルと最初の親クラスによって共有される) ) -> 仮想継承クラスの内容は、まず仮想関数テーブルのポインタ、次に各データメンバです。

このときのObject2クラスオブジェクトのメモリ配置は下図のようになります。左側の矢印はメモリ アドレスが増加する方向を指します。C++ クラス オブジェクトのメモリ レイアウトは、オブジェクトがヒープに割り当てられるかスタックに割り当てられるかには関係ありません。

 Low   |                                  |          
   |   |----------------------------------| <------ Object2 class object memory layout
   |   |           Object::vptr.          |---------|
   |   |----------------------------------|         |---------> |----------------------------|
   |   |     int32_t Object:: data_1      |                     |    Object2::~Object2()     |
  \|/  |----------------------------------|                     |----------------------------|
       |     int32_t Object:: data_2      |                     |    Object2::~Object2()     |
       |----------------------------------|                     |----------------------------|
       |     int32_t Object:: data_3      |                     |      Object::funcB()       |
       |----------------------------------|                     |----------------------------|
       |     int32_t Object:: data_4      |                     |      Object::funcC()       |
       |----------------------------------|                     |----------------------------|
       |     int32_t Object:: data_5      |                     |      Object2::funcG()      |
       |----------------------------------|                     |----------------------------|
       |     int32_t Object:: data_6      |                     |      Object2::funcF()      |
       |----------------------------------|                     |----------------------------|
       |          Object1::vptr           |---------|
       |----------------------------------|         |
       |     int32_t Object1:: data_1     |         |---------> |----------------------------|
       |----------------------------------|                     |    Object2::~Object2()     |
       |     int32_t Object1:: data_2     |                     |----------------------------|
       |----------------------------------|                     |    Object2::~Object2()     |
       |     int32_t Object1:: data_3     |                     |----------------------------|
       |----------------------------------|                     |      Object1::funcD()      |
       |     int32_t Object1:: data_4     |                     |----------------------------|
       |----------------------------------|                     |      Object1::funcE()      |
       |     int32_t Object1:: data_5     |                     |----------------------------|
       |----------------------------------|                     |      Object2::funcG()      |
       |     int32_t Object1:: data_6     |                     |----------------------------|
       |----------------------------------|
       |    int64_t Object2:: data_10     |
       |----------------------------------|
       |    int64_t Object2:: data_11     |         |---------> |----------------------------|
       |----------------------------------|         |           |    Object2::~Object2()     |
       |    int64_t Object2:: data_12     |         |           |----------------------------|
       |----------------------------------|         |           |    Object2::~Object2()     |
       |         ObjectBase::vptr         |---------|           |----------------------------|
       |----------------------------------|                     |      Object2::funcG()      |
       |   int64_t ObjectBase:: data_31   |                     |----------------------------|
       |----------------------------------|
       |   int64_t ObjectBase:: data_31   |
-------|----------------------------------|------------
       |                o                 |
       |                o                 |
       |                o                 |
       |                                  |
       |                                  |

参考資料

C++ 仮想継承と仮想基本クラス

さまざまなシナリオにおける C++ オブジェクトのメモリ レイアウト

メモリ レイアウト C++ オブジェクト [終了]

おすすめ

転載: blog.csdn.net/tq08g2z/article/details/125033324