Виртуальное наследование в C++
Я нашел это на веб-сайте, читая о виртуальном наследовании в 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 ответов
в принципе, если виртуальное наследование не используется, базовые члены фактически являются частью экземпляров производного класса. Память для базовых элементов выделяется в каждом экземпляре, и для доступа к ним не требуется никаких дополнительных косвенных действий:
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
членов, даже если он наследует их два раза как istream
и ostream
.
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;
основная проблема заключается в том, что если вы приведете указатель на наиболее производный тип к указателю на одну из его баз, указатель должен ссылаться на адрес в памяти, из которого каждый член типа может быть расположен кодом, который не знает производных типов. С не-виртуальным наследованием это обычно достигается за счет наличия точного макета, а это, в свою очередь, достигается за счет содержания субобъекта базового класса, а затем добавления дополнительных битов производного тип:
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
subobject через указатель. Когда мы можем создать most_derived
макет с одной базой, он может выглядеть так:
--------- <- derived starts here
base::ptr -----\
y |
--------- | <- derived2
base::ptr --\ |
z | |
--------- <--+-/ <- base
x
---------
теперь код в derived
и derived2
nows как получить доступ к базовому подобъекту (просто разыменование base::ptr
объект-членов), и в то же время у вас есть один экземпляр base
. Если код в любом промежуточном класс доступа x
они могут сделать это this->[hidden base pointer]->x
, и это будет разрешено во время выполнения в правильном положении.
важный бит здесь - это код, скомпилированный в derived
/derived2
layer может использоваться с объектом этого типа или любым производным объектом. Если бы мы написали второй most_derived2
объект, где порядок наследования был отменен, то они макет y
и z
можно поменять местами, а смещения от указателя на derived
или derived2
подобъекты для the base
subobject будет отличаться, но код для доступа x
все равно будет то же самое: разыменование вашего собственного скрытого базового указателя, гарантируя, что если метод в derived
является окончательной overrider, и что доступ к base::x
тогда он найдет его независимо от окончательного макета.
в C++ класс выкладывается в память в фиксированном порядке. Базовый класс существует буквально внутри памяти, выделенной производному классу, с фиксированным смещением, аналогично меньшей коробке внутри большей коробки.
если у вас нет виртуального наследования вы говорите iostream
содержит элемент istream
и ostream
каждый из которых содержит ios
. Поэтому iostream
содержит два ios
es.
при виртуальном наследовании виртуальный базовый класс не существует в фиксированное смещение. Это аналогично висению на внешней стороне коробки, связанной с кусочком веревки.
затем iostream
содержит элемент istream
и ostream
каждый из которых связан с ios
строкой. Поэтому iostream
один ios
, связанные двумя отдельными битами строки.
на практике бит строки является целым числом, которое говорит, где фактическое ios
запускается относительно адреса производного класса. Т. е. the istream
имеет скрытый член называется, например,__virtual_base_offset_ios
. Когда istream
методы хотят получить доступ к ios
база, они берут свои this
указатель, добавьте __ios_base_offset
и вот это ios
указатель базового класса.
-
другими словами, в не-виртуально производных классах производные классы знают, что такое смещение к базовому классу, потому что оно фиксировано и физически внутри производного класса. В фактически производных классах базовый класс должен быть общим, поэтому он не всегда может существовать внутри производного класса.
для удаления неоднозначности используется виртуальное наследование.
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();
}