14

我在阅读 C++ 中的虚拟继承时在一个网站上找到了这个

使用多重继承时,有时需要使用虚拟继承。一个很好的例子是标准 iostream 类层次结构:

//Note: this is a simplified description of iostream classes

class  ostream: virtual public ios { /*..*/ }
class  istream: virtual public ios { /*..*/ }

class iostream : public istream, public ostream { /*..*/ } 
//a single ios inherited

C++ 如何确保只存在一个虚拟成员的单个实例,而不考虑从它派生的类的数量?C++ 使用额外的间接级别来访问虚拟类,通常是通过指针。换句话说,iostream 层次结构中的每个对象都有一个指向 ios 对象共享实例的指针。额外的间接级别有轻微的性能开销,但这是一个很小的代价。

我对以下声明感到困惑:

C++ 使用额外的间接级别来访问虚拟类,通常通过指针

有人可以解释一下吗?

4

4 回答 4

10

要解决的基本问题是,如果您将指向最派生类型的指针强制转换为指向其基数之一的指针,则该指针必须引用内存中的地址,该类型的每个成员都可以通过不知道派生类型。对于非虚拟继承,这通常是通过具有精确的布局来实现的,而这又是通过包含一个基类子对象然后添加派生类型的额外位来实现的:

struct base { int x; };
struct derived : base { int y };

派生的布局:

--------- <- base & derived start here
    x
---------
    y
---------

如果你添加第二个派生类型和一个最派生类型(同样,没有虚拟继承),你会得到类似:

struct derived2 : base { int z; };
struct most_derived : derived, derived 2 {};

使用此布局:

--------- <- derived::base, derived and most_derived start here
    x
---------
    y
--------- <- derived2::base & derived2 start here
    x
---------
    z
---------

如果您有一个most_derived对象并且您绑定了一个类型的指针/引用,derived2它将指向标有 的行derived2::base。现在,如果从 base 继承是虚拟的,那么应该有一个base. 为了讨论,假设我们天真地删除了第二个base

--------- <- derived::base, derived and most_derived start here
    x
---------
    y
--------- <- derived2 start here??
    z
---------

现在的问题是,如果我们获得一个指向derived它的指针,它的布局与原始布局相同,但是如果我们试图获得一个指向布局的指针,derived2那么布局就会不同,并且代码中的代码derived2将无法定位该x成员。我们需要做一些更聪明的事情,这就是指针发挥作用的地方。通过添加一个指向每个虚拟继承对象的指针,我们得到了这个布局:

---------  <- derived starts here
base::ptr  --\
    y        |  pointer to where the base object resides
---------  <-/
    x
---------

同样对于derived2。现在,以额外的间接为代价,我们可以x通过指针定位子对象。当我们可以most_derived使用单个基础创建布局时,它可能如下所示:

---------          <- derived starts here
base::ptr  -----\
    y           |
---------       |  <- derived2
base::ptr  --\  |
    z         | |
---------  <--+-/  <- base
    x
---------

现在编写代码derivedderived2了解如何访问基本子对象(只需取消引用base::ptr成员对象),同时您有一个base. 如果任一中间类中的代码访问x它们,它们都可以通过这样做来访问this->[hidden base pointer]->x,这将在运行时解析到正确的位置。

这里重要的一点是在derived/derived2层编译的代码可以与该类型的对象或任何派生对象一起使用。如果我们编写了第二个most_derived2继承顺序颠倒的对象,那么它们的布局yz可以交换,并且从指向derivedderived2子对象的指针到子对象的偏移量base将不同,但访问代码x仍然相同: 取消引用你自己的隐藏基指针,保证如果一个方法 inderived是最终的覆盖器,base::x那么无论最终布局如何,它都会找到它。

于 2011-03-03T11:24:20.783 回答
9

基本上,如果不使用虚拟继承,则基成员实际上是派生类实例的一部分。基本成员的内存在每个实例中分配,并且不需要进一步的间接访问它们:

class Base {
public:
    int base_member;
};

class Derived: public Base {
public:
    int derived_member;
};


Derived *d = new Derived();
int foo = d->derived_member;  // Only one indirection necessary.
int bar = d->base_member;     // Same here.
delete d;

但是,当虚拟继承发挥作用时,虚拟基成员由其继承树中的所有类共享,而不是在基类被多重继承时创建多个副本。在您的示例中,iostream仅包含ios成员的一个共享副本,即使它从和继承了它们两次istreamostream

class Base {
public:
    // Shared by Derived from Intermediate1 and Intermediate2.
    int base_member;  
};

class Intermediate1 : virtual public Base {
};

class Intermediate2 : virtual public Base {
};

class Derived: public Intermediate1, public Intermediate2 {
public:
    int derived_member;
};

这意味着需要一个额外的间接步骤才能访问虚拟基成员:

Derived *d = new Derived();
int foo = d->derived_member;  // Only one indirection necessary.
int bar = d->base_member;     // Roughly equivalent to
                              // d->shared_Base->base_member.
delete d;
于 2011-03-03T11:12:28.927 回答
3

在 C++ 中,一个类以固定的顺序排列在内存中。基类实际上存在于分配给派生类的内存中,以固定偏移量存在,类似于较大盒子中的较小盒子。

如果你没有虚拟继承,你说的是iostream包含一个istream和一个ostream,其中每个都包含一个ios。因此aniostream包含两个ioses。

使用虚拟继承,虚拟基类不存在固定偏移量。它类似于挂在盒子外面,用一根绳子连接起来。

所以 theniostream包含一个istream和一个,其中的ostream每一个都链接到一个iosby 字符串。因此 aniostream有 one ios,由两个单独的字符串位链接。

在实践中,字符串的位是一个整数,表示ios相对于派生类的地址实际开始的位置。即istream有一个隐藏的成员,例如,__virtual_base_offset_ios。当istream方法要访问ios基类时,它们会获取自己的this指针,添加__ios_base_offset,这就是ios基类指针。

-

换句话说,在非虚拟派生类中,派生类知道基类的偏移量是多少,因为它是固定的,并且在派生类内部。在虚拟派生类中,基类必须是共享的,因此它不能总是存在于派生类中。

于 2011-03-03T11:29:18.973 回答
-2

为了消除歧义,使用了虚拟继承。

class base {
    public:
        int a;
};

class new1 :virtual public base
{
    public:
        int b;
};
class new2 :virtual public base
{
    public:
        int c;
};

class drive : public new1,public new2
{
    public:
        void getvalue()
        {
            cout<<"input a b c "<<endl;
            cin>>a>>b>>c;
        }
        void printf()
        {

            cout<<a<<b<<c;
        }
};

int main()
{
    drive ob;
    ob.getvalue();
    ob.printf();
}
于 2017-10-12T17:27:58.617 回答