Указатель базового класса C ++ вызывает дочернюю виртуальную функцию, почему указатель базового класса может видеть член дочернего класса - PullRequest
0 голосов
/ 26 мая 2018

Я думаю, что могу запутаться.Я знаю, что класс с виртуальными функциями в C ++ имеет vtable (по одному vtable на тип класса), поэтому в vtable класса Base будет один элемент &Base::print(), в то время как в vtable класса Child будет один элемент &Child::print(),

Когда я объявляю мои два объекта класса, vtable_ptr base и child, base будет указывать на vtable Base класса, тогда как vtable_ptr child будет указывать на ChildКласс Vtable.После того, как я назначил адрес base и child массиву указателей типа Base.Я звоню base_array[0]->print() и base_array[1]->print().Мой вопрос заключается в том, что base_array[0] и base_array[1] имеют тип Base* во время выполнения, хотя поиск в v-таблице даст правильный указатель на функцию, как тип Base* может видеть элемент в Child учебный класс?(в основном значение 2?).Когда я звоню base_array[1]->print(), base_array[1] имеет тип Base*, но во время выполнения обнаруживает, что будет использовать Child class print().Однако я запутался, почему в это время можно получить доступ к value2, потому что я играю с типом Base* ..... Я думаю, что-то где-то должен пропустить.

#include "iostream"
#include <string>
using namespace std;

class Base {
public:
    int value;
    string name;
    Base(int _value, string _name) : value(_value),name(_name) {
    }

    virtual void print() {
        cout << "name is " << name << " value is " << value << endl;
    }
};

class Child : public Base{
public:
    int value2;
    Child(int _value, string _name, int _value2): Base(_value,_name), value2(_value2) {
    }

    virtual void print() {
        cout << "name is " << name << " value is " << value << " value2 is " << value2 << endl;
    }
};

int main()
{
    Base base = Base(10,"base");
    Child child = Child(11,"child",22);

    Base* base_array[2];
    base_array[0] = &base;
    base_array[1] = &child;

    base_array[0]->print();
    base_array[1]->print();

    return 0;
}

Ответы [ 3 ]

0 голосов
/ 05 июня 2018

Я думаю, что я должен что-то упустить где-то

Да, и большинство вещей вы получили до самого конца.

Вот напоминание о действительно базовойвещи в C / C ++ (C и C ++: одно и то же концептуальное наследие, поэтому многие общие понятия являются общими, даже если в какой-то момент мелкие детали значительно расходятся).(Это может быть действительно очевидно и просто, но стоит сказать это громко, чтобы почувствовать это.)

Выражения являются частью скомпилированной программы, они существуют во время компиляции;объекты существуют во время выполнения.Объект (вещь) обозначается выражением (словом);они концептуально различны.

В традиционном C / C ++ lvalue (сокращение от left-value ) - это выражение, оценка выполнения которого обозначает объект;разыменование указателя дает значение l (например, *this).Это называется «левое значение», потому что оператор присваивания слева требует объекта для назначения.(Но не все lvalue могут быть слева от оператора присваивания: выражения, обозначающие объекты const, являются lvalues, и обычно их нельзя назначить.) Lvalue всегда имеют четко определенную идентичность, и у большинства из них есть адрес (только члены struct, объявленные какбитовое поле не может получить свой адрес, но базовый объект хранения все еще имеет адрес).

(В современном C ++ концепция lvalue была переименована в glvalue, и была изобретена новая концепция lvalue (вместо созданияновый термин для новой концепции и сохранение старого термина концепции объекта с идентичностью, которая может или не может быть изменяемой. Это было по моему не столь скромному мнению серьезная ошибка.)

Поведениеполиморфный объект (объект типа класса, по крайней мере, с одной виртуальной функцией) зависит от его динамического типа , от его типа начатой ​​конструкции (имени конструктора объекта, который начал конструировать элементы данных,или вошел в тело конструктора). Во время выполненияв теле конструктора Child динамический тип объекта, спроектированный с помощью *this, равен Child (во время выполнения тела конструктора базового класса динамический тип - это тип выполняемого конструктора базового класса).

Динамическая полиморфная означает, что вы можете использовать полиморфный объект с lvalue, чей объявленный тип (тип, выведенный во время компиляции из правил языка) не совсем тот же тип, носвязанный тип (связанный с наследованием) .В этом весь смысл ключевого слова virtual в C ++, без которого оно было бы совершенно бесполезным!

Если base_array[i] содержит адрес объекта (поэтому его значение четко определено, а не null), вы можете разыменоватьЭто.Это дает вам lvalue, объявленный тип которого всегда равен Base * по определению: это объявленный тип, объявление base_array:

Base (*(base_array[2])); // extra, redundant parentheses 

, которое, конечно, можно записать

Base* base_array[2];

если вы хотите написать это таким образом, но дерево разбора, способ декомпозиции объявления компилятором НЕ

{ Base* } { base_array[2] }

(использование фигурной скобки жирным шрифтом для символического представления синтаксического анализа)

, но вместо этого

Base {* {{ base_array } [2] }}

Я надеюсь, вы понимаете, что фигурные скобки здесь - мой выборМета-язык, а НЕ фигурные скобки, используемые в грамматике языка для определения классов и функций (я не знаю, как здесь рисовать рамки вокруг текста).

Как новичок, важно, чтобы вы "программировали" свою интуициюправильно, всегда читать объявления, как это делает компилятор;если вы когда-либо объявляете два идентификатора в одном и том же объявлении, разница важна int * a, b; означает int (*a), b; И НЕ int (*a), (*b);

(Примечание: даже если это может быть вам понятно, ОП, так как этоЯсно, что этот вопрос представляет интерес для начинающих в C ++, так как напоминание о синтаксисе объявления C / C ++ может пригодиться кому-то еще.)

Итак, возвращаясь к проблеме полиморфизма: объект производного типа (имя самого последнего введенного конструктора) может быть обозначен lvalue объявленного типа базового класса.Поведение вызовов виртуальных функций определяется динамическим типом (также называемым реальным типом) объекта, обозначенного выражением, в отличие от поведения вызовов не виртуальных функций;это семантика, определяемая стандартом C ++.

Способ, которым компилятор получает семантику, определенную стандартом языка, является его собственной проблемой и не описывается в стандарте языка, но когда существует только один эффективный способ сделать этовсе компиляторы делают это по существу одинаково (мелкие детали зависят от компилятора) с

  • одной таблицей виртуальных функций (" vtable ") на полиморфный класс
  • один указатель на vtable (" vptr ") для каждого полиморфного объекта

(Очевидно, и vtable, и vptr являются концепциями реализации, а не понятиями языка, но они настолько распространены, чтокаждый программист C ++ знает их.)

vtable - это описание полиморфных аспектов класса: операции времени выполнения над выражением заданного объявленного типа, поведение которого зависит от динамического типа.Существует одна запись для каждой операции во время выполнения.Vtable похож на структуру (запись) с одним элементом (записью) на операцию (все записи обычно являются указателями одного размера, поэтому многие люди описывают vtable как массив указателей, но я не описываю его какstruct).

vptr - это скрытый элемент данных (элемент данных без имени, недоступный для кода C ++), чья позиция в объекте фиксирована, как и любой другой элемент данных, который может быть прочитанкод времени выполнения, когда вычисляется lvalue типа полиморфного класса (назовите его D для "объявленного типа").Разыменование vptr в D дает вам виртуальную таблицу, описывающую D lvalue , с записями для каждого аспекта времени выполнения lvalue типа D .По определению местоположение vptr и интерпретация vtable (размещение и использование его записей) полностью определяются объявленным типом D .(Очевидно, что никакая информация, необходимая для использования и интерпретации vptr, не может быть функцией типа времени выполнения объекта: vptr используется, когда этот тип неизвестен.)

Семантика vptr - этонабор гарантированных действительных операций времени выполнения на vptr: как можно разыменовать vptr (vptr существующего объекта всегда указывает на действительный vtable).Это набор свойств формы: добавив смещение off к значению vptr, вы получите значение, которое можно использовать «таким образом». Эти гарантии формируют контракт времени выполнения.

Наиболее очевидный аспект времени выполнения полиморфного объекта - это вызов виртуальной функции, поэтому в vtable есть запись для D lvalueдля каждой виртуальной функции, которая может быть вызвана для lvalue типа D , это запись для каждой виртуальной функции, объявленной либо в этом классе, либо в базовом классе (не считая переопределителей, поскольку они одинаковы).Все нестатические функции-члены имеют «скрытый» или «неявный» аргумент, параметр this;при компиляции он становится обычным указателем.

Любой класс X , производный от D , будет иметь vtable для значений D l.Для эффективности в обычном случае простого (не виртуального) одиночного наследования семантика vptr базового класса (который мы затем называем первичным базовым классом) будет дополнена новыми свойствами, поэтому vtable для X будет расширен: макет и семантика виртуальной таблицы для D будут расширены: любое свойство виртуальной таблицы для D также является свойствомдля vtable для X семантика будет «унаследована»: существует «наследование» vtables параллельно с наследованием внутри классов.

В логическом смысле увеличивается число гарантий: гарантии vptr объекта производного класса сильнее, чем гарантии vptr объекта базового класса.Поскольку это более сильный контракт, весь код, сгенерированный для базового lvalue, все еще действителен.

[В более сложном наследовании это либо виртуальное наследование, либо не виртуальное вторичное наследование (в множественном наследовании - наследование от вторичной базы,это любая база, которая не определена как «первичная база»), расширение семантики vtable базового класса не так просто.]

[Один из способов объяснить реализацию классов C ++ - этоперевод на C (действительно, первый компилятор C ++ собирал на C, а не на сборку).Перевод функции-члена C ++ - это просто функция C, в которой явный неявный параметр this является нормальным параметром указателя.]

Запись vtable для виртуальной функции для D lvalueэто просто указатель на функцию с параметром в качестве явного теперь this параметра: этот параметр является указателем на D , он фактически указывает на базовый подобъект D объектакласс, производный от D , или объект фактического динамического типа D .

Если D является первичной базой X , это тот, который начинается с того же адреса, что и производный класс, и где vtable начинается с того же адреса, поэтому значение vptr одинаково, и vptr используется совместно для первичной базы и производного класса.Это означает, что виртуальные вызовы (вызовы lvalue, которые проходят через vtable) к виртуальным функциям в X , которые заменяют одинаково (которые переопределяют с тем же типом возврата), просто следуют тому же протоколу.Виртуальные переопределители могут иметь другой ковариантный тип возврата, и в этом случае может использоваться другое соглашение о вызовах.)

Существуют другие специальные записи vtable:

  • Несколько записей виртуальных вызовов дляданная сигнатура виртуальной функции, если переопределитель имеет ковариантный тип возвращаемого значения, который требует и корректирует (не является первичной базой).
  • Для специальных виртуальных функций: когда delete operator используется на полиморфной основе с виртуальнойдеструктор, это делается через удаление виртуального деструктора, чтобы вызвать правильный operator delete (заменил delete, если он есть).
  • Существует также не удаляющий виртуальный деструктор, который используется для явных вызовов деструкторов: l.~D();
  • В vtables хранятся смещения для каждого виртуального базового подобъекта для неявного преобразования в указатель виртуальной базыили для доступа к его элементам данных.
  • Существует смещение наиболее производного объекта для dynamic_cast<void*>.
  • Запись для оператора typeid, примененная к полиморфному объекту (в частности,name() класса).
  • Достаточно информации для операторов dynamic_cast<X*>, примененных к указателю на полиморфный объект для навигации по иерархии классов во время выполнения, для поиска заданного базового класса или производного подобъекта (если только X это не просто базовый класс приведенного типа, как без динамической навигации по иерархии).

Это просто обзор информации, представленной в vtable, и видов vtable.другие тонкости.(Виртуальные базы заметно сложнее, чем не виртуальные базы на уровне реализации.)

0 голосов
/ 13 июня 2018

Я думаю, вы, возможно, путаете способ, которым указатель объявлен , с типом объекта, на который он указывает.

Забудьте на мгновение о vtables.Они деталь реализации.Они просто средство для достижения цели.Давайте посмотрим, что ваш код на самом деле делает .

Итак, со ссылкой на код, который вы разместили, эта строка:

base_array[0]->print();

вызывает Base sреализация print(), поскольку объект, на который указывает , имеет тип Base.

В то время как эта строка:

base_array[1]->print();

вызывает реализацию Child sиз print(), потому что (да, как вы уже догадались) объект, на который указывает объект, имеет тип Child.Вам не нужны какие-то необычные приведения, чтобы это произошло.В любом случае это произойдет, при условии, что метод объявлен virtual.

Теперь внутри тела Base::print() компилятор не знает (или не заботится), указывает ли this объекттип Base или объект типа Child (или любой другой класс, производный от Base, в общем случае).Отсюда следует, что он может получить доступ только к элементам данных , объявленным по Base (или любым родительским классам Base, если они были).Как только вы это поймете, все достаточно просто.

Но внутри тела Child::print() компилятор знает немного больше о том, на что указывает this - он долженбыть экземпляром класса Child (или другого класса, производного от Child).Так что теперь компилятор может безопасно получить доступ к value2 - внутри тела Child::print() - и ваш пример поэтому компилируется правильно.

Я думаю, что это действительно так.Vtable существует только для отправки правильному виртуальному методу, когда вы вызываете этот метод через указатель, тип которого неизвестен во время компиляции, как это делает ваш пример кода.(*)


(*) Ну, почти.Оптимизация компиляторов в наши дни становится довольно забавной, там действительно достаточно информации, чтобы вы вызывали соответствующий метод напрямую, но, пожалуйста, не позволяйте этому каким-либо образом запутать проблему.

0 голосов
/ 26 мая 2018

Вызов print через указатель выполняет поиск в vtable, чтобы определить, какую именно функцию вызывать.

Функция знает фактический тип аргумента 'this'.

Компилятор также вставит код для корректировки фактического типа аргумента (скажем, у вас есть дочерний класс:

public base1, public base2 { void print(); };

, где print - это виртуальный член, унаследованный от base2. В этом случае соответствующая виртуальная таблица не будет иметь смещение 0 в дочернем элементе, поэтому для преобразования изсохраненное значение указателя на правильное местоположение объекта).

Данные, необходимые для этого исправления, обычно хранятся как часть скрытых блоков информации типа времени выполнения (RTTI).

...