我理解C ++虚拟继承的基础知识。但是,我很困惑我需要将virtual
关键字与复杂的类层次结构一起使用。例如,假设我有以下类:
A
/ \
B C
/ \ / \
D E F
\ / \ /
G H
\ /
I
如果我想确保在任何子类中都没有出现过多次类,那么需要将哪些基类标记为virtual
?他们都是?或者只在那些直接从一个类派生的类上使用它就足够了,否则这个类可能有多个实例(即B,C,D,E和F;以及G和H(但只有基类E,而不是基类D和F))?
答案 0 :(得分:23)
我一起玩弄了一个程序,可以帮助你研究虚拟基地的复杂性。它将I
下的类层次结构打印为适合graphiviz(http://www.graphviz.org/)的有向图。每个实例都有一个计数器,可以帮助您了解施工顺序。这是程序:
#include <stdio.h>
int counter=0;
#define CONN2(N,X,Y)\
int id; N() { id=counter++; }\
void conn() \
{\
printf("%s_%d->%s_%d\n",#N,this->id,#X,((X*)this)->id); \
printf("%s_%d->%s_%d\n",#N,this->id,#Y,((Y*)this)->id); \
X::conn(); \
Y::conn();\
}
#define CONN1(N,X)\
int id; N() { id=counter++; }\
void conn() \
{\
printf("%s_%d->%s_%d\n",#N,this->id,#X,((X*)this)->id); \
X::conn(); \
}
struct A { int id; A() { id=counter++; } void conn() {} };
struct B : A { CONN1(B,A) };
struct C : A { CONN1(C,A) };
struct D : B { CONN1(D,B) };
struct E : B,C { CONN2(E,B,C) };
struct F : C { CONN1(F,C) };
struct G : D,E { CONN2(G,D,E) };
struct H : E,F { CONN2(H,E,F) };
struct I : G,H { CONN2(I,G,H) };
int main()
{
printf("digraph inh {\n");
I i;
i.conn();
printf("}\n");
}
如果我运行它(g++ base.cc ; ./a.out >h.dot ; dot -Tpng -o o.png h.dot ; display o.png
),我会得到典型的非虚拟基础树:
alt text http://i34.tinypic.com/2ns6pt4.png
添加足够的虚拟......
struct B : virtual A { CONN1(B,A) };
struct C : virtual A { CONN1(C,A) };
struct D : virtual B { CONN1(D,B) };
struct E : virtual B, virtual C { CONN2(E,B,C) };
struct F : virtual C { CONN1(F,C) };
struct G : D, virtual E { CONN2(G,D,E) };
struct H : virtual E,F { CONN2(H,E,F) };
struct I : G,H { CONN2(I,G,H) };
..导致钻石形状(查看数字以了解施工顺序!!)
alt text http://i33.tinypic.com/xpa2l5.png
但是如果你把所有的基础虚拟化:
struct A { int id; A() { id=counter++; } void conn() {} };
struct B : virtual A { CONN1(B,A) };
struct C : virtual A { CONN1(C,A) };
struct D : virtual B { CONN1(D,B) };
struct E : virtual B, virtual C { CONN2(E,B,C) };
struct F : virtual C { CONN1(F,C) };
struct G : virtual D, virtual E { CONN2(G,D,E) };
struct H : virtual E, virtual F { CONN2(H,E,F) };
struct I : virtual G,virtual H { CONN2(I,G,H) };
你得到一个菱形,其初始化顺序不同:
alt text http://i33.tinypic.com/110dlj8.png
玩得开心!
答案 1 :(得分:7)
从A,B,C和E类中的任何一个(位于钻石顶部)继承时,必须指定virtual
继承。
class A;
class B: virtual A;
class C: virtual A;
class D: virtual B;
class E: virtual B, virtual C;
class F: virtual C;
class G: D, virtual E;
class H: virtual E, F;
class I: G, H;
答案 2 :(得分:2)
我的个人建议是从B和C开始:虚拟A,然后继续添加,直到编译器停止抱怨。
实际上,我会说B和C:虚拟A,G和H:虚拟E,以及E:虚拟B和C.所有其他继承链接都可以是正常继承。然而,这种怪异需要花费六十年才能进行虚拟通话。
答案 3 :(得分:1)
如果要确保层次结构中顶级类的对象(在您的情况下为I
)只包含每个父类的一个子对象,则必须找到层次结构中具有<的所有类。 em>多个超类并使这些类成为超类的虚拟基础。就是这样。
在您的案例类A
中,B
,C
和E
必须在每次从此层次结构中继承时成为虚拟基类。
类D
,F
,G
和H
不必成为虚拟基类。
答案 4 :(得分:0)
如果你想为每种类型的每个实例只有一个“物理”实例(只有一个A,只有一个B等)。每次使用继承时,你只需要使用虚拟继承。 / p>
如果您想要其中一种类型的单独实例,请使用普通继承。
答案 5 :(得分:0)
已编辑:我认为A是派生程度最高的类;)
@Luther的回答非常酷,但回到原来的问题:当继承自继承层次结构中至少有一个其他类继承的任何类时,您需要使用virtual
继承(在Luther的图中,它表示至少有两个箭头指向该类)。
此处在D
,F
,G
和H
之前没有必要,因为只有一个类派生自它们(目前没有派生自I
)。
但是,如果您事先不知道另一个类是否会从您的基类继承,则可以添加virtual
作为预防措施。例如,建议Exception
类从Stroustrup自己实际上从std::exception
继承。
正如路德所指出的,它修改了实例化顺序(并且对性能产生了轻微的影响),但我认为任何依赖于构造顺序的设计都是错误的。而且就像精度一样:你仍然保证在派生类的任何属性之前初始化基类,因此在执行派生的构造函数体之前。
答案 6 :(得分:0)
要记住的事情是C ++保留一个继承表。添加虚拟类越多,编译时间(链接)就越长,运行时就越重。
一般情况下,如果可以避免虚拟类,可以用某些模板替换或尝试以某种方式解耦。