Почему переопределение метода без virtual — это не переопределение?

Переопределение метода является одной из основных концепций ООП (объектно-ориентированного программирования). Когда класс наследует другой класс, он может переопределить (или заместить) методы, определенные в базовом классе. Но чтобы переопределение было возможно, класс-наследник должен явно указать, что он хочет заменить метод базового класса. В C++ для этого применяется ключевое слово virtual.

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

Можно дать пример, чтобы лучше понять ситуацию:

class Base {
public:
    void foo() {
        std::cout << "Base::foo()" << std::endl;
    }
};

class Derived : public Base {
public:
    void foo() { // без ключевого слова virtual
        std::cout << "Derived::foo()" << std::endl;
    }
};

Здесь класс Derived наследует класс Base, но метод foo в классе Derived не объявлен как virtual. Поэтому в результате его вызова будет использоваться реализация из класса Derived, а не из класса Base, что называется "скрытием метода" (hiding).

int main() {
    Base* baseObj = new Derived();
    baseObj->foo(); // выводит "Base::foo()"
    
    Derived derivedObj;
    derivedObj.foo(); // выводит "Derived::foo()"
    
    delete baseObj;
    return 0;
}

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

Таким образом, необъявление метода как virtual позволяет создавать новые реализации методов в классе-наследнике, но эти реализации не переопределяют методы базового класса. Вместо этого, они скрывают их, и вопрос о выборе реализации будет зависеть от типа указателя или ссылки, которые указывают на объект