14

我了解 C++ 虚拟继承的基础知识。但是,我对在哪里需要使用virtual具有复杂类层次结构的关键字感到困惑。例如,假设我有以下类:

            A
           / \
          B   C
         / \ / \
        D   E   F
         \ / \ /
          G   H
           \ /
            I

如果我想确保没有任何类在任何子类中出现多次,则需要标记哪些基类virtual?他们都是?或者仅在那些直接从可能具有多个实例的类(即 B、C、D、E 和 F;以及 G 和 H)派生的类上使用它就足够了(但仅与基类 E 一起使用,而不是与基类 D 和 F))?

4

7 回答 7

26

我一起玩了一个程序,它可以帮助你研究虚拟基地的复杂性。它将类层次结构打印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),我会得到典型的非虚拟基础树: 替代文字

添加足够的虚拟性...

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) };

..结果是菱形(看数字来学习施工顺序!!)

替代文字

但是如果你把所有的基地都变成虚拟的:

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) };

你会得到一个具有不同初始化顺序的钻石:

替代文字

玩得开心!

于 2010-08-05T13:05:36.827 回答
8

virtual当从任何 A、B、C 和 E 类(位于菱形顶部)继承时,您必须指定继承。

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;
于 2010-08-05T12:34:24.567 回答
2

我个人的建议是从 B 和 C 开始:虚拟 A,然后继续添加,直到编译器停止抱怨。

实际上,我会说B和C:虚拟A,G和H:虚拟E,以及E:虚拟B和C。所有其他继承链接都可以是正常继承。不过,这种怪物需要 6 年的时间才能打一个虚拟电话。

于 2010-08-05T12:32:42.223 回答
1

如果要确保层次结构中顶级类的对象(I在您的情况下)恰好包含每个父类的一个子对象,则必须找到层次结构中具有多个超类的所有类并使这些类虚拟化他们的超类的基础。就是这样。

在您的情况下,每次您在此层次结构中从它们继承时,类 、A和都必须成为虚拟基类。BCE

D,F和不必成为虚拟基类GH

于 2010-08-05T15:01:50.403 回答
0

如果您希望每种类型的每个实例只有一个“物理”实例(只有一个 A,只有一个 B 等),那么每次使用继承时都必须使用虚拟继承。

如果您想要其中一种类型的单独实例,请使用普通继承。

于 2010-08-05T12:36:34.203 回答
0

编辑:我认为 A 是最派生的类;)

@Luther 的回答真的很酷,但回到原来的问题:

virtual当从继承层次结构中至少有一个其他类继承的任何类继承时,您需要使用继承(在路德图中,这意味着至少有两个箭头指向该类)。

D在这里,在,和之前是不必要的F,因为只有一个类派生自它们(目前没有派生自它们)。GHI

但是,如果您事先不知道其他类是否会从您的基类继承,则可以添加virtual以防万一。例如,建议Exception类实际上继承自std::exceptionStroustrup 本人。

正如 Luther 所指出的,它修改了实例化顺序(并对性能有轻微影响),但我认为任何依赖于构造顺序的设计一开始都是错误的。并且作为一种精度:您仍然可以保证在派生类的任何属性之前初始化基类,因此在执行派生的构造函数体之前。

于 2010-08-05T15:43:11.127 回答
0

要记住的是 C++ 保留了一个继承表。添加的虚拟类越多,编译时间(链接)就越长,运行时也会越重。

一般来说,如果可以避免虚拟类,你可以用一些模板替换或尝试以某种方式解耦。

于 2015-10-29T14:08:46.490 回答