Давайте разберёмся, как происходит вызов обычных функций и методов классов. Вызов обычных функций и методов происходит через механизм, называемый статическим (статичным) связыванием (static binding) или ранним связыванием (early binding).
Раннее связывание использовалось во всех функциях и методах наших программ за исключением тех случаев, где мы использовали указатели на функции.
Когда мы запускаем сборку (building) программы, компилятор просматривает исходный код и превращает все операторы в команды процессора. Допустим, в коде встречается вызов какой-нибудь функции:
someFunction(arg); // some - какой-то
Если это обычная функция (не указатель на функцию), то при вызове используется механизм раннего связывания.
Во время компиляции для кода (определения) функции выделяется память, и назначаются адреса для каждого оператора. Первый адрес в определении (теле функции) является адресом функции. При вызове someFunction, процессор будет переходить на адрес функции и начнёт выполнять тело функции. Самое важное здесь то, что адрес функции назначается во время компиляции, и именно этот адрес используется при вызове функции. Это и есть раннее или статичное связывание. Т.е. имя функции крепко привязано к адресу функции.
На экран будет выведено две строки Базовый класс. На этапе компиляции память выделяется для двух копий Method - для базового класса и для производного. Оба адреса привязываются к именам методов: Base::Method, Derived::Method. Т.е. когда в коде мы вызываем Method, то вызывается метод, соответствующий типу объекта. Чтобы увидеть, что для каждого объекта вызывается свой метод, давайте переопределим метод Derived::Method:
public:
void Method () { cout << "Производный класс\n"; }
//-------- Вывод:
Базовый класс Производный класс
Здесь хорошо видно, что вызываются два разных метода. Теперь следующий пример. Определения классов оставим без изменений. Поработаем с указателями:
Замечание
В данном примере пока что не будем обращать внимание, зачем нужно в указатель на Base помещать объект Derived.
Base* b = new Derived; Derived* d = new Derived;
b->Method(); d->Method();
//-------- Вывод:
Базовый класс Производный класс
Самое важное здесь то, что компилятор спокойно "проглатывает" тот факт, что указатель на Base указывает на производный класс. Дело в том, что базовый и производный классы являются совместимыми по типу.
Во время выполнения программы процессор видит, что b - это указатель на Base. Процессор не обращает внимание, что на самом деле этот указатель указывает на объект Derived. При вызове метода объекта b процессор переходит к адресу Base::Method.
Чтобы объект b вызвал метод Derived::Method, нужно привести тип. Например, так:
static_cast<Derived*>(b)->Method();
//-------- Вывод:
Производный класс
Это примеры раннего связывания (статического). Обратите внимание, что в этом примере мы помещали в указатель на Base объект Derived, а не наоборот:
Derived* d = new Base; b->Method(); d->Method();
Этот случай нам не интересен. К тому же при компиляции возникнет ошибка - здесь нужно использовать static_cast или dynamic_cast. Практическое применение имеет только случай, когда Base* указывает на Derived. В этом случае появляется возможность использовать полиморфизм (polymorphism).