Почему оператор разрешения области видимости (::) не разрешает механизм виртуальной функции? что может привести к бесконечной рекурсии в противном случае

#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() , было бы бесконечной рекурсией, и было бы практически невозможно вызвать функцию из базового класса.

Посмотрите на воображаемое выполнение кода, если квалифицированный вызов не отключил виртуальную отправку:

  1. У вас есть A* a = new B;
  2. a->print() вызывается, виртуальная отправка определяет, что B::print() должно быть вызвано
  3. Первая инструкция B::print() вызовов A::print() , virtual dispatch определяет, что B::print() должно быть вызвано
  4. Бесконечная рекурсия

Теперь последовательность выполнения при квалифицированном вызове отключает виртуальную отправку:

  1. У вас есть A* a = new B;
  2. a->print() вызывается, виртуальная отправка определяет, что B::print() должно быть вызвано
  3. Первая инструкция B::print() вызывает A::print() именно эту функцию
  4. A::print() делает свое дело и завершает
  5. B::print() продолжает свое выполнение.
  6. Рекурсии не происходит.

Комментарии:

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. Не просто рекурсивная функция, а рекурсивная функция, у которой нет условия выхода. Таким образом, бесконечно.