我有两个类 - 一个基础类和一个从它派生:
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 ) ; }
要么
class base {
int i ;
public :
~ base () { }
};
class derived : virtual public base { int j ; };
main()
{ cout << sizeof ( derived ) ; }
在这两种情况下的答案是12。
有人可以请解释为什么会出现在第一个派生类和另2例的大小有区别吗?
(我的代码::块10.05工作,如果有人真的需要这个)
虚拟继承的要点是允许基类的共享。 这里的问题:
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内的基地,还是内部derived1基地。
如果你改变了二三线如下,问题消失:
struct derived0 : virtual base { int d0; };
struct derived1 : virtual base { int d1; };
你J对象现在只有基地,而不是两个中的一个副本,所以最后的4行不再是模糊的。
不过想想如何必须被执行。 通常情况下,在derived0中,D0配备m个之后,并在derived1时,D1将M之后说到。 但是,随着虚拟继承,他们都共享相同的男,所以你不能兼得D0和后马上D1来。 所以,你会需要某种形式的额外间接的。 这就是额外的指针从何而来。
如果你想完全布局就是知道,这取决于你的目标平台和编译器。 只是“海湾合作委员会”是不够的。 但是,对于许多现代非Windows的目标,答案由安腾C ++ ABI,它是在文件中定义http://mentorembedded.github.com/cxx-abi/abi.html#vtable 。
这里有两个独立的事情,导致额外的开销。
首先,在基类具有虚拟函数的指针的大小(在这种情况下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 |
如果一个类有任何虚函数,这个类的对象需要有一个vptr的,这是一个指向虚函数表,那是从哪儿正确的虚函数的地址,可以发现虚表。 调用的函数依赖于动态型的对象,这是最派生类对象是一个基子对象。
因为派生类从基类继承实际上,基类相对于派生类的位置是不固定的,这取决于动态类型的对象的太。 用gcc与虚基类的类需要的vptr定位基类(即使不存在虚拟函数)。
此外,基类包含一个数据成员,这是刚刚基类的vptr后定位。 基类存储器布局是:{的vptr, int
}
如果一个基类的vptr需要,从它派生的类将需要的vptr过,但往往有一个基类子对象的“第一”的vptr被重复使用(与重复使用的vptr这个基类被称为主基地)。 然而这是不可能在这种情况下,因为派生类需要的vptr不仅要确定如何调用虚函数,而且在虚拟基地。 不使用的vptr派生类无法找到它的虚基类; 如果虚拟基类被用作主基站, 派生类将需要用来确定它的主基站来读取的vptr,并且将需要读取的vptr定位其主基站 。
因此派生不能有一个主基地,并介绍了自己的vptr。
类型的基类子对象的布局derived
是这样的:{的vptr, int
}与vptr的指向为衍生的虚函数表 ,不仅含有虚函数的地址,但也所有虚拟基类的相对位置(在此只是base
),表示作为偏移。
类型的一个完整的对象的布局derived
为:{式的基类子对象derived
, base
}
这样的最小可能尺寸derived
为(2 int
+ 2的vptr)或共同的ptr = 4个字int
=字体系结构,或在此情况下16个字节。 (和Visual C ++使得更大的对象(当虚拟基类都参与),相信derived
将具有一个以上的指针。)
所以,是的, 虚函数有一个成本,虚拟继承是有成本的。 虚拟继承的这种情况下的存储器费用是每个对象一个更指针。
在具有许多虚拟基类的设计中,每个对象的存储器成本可能是成比例的虚拟基类,或不的数目; 我们需要讨论具体的类层次结构来估算成本。
在没有多重继承或虚基类(甚至是虚拟函数)的设计,你可能要效仿很多事情由编译器自动完成的,带着一帮指针,可能函数指针,可能抵消......这有可能会混乱且容易出错。
这是怎么回事用来标志一类具有虚拟成员或涉及虚拟继承了额外的开销。 多少额外的依赖于编译器。
小心的标志:制作类与类其析构函数不是虚拟的,通常找麻烦派生。 大麻烦。
可能需要额外的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);
}
额外的大小是由于虚函数表/虚函数表指针是“无形”添加到您的类,以保持成员函数指针,这个类的特定对象或它的后代/祖先。
如果说不清楚,你需要做更多的阅读关于C ++虚继承。