1. 普通类对象是什么布局?
struct Base {Base() = default;~Base() = default;void Func() {}int a;int b;
};int main() {Base a;return 0;
}
2. 带虚函数的类对象是什么布局?
struct Base {Base() = default;virtual ~Base() = default;void FuncA() {}virtual void FuncB() {printf("FuncB\n");}int a;int b;
};int main() {Base a;return 0;
}
这个含有虚函数的结构体大小为16,在对象的头部,前8个字节是虚函数表的指针,指向虚函数的相应函数指针地址,a占4个字节,b占4个字节,总大小为16。
我们来探秘下传说中的虚函数表:
offset_to_top(0):表示当前这个虚函数表地址距离对象顶部地址的偏移量,因为对象的头部就是虚函数表的指针,所以偏移量为0。
RTTI指针:指向存储运行时类型信息(type_info)的地址,用于运行时类型识别,用于typeid和dynamic_cast。
RTTI下面就是虚函数表指针真正指向的地址啦,存储了类里面所有的虚函数,至于这里为什么会有两个析构函数,大家可以先关注对象的布局,最下面会介绍。
3. 单继承下不含有覆盖函数的类对象是什么布局?
struct Base {Base() = default;virtual ~Base() = default;void FuncA() {}virtual void FuncB() {printf("Base FuncB\n");}int a;int b;
};struct Derive : public Base{
};int main() {Base a;Derive d;return 0;
}
注意下虚函数表这里的FuncB函数,还是Base类中的FuncB,因为在子类中没有重写这个函数,那么如果子类重写这个函数后对象布局是什么样的,请继续往下看哈。
4. 单继承下含有覆盖函数的类对象是什么布局?
struct Base {Base() = default;virtual ~Base() = default;void FuncA() {}virtual void FuncB() {printf("Base FuncB\n");}int a;int b;
};struct Derive : public Base{void FuncB() override {printf("Derive FuncB \n");}
};int main() {Base a;Derive d;return 0;
}
注意这里虚函数表中的FuncB函数已经是Derive中的FuncB啦,因为在子类中重写了父类的这个函数。
再注意这里的RTTI中有了两项,表示Base和Derive的虚表地址是相同的,Base类里的虚函数和Derive类里的虚函数都在这个链条下,这里可以继续关注下面多继承的情况,看看有何不同。
5. 多继承下不含有覆盖函数的类对象是什么布局?
struct BaseA {BaseA() = default;virtual ~BaseA() = default;void FuncA() {}virtual void FuncB() {printf("BaseA FuncB\n");}int a;int b;
};struct BaseB {BaseB() = default;virtual ~BaseB() = default;void FuncA() {}virtual void FuncC() {printf("BaseB FuncC\n");}int a;int b;
};struct Derive : public BaseA, public BaseB{
};int main() {BaseA a;Derive d;return 0;
}
offset_to_top(0):表示当前这个虚函数表(BaseA,Derive)地址距离对象顶部地址的偏移量,因为对象的头部就是虚函数表的指针,所以偏移量为0。
再注意这里的RTTI中有了两项,表示BaseA和Derive的虚表地址是相同的,BaseA类里的虚函数和Derive类里的虚函数都在这个链条下,截至到offset_to_top(-16)之前都是BaseA和Derive的虚函数表。
offset_to_top(-16):表示当前这个虚函数表(BaseB)地址距离对象顶部地址的偏移量,因为对象的头部就是虚函数表的指针,所以偏移量为-16,这里用于this指针偏移,下一小节会介绍。
注意下后面的这个RTTI:只有一项,表示BaseB的虚函数表,后面也有两个虚析构函数,为什么有四个Derive类的析构函数呢,又是怎么调用呢,请继续往下看~
6. 多继承下含有覆盖函数的类对象的是什么布局?
struct BaseA {BaseA() = default;virtual ~BaseA() = default;void FuncA() {}virtual void FuncB() {printf("BaseA FuncB\n");}int a;int b;
};struct BaseB {BaseB() = default;virtual ~BaseB() = default;void FuncA() {}virtual void FuncC() {printf("BaseB FuncC\n");}int a;int b;
};struct Derive : public BaseA, public BaseB{void FuncB() override {printf("Derive FuncB \n");}void FuncC() override {printf("Derive FuncC \n");}
};int main() {BaseA a;Derive d;return 0;
}
7. 多继承中不同的继承顺序产生的类对象布局相同吗?
struct BaseA {BaseA() = default;virtual ~BaseA() = default;void FuncA() {}virtual void FuncB() {printf("BaseA FuncB\n");}int a;int b;
};struct BaseB {BaseB() = default;virtual ~BaseB() = default;void FuncA() {}virtual void FuncC() {printf("BaseB FuncC\n");}int a;int b;
};struct Derive : public BaseB, public BaseA{void FuncB() override {printf("Derive FuncB \n");}void FuncC() override {printf("Derive FuncC \n");}
};int main() {BaseA a;Derive d;return 0;
}
BaseB的虚函数表指针和数据在上面,BaseA的虚函数表指针和数据在下面,以A,B的顺序继承,对象的布局就是A在上B在下,以B,A的顺序继承,对象的布局就是B在上A在下。
8. 虚继承的类对象是什么布局?
struct Base {Base() = default;virtual ~Base() = default;void FuncA() {}virtual void FuncB() {printf("BaseA FuncB\n");}int a;int b;
};struct Derive : virtual public Base{void FuncB() override {printf("Derive FuncB \n");}
};int main() {Base a;Derive d;return 0;
}
虚继承下,这里的对象布局和普通单继承有所不同,普通单继承下子类和基类共用一个虚表地址,而在虚继承下,子类和虚基类分别有一个虚表地址的指针,两个指针大小总和为16,再加上a和b的大小8,为24。
9. 菱形继承下类对象是什么布局?
struct Base {Base() = default;virtual ~Base() = default;void FuncA() {}virtual void FuncB() {printf("BaseA FuncB\n");}int a;int b;
};struct BaseA : virtual public Base {BaseA() = default;virtual ~BaseA() = default;void FuncA() {}virtual void FuncB() {printf("BaseA FuncB\n");}int a;int b;
};struct BaseB : virtual public Base {BaseB() = default;virtual ~BaseB() = default;void FuncA() {}virtual void FuncC() {printf("BaseB FuncC\n");}int a;int b;
};struct Derive : public BaseB, public BaseA{void FuncB() override {printf("Derive FuncB \n");}void FuncC() override {printf("Derive FuncC \n");}
};int main() {BaseA a;Derive d;return 0;
}
10. 为什么要引入虚继承?
非虚继承时,显然D会继承两次A,内部就会存储两份A的数据浪费空间,而且还有二义性,D调用A的方法时,由于有两个A,究竟时调用哪个A的方法呢,编译器也不知道,就会报错,所以有了虚继承,解决了空间浪费以及二义性问题。在虚拟继承下,只有一个共享的基类子对象被继承,而无论该基类在派生层次中出现多少次。共享的基类子对象被称为虚基类。在虚继承下,基类子对象的复制及由此而引起的二义性都被消除了。
11. 为什么虚函数表中有两个析构函数?
前面的代码输出中我们可以看到虚函数表中有两个析构函数,一个标志为deleting,一个标志为complete,因为对象有两种构造方式,栈构造和堆构造,所以对应的实现上,对象也有两种析构方式,其中堆上对象的析构和栈上对象的析构不同之处在于,栈内存的析构不需要执行 delete 函数,会自动被回收。
12. 为什么构造函数不能是虚函数?
构造函数就是为了在编译阶段确定对象的类型以及为对象分配空间,如果类中有虚函数,那就会在构造函数中初始化虚函数表,虚函数的执行却需要依赖虚函数表。如果构造函数是虚函数,那它就需要依赖虚函数表才可执行,而只有在构造函数中才会初始化虚函数表,鸡生蛋蛋生鸡的问题,很矛盾,所以构造函数不能是虚函数。
13. 为什么基类析构函数需要是虚函数?
一般基类的析构函数都要设置成虚函数,因为如果不设置成虚函数,在析构的过程中只会调用到基类的析构函数而不会调用到子类的析构函数,可能会产生内存泄漏。
14. 非继承下的C++对象模型
class Base
{
public:Base(int i) :baseI(i){};int getI(){ return baseI; }static void countI(){};virtual ~Base(){}virtual void print(void){ cout << "Base::print()"; }
private:int baseI;static int baseS;
};
概述:在此模型下,nonstatic 数据成员被置于每一个类对象中,而static数据成员被置于类对象之外。static与nonstatic函数也都放在类对象之外,而对于virtual 函数,则通过虚函数表+虚指针来支持,具体如下:
- 每个类生成一个表格,称为虚表(virtual table,简称vtbl)。虚表中存放着一堆指针,这些指针指向该类每一个虚函数。虚表中的函数地址将按声明时的顺序排列,不过当子类有多个重载函数时例外,后面会讨论。
- 每个类对象都拥有一个虚表指针(vptr),由编译器为其生成。虚表指针的设定与重置皆由类的复制控制(也即是构造函数、析构函数、赋值操作符)来完成。vptr的位置为编译器决定,传统上它被放在所有显示声明的成员之后,不过现在许多编译器把vptr放在一个类对象的最前端。关于数据成员布局的内容,在后面会详细分析。
- 另外,虚函数表的前面设置了一个指向type_info的指针,用以支持RTTI(Run Time Type Identification,运行时类型识别)。RTTI是为多态而生成的信息,包括对象继承关系,对象本身的描述等,只有具有虚函数的对象在会生成。
15. 继承下的C++对象模型
15.1 单继承
class Derive : public Base
{
public:Derive(int d) :Base(1000), DeriveI(d){};//overwrite父类虚函数virtual void print(void){ cout << "Drive::Drive_print()" ; }// Derive声明的新的虚函数virtual void Drive_print(){ cout << "Drive::Drive_print()" ; }virtual ~Derive(){}
private:int DeriveI;
};
在C++对象模型中,对于一般继承(这个一般是相对于虚拟继承而言),若子类重写(overwrite)了父类的虚函数,则子类虚函数将覆盖虚表中对应的父类虚函数(注意子类与父类拥有各自的一个虚函数表);若子类并无overwrite父类虚函数,而是声明了自己新的虚函数,则该虚函数地址将扩充到虚函数表最后(在vs中无法通过监视看到扩充的结果,不过我们通过取地址的方法可以做到,子类新的虚函数确实在父类子物体的虚函数表末端)。而对于虚继承,若子类overwrite父类虚函数,同样地将覆盖父类子物体中的虚函数表对应位置,而若子类声明了自己新的虚函数,则编译器将为子类增加一个新的虚表指针vptr,这与一般继承不同,在后面再讨论。
15.2 多继承
单继承中(一般继承),子类会扩展父类的虚函数表。在多继承中,子类含有多个父类的子对象,该往哪个父类的虚函数表扩展呢?当子类overwrite了父类的函数,需要覆盖多个父类的虚函数表吗?
- 子类的虚函数被放在声明的第一个基类的虚函数表中。
- overwrite时,所有基类的print()函数都被子类的print()函数覆盖。
- 内存布局中,父类按照其声明顺序排列。
其中第二点保证了父类指针指向子类对象时,总是能够调用到真正的函数。
15.3 菱形继承
16. 虚继承
虚继承解决了菱形继承中最派生类拥有多个间接父类实例的情况。虚继承的派生类的内存布局与普通继承很多不同,主要体现在:
- 虚继承的子类,如果本身定义了新的虚函数,则编译器为其生成一个虚函数指针(vptr)以及一张虚函数表。该vptr位于对象内存最前面。
- vs非虚继承:直接扩展父类虚函数表。
- 虚继承的子类也单独保留了父类的vptr与虚函数表。这部分内容接与子类内容以一个四字节的0来分界。
- 虚继承的子类对象中,含有四字节的虚表指针偏移值。
为了分析最后的菱形继承,我们还是先从单虚继承继承开始
16.1 虚基类表
在C++对象模型中,虚继承而来的子类会生成一个隐藏的虚基类指针(vbptr),在Microsoft Visual C++中,虚基类表指针总是在虚函数表指针之后,因而,对某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在实例的0字节偏移处(该类没有vptr时,vbptr就处于类实例内存布局的最前面,否则vptr处于类实例内存布局的最前面),也可能在类实例的4字节偏移处。
一个类的虚基类指针指向的虚基类表,与虚函数表一样,虚基类表也由多个条目组成,条目中存放的是偏移值。第一个条目存放虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,由第一段的分析我们知道,这个偏移值为0(类没有vptr)或者-4(类有虚函数,此时有vptr)。我们通过一张图来更好地理解。
16.2 简单虚继承
class B{...}
class B1 : virtual public B
16.3 虚拟菱形继承
class B{...}
class B1: virtual public B{...}
class B2: virtual public B{...}
class D : public B1,public B2{...}
菱形虚拟继承下,最派生类D类的对象模型又有不同的构成了。在D类对象的内存构成上,有以下几点:
- 在D类对象内存中,基类出现的顺序是:先是B1(最左父类),然后是B2(次左父类),最后是B(虚祖父类)
- D类对象的数据成员id放在B类前面,两部分数据依旧以0来分隔。
- 编译器没有为D类生成一个它自己的vptr,而是覆盖并扩展了最左父类的虚基类表,与简单继承的对象模型相同。
- 超类B的内容放到了D类对象内存布局的最后。
函数,此时有vptr)。我们通过一张图来更好地理解。
16.2 简单虚继承
class B{...}
class B1 : virtual public B
16.3 虚拟菱形继承
class B{...}
class B1: virtual public B{...}
class B2: virtual public B{...}
class D : public B1,public B2{...}
菱形虚拟继承下,最派生类D类的对象模型又有不同的构成了。在D类对象的内存构成上,有以下几点:
- 在D类对象内存中,基类出现的顺序是:先是B1(最左父类),然后是B2(次左父类),最后是B(虚祖父类)
- D类对象的数据成员id放在B类前面,两部分数据依旧以0来分隔。
- 编译器没有为D类生成一个它自己的vptr,而是覆盖并扩展了最左父类的虚基类表,与简单继承的对象模型相同。
- 超类B的内容放到了D类对象内存布局的最后。