Я пытаюсь понять следующий фрагмент кода:
#include<iostream>
using namespace std;
class Base {
public:
virtual void f(float) { cout << "Base::f(float)\n"; }
};
class Derived : public Base {
public:
virtual void f(int) { cout << "Derived::f(int)\n"; }
};
int main() {
Derived *d = new Derived();
Base *b = d;
d->f(3.14F);
b->f(3.14F);
}
Это печатает
Derived::f(int)
Base::f(float)
И я не уверен, почему именно.
Первый вызов d->f(3.14F) вызывает функцию f в Derived. Я не уверен на 100%, почему. Я посмотрел на это (http://en.cppreference.com/w/cpp/language/implicit_cast), в котором говорится:
Значение prvalue типа с плавающей запятой может быть преобразовано в значение prvalue любого целочисленного типа. Дробная часть усекается, то есть дробная часть отбрасывается. Если значение не соответствует целевому типу, поведение не определено.
Что для меня говорит, что вы не можете этого сделать, так как float не вписывается в int. Почему разрешено это неявное преобразование?
Во-вторых, даже если я просто принимаю вышеизложенное как нормальное, второй вызов b->f(3.14F) не имеет смысла. b->f(3.14F) вызывает виртуальную функцию f, поэтому она динамически разрешается для вызова f(), связанного с динамическим типом объекта, на который указывает b, который является производным объектом. Поскольку нам разрешено преобразовывать 3.14F в int, поскольку первый вызов функции указывает, что это допустимо, это (насколько я понимаю) должно снова вызывать функцию Derived::f(int). Тем не менее, он вызывает функцию в базовом классе. Так почему это?
edit: вот как я это понял и объяснил себе.
b является указателем на базовый объект, поэтому мы можем использовать b только для доступа к членам базового объекта, даже если b действительно указывает на какой-либо производный объект (это стандартный объектно-ориентированный подход/наследование).
Единственным исключением из этого правила является случай, когда функция-член класса Base объявлена виртуальной. В таком случае производный объект может переопределить эту функцию, предоставив другую реализацию с использованием точно такой же подписи. Если это произойдет, то эта производная реализация будет вызываться во время выполнения, даже если нам случится получить доступ к функции-члену через указатель на базовый объект.
Теперь, в приведенном выше фрагменте кода, у нас не происходит переопределения, потому что сигнатуры B::f и D::f разные (одна — число с плавающей запятой, другая — целое число). Поэтому, когда мы вызываем b->f(3.14F), единственная рассматриваемая функция — это исходная B::f, которая и вызывается.