#c #class #oop #class-hierarchy #scope-resolution-operator
#c #класс #ооп #иерархия классов #оператор разрешения области видимости
Вопрос:
Я читал о виртуальных функциях из книги Бьярне Страуструпа «Язык программирования C » и столкнулся со следующим фрагментом кода:-
class A {
//...
protected:
int someOtherField;
//...
public:
virtual void print() const;
//...
};
class B : public A {
//...
public:
void print() const;
//...
};
void B::print() const {
A::print();
cout<<A::someOtherField;
//...
}
В книге написано, что
«Вызов функции с использованием оператора разрешения области видимости (::), как это сделано в B::print(), гарантирует, что виртуальный механизм не используется. В противном случае B::print() подвергался бы бесконечной рекурсии.»
Я не понимаю, почему это так, поскольку вызов функции базового класса корректно и явно сообщает, что мы вызываем ::print(), а не что-либо еще. Почему это может привести к бесконечной рекурсии?
Редактировать — Я неуместно вставил ключевое слово «virtual», я очень сожалею об этом, но все еще изучаю этот вопрос, что бы произошло, если бы там был следующий код?
- комментарий @HTNW дает правильное представление
class A {
//...
void print() const;
//...
}
class B : public A {
//...
virtual void print() const;
//...
}
Комментарии:
1. Это не приведет к бесконечной рекурсии. В «В противном случае B::print() подвергся бы бесконечной рекурсии». «в противном случае» означает не использовать
::
.2. Если
::
не отключить виртуальную отправку, то записьA::print()
вызвала быB::print()
вызов, потому что, ну, последнее переопределяет первое.3. Здесь есть множество тонких проблем, каждая из которых иллюстрирует, почему C может быть таким опасным — почему часто очень, очень легко непреднамеренно написать ДЕФЕКТНЫЙ код на C . В частности, вы заметите, что B::print() не переопределяет A::print() (как можно было бы ожидать от виртуальной функции); это затеняет ее. Следовательно, необходимо уточнить это, используя оператор области видимости.
4.@HolyBlackCat Nit:
B::print
не переопределяетA::print
, не так ли?A::print
нетvirtual
.B::print
скрываетсяA::print
из-за доминирования. Например.A amp;amp;x = B(); x.print();
вызовыA::print
посредством статической отправки. «Отключение виртуальной отправки» отличается от простого «обхода виртуальной отправки», и здесь продемонстрировано только последнее. IMO, «отключение виртуальной отправки» было бы показано только в том случае, еслиA::print
были такжеvirtual
.
Ответ №1:
Если бы квалифицированный вызов A::print()
не отключал виртуальную отправку, использование, подобное представленному в B::print()
, было бы бесконечной рекурсией, и было бы практически невозможно вызвать функцию из базового класса.
Посмотрите на воображаемое выполнение кода, если квалифицированный вызов не отключил виртуальную отправку:
- У вас есть
A* a = new B;
a->print()
вызывается, виртуальная отправка определяет, чтоB::print()
должно быть вызвано- Первая инструкция
B::print()
вызововA::print()
, virtual dispatch определяет, чтоB::print()
должно быть вызвано - Бесконечная рекурсия
Теперь последовательность выполнения при квалифицированном вызове отключает виртуальную отправку:
- У вас есть
A* a = new B;
a->print()
вызывается, виртуальная отправка определяет, чтоB::print()
должно быть вызвано- Первая инструкция
B::print()
вызываетA::print()
именно эту функцию A::print()
делает свое дело и завершаетB::print()
продолжает свое выполнение.- Рекурсии не происходит.
Комментарии:
1. Первый вызов print() будет использовать механизм динамической отправки, но почему он использует механизм снова, когда четко указано, что нужно вызывать ::print()? Используется ли виртуальный механизм для всех вызовов, инициируемых объектом экземпляра?
2. @AnkitKumar Первая последовательность гипотетична — что произошло бы, если
A::print()
не предотвратить виртуальную отправку. Извините, если я неправильно понял ваш вопрос.3. Вы ответили правильно, спасибо вам за это, но чего я не понимаю, так это того, что каждый вызов print() будет (гипотетически!) в данном случае используйте механизм динамической отправки, даже если он находится внутри B::print()? Могу ли я заключить, что каждый вызов будет заменен компилятором некоторым «vptr»? Означает ли это, что создается нефизическая функция print(), которая просто предоставляет абстрактный интерфейс для всех других функций print()?
4. @Ankit Да, каждый неквалифицированный вызов
print()
, когдаprint()
естьvirtual
, будет использовать динамическую отправку. Подробности о том, как компилятор справляется с этим, зависят от конкретной реализации, но большинство добавляет vptr в качестве скрытого члена к классу object и одну общую vtable для каждого класса сvirtual
функциями где-то в памяти. Тогда вызов наprint()
будет заменен компилятором наvptr[functionId]
и в зависимости от того, чтоvptr
есть, будет вызвана функция из правильной виртуальной таблицы.5. Последнее сомнение, почему вызов A::print() внутри void B::print() const {} использует виртуальный механизм, поскольку он квалифицирован для явного вызова родительского метода? Спасибо
Ответ №2:
В противном случае B::print() подвергался бы бесконечной рекурсии.
Это относится к коду без A::
:
void B::print() const {
print();
cout<<A::someOtherField;
//...
}
Это просто создало бы B::print
рекурсивную функцию, а не то, что предполагал автор.
Комментарии:
1. Не просто рекурсивная функция, а рекурсивная функция, у которой нет условия выхода. Таким образом, бесконечно.