在《C++内存布局(一)》 中,我们介绍了C++内存布局的基本知识,本篇我们仍着重探讨C++类的内存布局,尤其是 多重继承
、钻石继承(菱形继承)
场景下的虚函数表的情况。
class A
{
public:
virtual void da(){MYTRACE();}
};
class B
{
public:
virtual void db(){MYTRACE();}
};
class C: public A, public B
{
public:
virtual void dc(){MYTRACE();}
};
int main()
{
A a;
B b;
C c;
return 0;
}
C 中只有两张表
[A::vptr] 继承自基类A中的虚函数表不仅记录了class A的虚函数地址,同时记录了
class B中的虚函数地址,而且还记录了class C 自己定义的虚函数地址。另外还记录一些其他的关联信息。
[B:vptr] 只记录了class B中虚函数的地址
class A
{
public:
virtual void da(){MYTRACE();}
};
class B
{
public:
virtual void db(){MYTRACE();}
};
相比1.1中的代码示例,我们在这里交互继承顺序
class C: public B, public A
{
public:
virtual void dc(){MYTRACE();}
};
int main()
{
A a;
B b;
C c;
return 0;
}
好家伙,多重继承掉个顺序,这下子表的顺序也调整了。
看来,在多重继承中,如果继承的多个基类存在多张虚函数表,会按照继承顺序,优先选择第一个基类中的虚函数表作为[基表] (自创词汇,勿cue)将所有关联的虚函数信息记录在此表中。但是表的数量仍取决于基类中虚函数表的总数。
class A
{
public:
virtual void da(){MYTRACE();}
};
class B
{
public:
virtual void db(){MYTRACE();}
};
/// 虚继承自A
class C: public virtual A, public B
{
public:
virtual void dc(){MYTRACE();}
};
int main()
{
A a;
B b;
C c;
return 0;
}
这种情况(多重继承非全虚继承)下,还是只有两张虚函数表(取决于所有基类中虚函数表总数);不过有趣的是,这次竟然没有按照继承顺序优先选择【基表】,而是在未采用虚继承的class B
中 B::vptr
里优先记录了所有的虚函数地址及关联的一些信息。所以我们貌似总结出了一些规律:
虚继承 > 继承顺序
同样,在1.3的基础上,我们把虚继承换个顺序,看看结果怎么样
class A
{
public:
virtual void da(){MYTRACE();}
};
class B
{
public:
virtual void db(){MYTRACE();}
};
class C: public A, virtual public B
{
public:
virtual void dc(){MYTRACE();}
};
int main()
{
A a;
B b;
C c;
return 0;
}
结果得到验证,与我们在1.3中得到规律的预期一致。表还是两张,在未采用虚继承的class A
中 A::vptr
里优先记录了所有的虚函数地址及关联的一些信息。
class A
{
public:
virtual void da(){MYTRACE();}
};
class B
{
public:
virtual void db(){MYTRACE();}
};
class C: virtual public B, public A
{
public:
virtual void dc(){MYTRACE();}
};
int main()
{
A a;
B b;
C c;
return 0;
}
交换了顺序,与我们在1.4中得到的结果依然一致。可见虚继承 优于 继承顺序
,在我们目前用于验证的VS2019 x64 的编译工具链上确实是这样的。
这次我们in-all 了,多重继承的全部改为虚继承,我们再来探究下。
class A
{
public:
virtual void da(){MYTRACE();}
};
class B
{
public:
virtual void db(){MYTRACE();}
};
class C: virtual public A, virtual public B
{
public:
virtual void dc(){MYTRACE();}
virtual void dc1(){MYTRACE();}
};
int main()
{
A a;
B b;
C c;
return 0;
}
喔嚯,这次大不同啊,大不同!在全部采用虚继承的情况下,编译器为class C
创建了一张新的表C::vptr
,用于记录class A
、class B
和class C
中的虚函数地址以及其他关联信息。三张表啊三张表。
class A
{
public:
virtual void da(){MYTRACE();}
};
class B
{
public:
virtual void db(){MYTRACE();}
};
class C: virtual public A, virtual public B
{
public:
virtual void dc(){MYTRACE();}
virtual void dc1(){MYTRACE();}
};
class D : public C
{
};
int main()
{
A a;
B b;
C c;
D d;
return 0;
}
我们看菱形继承到class D
这儿,D
的实例化对象d
中包含了多张虚函数表,包含
A::vptr
B::vptr
C::vptr
C::A::vptr
⇒ 这里我只是用来表明层级关系,::
在此并不表示作用域,别纠结
C::B::vptr
是不是很妙。
如果class D
也定义了虚函数呢,会创建新的表么?如下,显然没有,D
的虚函数地址被记录在了继承自C
的C::vptr
下。
果如其然,虚继承情况下创建了一张新的表,用于单独记录class D
中虚函数地址。
class A
{
public:
virtual void d(){MYTRACE();}
virtual void da(){MYTRACE();}
};
class B : virtual public A
{
public:
virtual void db(){MYTRACE();}
};
class C: virtual public A
{
public:
virtual void dc(){MYTRACE();}
};
/// 非虚继承
class D : public B, public C
{
public:
virtual void dd(){}
};
int main()
{
D d;
d.d();
return 0;
}
A
/ \
B C
\ /
D
class A
{
public:
virtual void d(){MYTRACE();}
virtual void da(){MYTRACE();}
};
/// 菱形继承必须使用虚继承
class B : virtual public A
{
public:
virtual void db(){MYTRACE();}
};
/// 菱形继承必须使用虚继承
class C: virtual public A
{
public:
virtual void dc(){MYTRACE();}
};
/// 全虚继承
class D : virtual public B, virtual public C
{
public:
virtual void dd(){}
};
int main()
{
D d;
d.d();
return 0;
}
vptr
)的数量取决于所有继承的父类中分别包含的虚函数表的总数。如果子类也有自定义的虚函数,按照基类继承顺序的优先级, 此虚函数地址被记录在继承的第一个基类的虚函数表中;vptr
)的数量取决于所有继承的父类中分别包含的虚函数表的总数N
。如果子类也有自定义的虚函数,按照基类继承顺序的优先级, 此虚函数地址被记录在继承的第一个被非虚继承的虚基类的虚函数表中,子类中包含的虚函数表总数N
;如果第一个被非虚继承的虚基类不存在,则会为子类新建一张虚函数表vptr[]
,此时子类中包含的虚函数表总数N+1
;vptr
)的数量取决于所有继承的父类中分别包含的虚函数表的总数N
。如果子类也有自定义的虚函数,那编译器会为子类单独创建一张虚函数表,用于记录子类中自己定义的虚函数地址。此时,子类中包含的虚函数表的总数为N+1
virtual
虚继承,避免函数名重复导致编译错误;虚函数表及地址的记录规则与上面几条保持一致。以上,属于个人总结,纰漏指出请不吝指正~~