我有两个类 - 一个基类,一个派生自它:
class base {
int i ;
public :
virtual ~ base () { }
};
class derived : virtual public base { int j ; };
main()
{ cout << sizeof ( derived ) ; }
这里答案是16.但是如果我改为非虚拟公共继承或使基类非多态,那么我得到12的答案,即如果我这样做:
class base {
int i ;
public :
virtual ~ base () { }
};
class derived : public base { int j ; };
main()
{ cout << sizeof ( derived ) ; }
OR
class base {
int i ;
public :
~ base () { }
};
class derived : virtual public base { int j ; };
main()
{ cout << sizeof ( derived ) ; }
在这两种情况下,答案是12.
有人可以解释一下为什么第一个和另外两个案例中派生类的大小有差异?
(我在代码::块10.05上工作,如果有人真的需要这个)
答案 0 :(得分:3)
这里有两个不同的东西会导致额外的开销。
首先,在基类中使用虚函数会增加指针大小(在本例中为4个字节),因为它需要将指针存储到虚方法表中:
normal inheritance with virtual functions:
0 4 8 12
| base |
| vfptr | i | j |
其次,在虚拟继承中,derived
需要额外的信息才能找到base
。在正常继承中,derived
和base
之间的偏移量是编译时常量(单继承为0)。在虚拟继承中,偏移量可以取决于对象的运行时类型和实际类型层次结构。实现可能会有所不同,但例如Visual C ++就是这样的:
virtual inheritance with virtual functions:
0 4 8 12 16
| base |
| xxx | j | vfptr | i |
其中xxx
是指向某个类型信息记录的指针,允许确定base
的偏移量。
当然,没有虚函数就可以实现虚拟继承:
virtual inheritance without virtual functions:
0 4 8 12
| base |
| xxx | j | i |
答案 1 :(得分:3)
如果一个类有任何虚函数,则该类的对象需要有一个vptr,即指向vtable的指针,即可以找到正确虚函数地址的虚拟表。调用的函数取决于对象的动态类型,它是对象是基础子对象的派生程度最高的类。
因为派生类实际上是从基类继承的,所以基类相对于派生类的位置不是固定的,它也取决于对象的动态类型。使用gcc,具有虚拟基类的类需要vptr来定位基类(即使没有虚函数)。
此外,基类包含一个数据成员,它位于基类vptr之后。基类内存布局为:{vptr,int
}
如果基类需要vptr,从它派生的类也需要一个vptr,但通常会重用基类子对象的“第一个”vptr(这个带有重用vptr的基类称为主基类)。但是在这种情况下这是不可能的,因为派生类不仅需要vptr来确定如何调用虚函数,还需要确定虚拟基的位置。派生类在不使用vptr的情况下无法定位其虚拟基类;如果虚拟基类被用作主要基类,则派生类需要找到其主要基础来读取vptr,并且需要读取vptr以找到其主要基础。
所以派生的不能有主要基础,它引入了自己的vptr 。
类型为derived
的基类子对象的布局是:{vptr,int
},其中vptr指向用于派生的vtable ,不仅包含虚函数的地址,还包含其所有虚拟基类的相对位置(此处仅为base
),表示为偏移量。
类型为derived
的完整对象的布局为:{{class 1}}类型的基类子对象,derived
}
因此base
的最小可能大小为(2 derived
+ 2 vptr)或共有ptr = int
=字架构上的4个字,或者在这种情况下为16字节。 (而Visual C ++生成更大的对象(当涉及虚拟基类时),我相信int
会有一个指针。)
是的,虚拟功能有成本,虚拟继承有成本。在这种情况下,虚拟继承的内存开销是每个对象多一个指针。
在具有许多虚拟基类的设计中,每个对象的内存成本可能与虚拟基类的数量成正比,与否;我们需要讨论特定的类层次结构来估算成本。
在没有多重继承或虚拟基类(甚至是虚函数)的设计中,您可能需要模拟编译器为您自动完成的许多事情,包括一堆指针,可能是指向函数的指针,可能是偏移...这可能会让人感到困惑和容易出错。
答案 2 :(得分:2)
正在发生的是用于将类标记为具有虚拟成员或涉及虚拟继承的额外开销。多少取决于编译器。
注意事项:使一个类派生自一个析构函数不是虚拟的类,通常会遇到麻烦。很麻烦。
答案 3 :(得分:2)
在运行时标记类类型可能需要额外的4个字节。 例如:
class A {
virtual int f() { return 2; }
}
class B : virtual public A {
virtual int f() { return 3; }
}
int call_function( A *a) {
// here we don't know what a really is (A or B)
// because of this to call correct method
// we need some runtime knowledge of type and storage space to put it in (extra 4 bytes).
return a->f();
}
int main() {
B b;
A *a = (A*)&b;
cout << call_function(a);
}
答案 4 :(得分:2)
虚拟继承的目的是允许共享基类。这是问题所在:
struct base { int member; virtual void method() {} };
struct derived0 : base { int d0; };
struct derived1 : base { int d1; };
struct join : derived0, derived1 {};
join j;
j.method();
j.member;
(base *)j;
dynamic_cast<base *>(j);
最后4行都是模棱两可的。您必须明确表示您是想在derived0中使用base还是在derived1中使用base。
如果按如下方式更改第二行和第三行,则问题就会消失:
struct derived0 : virtual base { int d0; };
struct derived1 : virtual base { int d1; };
你的j对象现在只有一个base副本,而不是两个,所以最后4行不会有歧义。
但请考虑如何实施。通常,在derived0中,d0紧跟在m之后,而在derived1中,d1恰好在m之后。但是对于虚拟继承,它们都共享相同的m,所以你不能同时拥有d0和d1。所以你需要某种形式的额外间接。这就是额外指针的来源。
如果您想确切知道布局是什么,它取决于您的目标平台和编译器。只是“gcc”是不够的。但是对于许多现代非Windows目标,答案由Itanium C ++ ABI定义,它在http://mentorembedded.github.com/cxx-abi/abi.html#vtable中有记录。
答案 5 :(得分:0)
额外的大小是由于vtable / vtable指针被“无形地”添加到你的类中,以便保存该类或它的后代/祖先的特定对象的成员函数指针。
如果不清楚,你需要在C ++中做更多关于虚拟继承的阅读。