Базовый курс C++ (MIPT, ILab). Lecture 8. Наследование и полиморфизм

preview_player
Показать описание
Лекции в бакалавриате МФТИ по C++ на русском языке.

В этой лекции нас ждёт несколько открытий. Сначала мы случайно откроем наследование. Потом мы случайно изобретём динамический полиморфизм. А в конце даже попытаемся помирить друг с другом несколько типов динамического полиморфизма

Лектор: Константин Владимиров
Дата лекции: 1 ноября 2021 года
Съёмка и звук: Дмитрий Рябцев

Timeline:
00:00 ParaCL
06:00 Unions
13:23 Изобретаем наследование
25:07 Принцип подстановки
36:30 Домашнее задание
42:53 Полиморфизм
1:02:56 Как правильно писать классы?
1:10:14 Четыре правильных способа
1:22:27 Pure virtual calls
1:29:25 Статическое и динамическое связывание
1:36:20 Перегрузка виртуальных функций
1:43:45 Закрытое наследование

Errata:
* Тут пока пусто
Рекомендации по теме
Комментарии
Автор

Не встречал на русском более качественного курса чем у Вас. Большое спасибо за труд :-)

std-sort
Автор

Очень полезная лекция. Прямо всё самое важное по этой теме. Очень понравилось, как вы рассказываете: и слушать интересно, и всё запоминается, потому что специально акцентируете внимание на важных моментах. Очень круто!

Dima-Teplov
Автор

Какие же крутые лекции! Но как же хотелось бы увидеть лекции по си в вашем исполнении!

kotanvich
Автор

1:34:41 Паттерн "Public Морозов"

samolisov
Автор

Спасибо за лекции! Безумно интересно)
Есть опечатка в слайде №54 (1:35:08 ) строки struct Derived : public Base, необходимо : public BaseNVI

evgenyrozhnowsky
Автор

Спасибо за очередную замечательную лекцию!
По поводу замечания в 55:41 писать или не писать virtual. Это от части code-style. Но у привычки писать virtual есть одно важное преимущество:
Когда читаешь код, virtual у overrided функций помогают лучше видеть интерфейс класса.
virtual всегда идёт перед функцией, его проще увидеть.
Разумеется, это замечание не относится к ревью, а только к правильно написанному коду.

ufabiz
Автор

Спасибо!
Вопрос по поводу слайда 24 (48:25).
Насколько мне известно работа с vptr все-таки обычно организована несколько иначе. На слайде приведена упрощенная картина для более простого понимания, или есть какие-то специфические имплементации, устроенные таким образом?
Насколько мне известно в момент компиляции программы уже известны какие виртуальные функции должна содержать таблица виртуальных функций для каждого класса, поэтому перед выполнением программы все таблицы виртуальных функций уже содержатся в статических данных, а все что делают конструкторы-деструкторы, это просто переставляют указатель vptr конкретных экземпляров классов на эти таблицы. И судя по листингу ассемблера эта таблица не обязана заканчиваться nullptr.
Я проверил по нескольким компиляторам и там нигде нет динамической аллокации, а работает именно как я описал.

Если предположить что конструктор должен динамически выделять память под vtable, то в голову приходит как минимум пара проблем:
1) должны ли разные экземпляры одного класса разделять одну таблицу, или у каждого экземпляра должен быть свой экземпляр? Если каждый инстанс хранит свою копию таблицы, то это выглядит супер дорого, если нет, то кто отвечает за удаление таблицы.
2) поскольку производные классы могут накидывать дополнительные поля в таблицу виртуальных функций, то базовый класс на момент создания не может знать размер таблицы виртуальных функций. И либо производные классы должны ее переаллоцировать, либо в базовый конструктор нужно еще прокидывать неявный параметр на реальный размер таблицы для этого класса, ну или еще какое-нибудь костыльное решение. Все это выглядит не очень, на фоне просто метода, описанного выше.

flowf
Автор

Приятные воспоминания. Второй курс, только не компилятор, а интерпретатор, мощного и лаконичного подмножества русского языка. Жаль только на кафедре к этому отнеслись немного странно. 😉

ussr
Автор

1:00:40
В строке ISquare *sq = new Triangle<int>; delete sq;
Откуда operator delete знает сколько байт нужно освободить? Почему он освободит sizeof(Triangle<int>), а не sizeof(ISquare)?

Vakenrovec
Автор

На задании 1:50:30 может, можно проинициализировать IBuffer нашим MyBuffer? 😅Или это вызовет проблемы, которые я не вижу?(

sehzadeselim
Автор

Кстати, вспомнил, как можно вызвать деструктор наследника, если есть ссылка на базовый класс и у того деструктор невиртуальный. Есть же правило, что константные ссылки продляют время жизни временных объектов и такой код const Basic &b = make_derived() вполне валиден если make, _derived() возвращает объект класса Derived по значению. Тогда при окончании времени жизни b будет вызван деструктор временного объекта, Derived, а не Basic, несмотря на тип ссылки. И здесь неважно виртуальный деструктор у Basic или нет.

samolisov
Автор

Константин, похоже на слайде 43 опечатка:

нужно заменить
unique_ptr(T *ptr = nullptr, Deleter del = Deleter()) :
Deleter(del), ptr_(ptr), del_(del) {}

на это
unique_ptr(T *ptr = nullptr, Deleter del = Deleter()) :
Deleter(del), ptr_(ptr) {}

ДмитрийСадков-ец
Автор

Спасибо за лекции!

1:29:25 Насколько я понимаю, early/late binding - это про связывание имени с конкретным типом, в C++ же имена всегда(?) связываются статически.
Терминология довольно расплывчатая и сбивает с толку. Вплоть до того, что статьи Late Binding и Dynamic Dispatch на вики напрямую друг другу противоречат в разделе о C++.
Есть точка зрения, что late binding в C++ не поддерживается и называть dynamic dispatch (которым является вызов виртуальных функций и наверное любой type erasure в языке) late binding'ом некорректно.
При этом постоянно вижу, что late binding и dynamic dispatch используются как взаимозаменяемые понятия, а потому не могу уложить их в голове, ведь они означают совершенно разные вещи.
Можете что-то сказать по этому поводу?

Indev
Автор

А если и в базовом классе не тривиальный деструктор и в наследуемом, то как нужно определить деструктор, ведь если мы переопределим деструктор, то базовая не часть утечет

bkWorm-gxpi
Автор

Здравствуйте.
Не совсем понимаю, как функции "хранятся" (понимаю, что не хранятся они там) внутри класса? То есть, по факту, это функции, которые принимают неявно this, но как это реализовано на практике?
При использовании пространств имён механизм схожий?

TheFX
Автор

Имеет ли смысл внутри дочернего класса вызвать using родительского?

как на 1:43:23 using Matrix; вместо using Matrix::pow;

ДмитрийАндреев-фх
Автор

На слайдах 13 и 14 (27:14) видимо произошла путаница для определений double_square. На 13 принимает Square &s, но не использует; а на 14 нет аргументов, но использует s в теле метода.
P.S. Огромное спасибо за познавательные лекции!

oltimuss
Автор

Если создаем абстрактный класс, компилятор выдает варнинг: виртуальная таблица будет создана в каждой единице трансляции. Добавление какой-либо функции с телом в .cpp решает эту проблему. Есть ли другие способы решения этой проблемы?

qp
Автор

Здравствуйте, не очень понял на 1:50:00. Порядок инициализации объектов же не зависит от их порядка в списке инициализации? То есть Array будет проинициализирован первым не зависимо от того на каком месте он будет в списке инициализации. И он будет проиницилизирован указателем на непроинициализированную область памяти, и соответственно потом это останется висячим указателем. Но такой код скомпилируется. (Рассуждения для себя, если я ошибаюсь, буду рад если кто нибудь поправит)) Автору канала огромное спасибо за такие лекции!

razenkovv
Автор

Добрый вечер. Почему бы на 9:52 просто не использовать mem-initializer-list вместо placement new?
union U {
std::string s_;
std::vector<int> v_;
U(std::string s) : s_(std::move(s)) {}
U(std::vector<int> v) : v_(std::move(v)) {}
~U() {}
};

ddvamp