C++を書きはじめて半年もすると、ほとんどのプログラマはクラスの定義を見ただけで生成されるオブジェクトのメモリレイアウトが浮かんでくるようになります。
1 2 3 4 5 6 7 8 9 10 11 |
#include <cstdint> struct AAA { std::uint8_t a1; std::uint8_t a2; std::int16_t a3; }; struct BBB : public AAA { std::int16_t b; }; |
構造体AAAのメモリレイアウト
1 2 3 4 |
0x8000 0000 a1 0x8000 0001 a2 0x8000 0002 a3 0x8000 0003 |
構造体BBBのメモリレイアウト
1 2 3 4 5 6 |
0x8000 0000 a1 0x8000 0001 a2 0x8000 0002 a3 0x8000 0003 0x8000 0004 b 0x8000 0005 |
このように一目です。(ただし浮かんでくるだけで合っているとは言っていない)
基本的にクラス内のメンバ変数は定義された順にメモリ上に並びます。
単一継承の場合、親クラス→子クラスというメモリレイアウトになります。
もう少し複雑な設計になるとどうでしょうか。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
#include <cstdint> // さっきと同じ struct AAA { std::uint8_t a1; std::uint8_t a2; std::int16_t a3; }; struct BBB : public AAA { std::int16_t b; }; // ここまでは一緒 struct CCC { virtual void hoge(); // 仮想関数 std::uint32_t c; }; struct DDD : public AAA // 多重継承 , public CCC { void hoge() override; int d; }; |
多少複雑になりました。
構造体DDDは仮想関数を持ち、多重継承しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
0x8000 0000 vptr 0x8000 0001 0x8000 0002 0x8000 0003 0x8000 0004 0x8000 0005 0x8000 0006 0x8000 0007 0x8000 0008 c 0x8000 0009 0x8000 000a 0x8000 000b 0x8000 000c a1 0x8000 000d a2 0x8000 000e a3 0x8000 000f 0x8000 0010 d 0x8000 0011 0x8000 0012 0x8000 0013 |
これは64bit環境における一例です。
しかし、元のコードだけでは圧倒的に情報が不足しているため、この構造体定義から導き出されるメモリレイアウトは無数にあります。ある意味、どんなレイアウトを想像しても正解と言えなくもありません。
intは4byteかもしれませんし、8byteかもしれません。
vtableのポインタは多くの場合メンバの最初に置かれますがこれはコンパイラ依存です。さらに、そもそも動的なディスパッチを仮想関数テーブルによって実装しない場合はvtableはありません。
同様に多重継承した場合のレイアウトも宣言順とは限らず不定です。
クラスのメモリレイアウトに関して少しマイナーな情報もあります。
最もシンプルな例、構造体AAAに戻って話を進めましょう。
1 2 3 4 5 6 7 8 9 10 11 12 |
#include <cstdint> struct AAA { std::int8_t a; private: std::int8_t b; public: std::int8_t c; protected: std::int8_t d; std::int8_t e; }; |
このメンバa〜e のメモリレイアウトは不定です。宣言順にabcdeと順序よく配置される保証はありません。
C++の規格ではあるアクセス指定子の範囲において宣言された順にレイアウトされることは保証していますがアクセスレベルを超えた宣言についてのメモリレイアウトには規程がありません。
Nonstatic data members of a (non-union) class with the same access control (Clause 11) are allocated so that later members have higher addresses within a class object. The order of allocation of non-static data members with different access control is unspecified (11).
__n3337
この場合d,eが連続したメモリに配置されることのみ保証されます。
aもbもprivateメンバですが、アクセス指定子を超えた場所で宣言されているのでレイアウトの保証対象外です。
もしあなたが複数のプラットフォーム向けにビルドする予定のアプリケーションを書く可能性があるのなら、どのように書くとメモリレイアウトが「不定」になるかを知ることは非常に重要です。
まとめ
- メンバ変数はあるアクセス指定子の中では宣言された順にレイアウトされる
- アクセス指定子をまたぐ場合のレイアウトは不定
- 仮想関数を持ったクラスのレイアウトも実装依存なので不定
- 多重継承のレイアウトも実装依存なので不定
- メモリレイアウトが不定になるシチュエーションを知ろう