Предыдущая часть:
Рассмотрим для затравки пример из другого языка – PHP:
Несмотря на разницу в синтаксисе, легко понять, что класс Child наследуется от класса Prnt. Я его назвал Prnt, так как Parent в PHP это зарезервированное слово. И родитель, и потомок имеют метод setX().
Далее, мы передаём в функцию test() экземпляры классов Prnt и Child и смотрим, что происходит.
$parent = new Prnt();
test($parent);
$child = new Child();
test($child);
Когда передаём $parent, то вызывается метод Prnt::setX(), когда передаём $child, вызывается Child::setX(), то есть это два разных метода, и какой из них вызывать, определяет класс переданного объекта.
Функция ожидает на входе конкретный класс Prnt, но почему она тогда принимает класс Child? Потому что он потомок Prnt и значит включает в себя Prnt. Хорошо, а почему тогда вызывается не метод Prnt::setX(), а метод Child::setX()? Ведь раз функция ожидает Prnt, то и работать она должна с методами Prnt?
Во-о-от, а теперь посмотрим на C++:
В таком варианте, хотя мы и будем передавать в функцию test() экземпляры Parent и Child, будет всегда вызываться метод Parent::setX(). Что вроде бы и логично, так как мы явно указали, что аргумент функции имеет тип Parent.
Сама функция test() генерируется таким образом, что в ней жёстко прописывается вызов Parent::setX(). А строки "setX child" в программе нет вообще, потому что она никогда не используется.
В чём разница между PHP и C++?
PHP в некоторых аспектах стал типизированным языком, но при этом сохраняет динамические типы, которые определяются не во время компиляции, а во время исполнения. Функция test() не знает, есть ли в переданном объекте метод setX(), пока не поищет его. Это значит, что объект должен приносить с собой свои методы. Соответственно, Prnt приносит свой setX(), а Child свой.
К примеру, если у Child будет свой метод setY(), которого нет у Parent, и мы попытаемся вызвать этот метод в функции test(), то C++ нас немедленно пошлёт. Компилятор знает, что тип аргумента Parent, а у Parent такого метода нет. А PHP просто поищет метод в объекте, и если найдёт, то спокойно его вызовет. А если не найдёт, то программа упадёт.
Мы можем заставить объекты C++ также носить свои методы с собой. Для этого метод setX() в родительском классе надо объявить как виртуальный:
Это приводит к значительным изменениям в сгенерированном коде. У классов Parent и Child вдруг появляются громоздкие конструкторы, а впридачу к ним появляются так называемые
Виртуальные таблицы
Добавление слова virtual к объявлению метода говорит о том, что он теперь будет динамически определяться во время исполнения программы. Как это происходит?
Компилятор заранее строит таблицы методов для каждого класса. У класса Parent появляется своя таблица, где содержится указатель на метод Parent::setX(). У класса Child также появляется своя таблица, где содержится указатель на метод Child::setX().
Теперь при создании класса Parent вызывается конструктор, который берёт указатель на таблицу для Parent и помещает этот указатель в созданный объект. Обратим внимание, что класс Parent был пустой, то есть не содержал никаких свойств. Теперь он неявно содержит свойство, в котором хранится указатель на таблицу методов, в которой в свою очередь находится указатель на метод Parent::setX().
Аналогично создаётся объект класса Child. Его конструктор берёт указатель на таблицу методов для Child и помещает его внутри объекта.
Таким образом, таблица методов назначается именно в момент создания объекта, и затем остаётся с ним всё время. Созданный объект будет носить с собой методы из своей таблицы.
Однако, если повторить эксперимент с передачей в test() объектов разных классов:
Parent parent;
test(parent);
Child child;
test(child);
Мы увидим, что да, объекты конструируются как надо и передаются в функцию, но сама функция по-прежнему использует жёстко прошитый вызов Parent::setX() и поэтому ничего не меняется.
Изменим передачу объектов на указатели:
И на этот раз функция будет вызывать уже не Parent::setX(), а брать из переданного объекта указатель на таблицу, который он принёс с собой, и из этой таблицы брать указатель на метод. Так что при передаче parent будет вызван Parent::setX(), а при передаче child будет вызван Child::setX(). Как в первом примере с PHP.
Почему виртуальные функции работают только с указателями на объекты? Попробуем узнать в руководстве по C++:
Виртуальные функции это методы класса, чьё поведение может быть переопределено классами-потомками. В противоположность невиртуальным функциям, переопределённое поведение сохраняется даже тогда, когда отсутствует информация о реальном типе класса во время компиляции. Таким образом, если класс-потомок используется через указатель на базовый класс, вызов переопределённой функции будет иметь поведение как у класса-потомка. Такой вызов известен как "вызов виртуальной функции" или "виртуальный вызов".
Здесь явно говорится, что виртуальные функции вызываются, когда мы работаем с классом-потомком через указатель класса-родителя.
Видимо, дело в способности компилятора получить информацию о реальном типе класса во время компиляции. Если он её получает, то виртуальный вызов не требуется. И что же тогда происходит здесь?
Parent parent;
test(parent);
Child child;
test(child);
Когда мы в функцию передаём объект не по указателю, а по значению, то этот объект копируется. В функцию передаётся не он, а его копия. А что происходит при копировании? Вызывается имплицитный конструктор копирования Parent, о котором речь шла в предыдущей части.
Компилятор видит, что мы хотим передать структуру типа Child как аргумент типа Pаrent, и поэтому на лету создаёт из структуры Child временную структуру Parent, и передаёт уже её. Таким образом, функция получает то, что и ожидает – Parent. Значит, тип во время компиляции определён и можно использовать фиксированный вызов Parent::setX(), а не виртуальный.
Имплицитное копирование можно запретить, сделав явный конструктор explicit Parent(Parent const&), но тогда сломается и передача в функцию по значению. Раз нет копирования, нет и передачи.
Думаю, что если тщательно руками написать конструктор копирования из Child в Parent, то наверное можно будет и как-то настроить виртуальные методы, чтобы они вызывались именно от Child. Но это не точно.
Чувствую, что имплицитное копирование ещё много где подпортит жизнь.
Читайте дальше: