C++ 对象模型深入浅出 Shepard-Wang

class layout

不影响 class layout

  1. static member 具有静态的作用域(static lifetime) 以及 线程的存储期(thread storage duration)。
  2. type member (包括嵌套类型)
  3. non-virtual member functions

0. Empty Base Class Optimize

class Empty0 {};
class Empty {};
class Empty2
{
    Empty e1;
    Empty e2;
    Empty e3;
};

class I
{
    int i;
};

class Empty3 : public Empty0, public Empty
{
    int a;
};

int main()
{
    Empty e1;
    Empty2 e2;
    Empty3 e3;
    cout << sizeof e1 << " "  << sizeof e2 << " " << sizeof e3 << endl;
}
1 3 4

1. 多态会修改指针的值!

derived -> base 编译时绑定

derieve class ptr 赋给 base class ptr 时, ptr 的值会根据 base objectderieve class 中的 layout 进行偏移

class base1
{
public:
    virtual void func1() {}
    int i;
};

class base2
{
public:
    virtual void func1() {}
};

class derieve1 : public base1, public base2
{
public:
    void func1() override {}
};

int main()
{
     derieve1* d1 = new derieve1;
     base1* b1 = d1;
     base2* b2 = d1;

     cout << b1 << " " << b2 << endl;
}

输出:

[global operator new] size = 24
0x18cc27918e0 0x18cc27918f0

class_layout1.png

如果涉及到了 virtual inheritance这个现象会更加明显( virtual继承下,如果基类和派生类没有 data,编译器会优化 ):

class base1
{
public:
    virtual void func1() {}
    int i;
};

class derieve2 : virtual public base1
{
public:
    int x;
};

class derieve3 : virtual public base1
{
public:

};

class derieve4 : public derieve2, public derieve3
{
    int i;
};

int main()
{
	derieve4* d3 = new derieve4;
     base1* b4 = d3;
     derieve2* d4 = d3;
     derieve3* d5 = d3;
     cout << d3 << endl;
     cout << d4 << " " << d5 << " " << b4 << endl;
}

输出为:

[global operator new] size = 48
0x18cc2791a60
0x18cc2791a60 0x18cc2791a70 0x18cc2791a80

class_layout2.png

base -> derived 运行时绑定

base class ptr 通过 dynamic_cast 转回 derieved class 也会改变指针的值:

derieve2* d6 = new derieve2;
base1* b5 = d6;
cout << d6 << " " << b5 << endl;
derieve2* d7 = dynamic_cast<derieve2*>(b5);
cout << d7 << endl;

输出为:

[global operator new] size = 32
0x18cc2791aa0 0x18cc2791ab0
0x18cc2791aa0

2. 同一个对象可以有多个合法地址(多继承)

一个类对象的指针可以与其所有父类的指针在 == 的比较下相同

class_layout10.png

class_layout8.png

class_layout9.png

实际上是子类对象指针在尝试想父类进行 static_cast

derieve1* d1 = new derieve1;
base1* b1 = d1;
base2* b2 = d1;

if(d1 == b2) cout << "d1 == b2" << endl;
if(d1 == b1) cout << "d1 == b1" << endl;
//if(b1 == b2) cout << "b1 == b2" << endl; // error

cout << b1 << " " << b2 << endl;
[global operator new] size = 24
d1 == b2
d1 == b1
0x1c8d9bd18e0 0x1c8d9bd18f0

3. 单继承下同一个对象也可能有多个地址!

class_layout11.png

using std::cout;

class Fruit
{
public:
    Fruit() {  }
    void whoAmI() { cout << "Fruit\n"; }
    void whoAmIReally() { cout << "Fruit\n"; }
    ~Fruit() = default ;
};

class Apple : public Fruit
{
public:
    Apple() {  }
    void whoAmI() { cout << "Apple\n"; }
    virtual void whoAmIReally() { cout << "Apple\n"; }
    virtual ~Apple() = default ;
};

int main()
{
    std::shared_ptr<Apple> e1 = std::make_shared<Apple>();
    std::shared_ptr<Fruit> e2 = e1;
    std::shared_ptr<Apple> e3 = std::dynamic_pointer_cast<std::shared_ptr<Apple>>(e2); // error
}

这种情况下 base class 是不能 dynamic_castderived class 的,因为 base class 没有 vptr

4. 虚拟继承和多继承下 查找 this 指针的问题

class_layout3.png

b2p 指向 base class B2class D 的内存模型中的位置,当其需要调用 D 重写的函数时,需要知道 Dthis 指针。

使用 immediate data ?

class_layout4.png

delta 是一条计算好的指令

class_layout7.png

查虚表?

这种方案可行,但是我认为编译器不会采取这个方案。因为从 derived -> base 这是 static_cast 也就是编译时完成的,所以所有 base class subobjectcomplete object 的偏移 delta 都是编译器知道的,不用差虚表也可以完成。

class_layout5.png

class_layout6.png

dynamic_cast

0. 虚表总是被 MDO 控制

  1. 虚表整体(schema)被对象的静态类型控制
  2. 虚表中的数据由对象(MDO)的动态类型控制

2. dynamic_cast 三个使用场景

Ⅰ. dynamic_cast to most-derived class

void* 来表示 MDO

dynamic_cast1.png

示例代码:

dynamic_cast2.png

dynamic_cast to sibling base

  1. 首先转为 MDO,查表,找到 typeid
  2. 如果 typeid(mdo) == source type,返回 null
  3. 否则调用 castToBase

dynamic_cast3.png

dynamic_cast4.png

dynamic_cast5.png

dynamic_cast6.png

dynamic_cast from base to derived

原理同 2

dynamic_cast7.png

注意:如果是将 derived class ptr 转为 base class ptr 使用的是 dynamic_cast ,实际上等同于 static_cast,这是编译期完成的类型转换,不是运行时绑定的。

Ⅳ. 一个例子

dynamic_cast8.png

ItanumMSVC 会产生只读的数据,根据类的继承体系建图,通过图算法查看 A 和 B 是否联通。

dynamic_cast9.png

3. 无二义且可访问

dynamic_cast10.png

三 C++ object model

An object is a region of storage

address order

object_model1.png

pod

object_model2.png

inheritance

object_model3.png

object_model4.png

name mangling

  1. 函数形参增加 this 指针
  2. 将函数末尾 cv 限定符移动到形参上
  3. 实参加上 this->
  4. 修改函数名称

object_model5.png

c++filt

class with non-static function

类中的函数并不代表类中存在一个指向该函数的指针。按照 C++ 的 0开销原则,与其让大量对象的大量指针指向相同的函数,不如让编译器去决定该调用那个函数。

object_model6.png

virtual function

object_model7.png

object_model8.png

使用智能指针!
using std::cout;

class Fruit
{
public:
    void whoAmI() { cout << "Fruit\n"; }
    virtual void whoAmIReally() { cout << "Fruit\n"; }
    virtual ~Fruit() = default ;
};

class Apple : public Fruit
{
public:
    void whoAmI() { cout << "Apple\n"; }
    void whoAmIReally() override { cout << "Apple\n"; }
    ~Apple() = default ;
};

int main()
{
    std::unique_ptr<Fruit> e1 = std::make_unique<Apple>();
    e1->whoAmI();
    e1->whoAmIReally();
}

输出:

[global operator new] size = 8
Fruit
Apple
[global operator delete] ptr = 0x25c6a2018e0

object_model9.png

object_model10.png

object_model11.png

vtable 中的值由谁设置?

虚函数表在数据段,虚函数在代码段。

构造函数 在运行期初始化 vtable

构造函数为你做的工作

对于 Complex 类:

  1. 在对象的最前面插入一个 vptr
  2. 在构造函数初始化列表执行前中初始化 vptr

object_model13.png

对于 Derived 类:

  1. 调用父类构造函数
  2. 修改 vptr 的值

object_model14.png

思考下面的问题:

object_model15.png

当父类构造函数中的 whoAmIReally 被调用时,vptr 指向的还是 Fruit 类的虚表!没不是 Apple 类的虚表。

可以这样理解:

当调用基类的构造函数时,vptr 可以看作是基类类型的

using std::cout;

class Fruit
{
public:
    Fruit() { whoAmIReally(); }
    void whoAmI() { cout << "Fruit\n"; }
    virtual void whoAmIReally() { cout << "Fruit\n"; }
    virtual ~Fruit() = default ;
};

class Apple : public Fruit
{
public:
    Apple() {  }
    void whoAmI() { cout << "Apple\n"; }
    void whoAmIReally() override { cout << "Apple\n"; }
    ~Apple() = default ;
};

int main()
{
    Apple e1;
}

object_model16.png

override 很关键

尤其是在使用库函数时,如果不写 override ,那么会改变函数标签!你就在写一个全新的函数!