
- •1. Понятие класса. Методы класса. Управление доступом к компонентам.
- •2. Объявление и определение класса. Внешнее определение функций.
- •3. Создание, копирование и удаление объекта.
- •4. Статические компоненты класса. Инициализация статических компонентов класса.
- •5. Наследование. Типы наследования. Виртуальное наследование.
- •6. Виртуальные функции
- •7. Абстрактные классы и чистые виртуальные функции. Интерфейс
- •8. Дружественность. Дружественные классы и функции.
- •9. Вложенные классы. Внутреннее и внешнее определение.
- •10. Шаблоны классов
- •11.Создание экземпляров шаблона. Инстанцирование.
- •12.Шаблоны и наследование.
- •13.Терминология шаблонов.
- •14. Параметры и аргументы шаблона.
- •15. Шаблоны компонентных функций
- •16. Полная специализация шаблонов.
- •17. Частичная специализация шаблонов.
- •18. Перегрузка операций. Основные понятия.
- •19. Перегрузка унарных операций.
- •20. Перегрузка бинарных операций.
- •Int test() {
- •Int test() {
- •Int test() {
- •// Делаем что-то
- •Вопрос 23
- •24. Группировка и композиция исключений. Повторная генерация. Перехват всех исключений.
- •25. Автоматическое управление ресурсами. Методика raii.
- •Void f() { FileOpen("myfile.Txt", "rt"); //здесь выполняем нужную работу с файлом //... }
- •Void f (int a) throw (x2, x3)
- •27. Стандартная библиотека. Организация стандартной библиотеки
- •28. Тип вектора. Вложенные типы. Итераторы. Доступ к элементам
- •29.Тип Вектора. Конструкторы. Операции со стеком. Списочные операции. Размеры и емкость.
- •30. Стандартные контейнеры. Вопросы производительности операций.
- •31. Процесс разработки по. Цели и этапы проектирования.
- •32. Процесс разработки по. Выявление классов. Определение операций.
- •33. Процесс разработки по. Определение взаимосвязей. Определение интерфейсов.
- •Этап 3: выявление зависимостей
- •Этап 4: определение интерфейсов
- •34. Паттерны проектирования. Основные паттерны.
- •35. Тестирование по. Методы тестирования.
6. Виртуальные функции
Для примеров будем использовать классы из предыдущей лекции. Следует напомнить, что у нас был класс Person, от которого был унаследован класс Student. Рассмотрим следующий пример:
Student s; Person &p = s; s.name(); //Student::name() p.name(); //Person::name()
В 3-й строке вызовется метод класса Student, т.к. s является объектом этого класса. Однако, в строке 4 вызовется метод name базового класса Person, хотя по логике следовало бы тоже ожидать вызов name() класса Student — ведь p — это ссылка на объект производного класса.
Возможность вызова методов производного класса через ссылку или указатель на базовый класс осуществляется с помощью механизма виртуальных функций. Чтобы при вызове p.name() вызвался метод класса Student, реализуем классы следующим образом:
struct Person { virtual string name() const; }; struct Student: Person { string name() const; };
Перед методом name класса Person мы указали ключевое слово virtual, которое указывает, что метод является виртуальным. Теперь при вызове p.name() произойдет вызов метода класса Student, несмотря на то, что мы его вызываем через ссылку на базовый класс Person. Аналогичная ситуация и с указателями:
Student s; Person *p = &s ; p->name(); //вызовется Student::name(); Person n; p = &n; p->name(); //вызовется Person::name()
Если с некоторого класса в иерархии наследования метод стал виртуальным, то во всех производных от него классах он будет виртуальным, вне зависимости от того, указано ли ключевое слово virtual в классах наследниках.
Механизм виртуальных функций реализует полиморфизм времени выполнения: какой виртуальный метод вызовется будет известно только во время выполнения программы.
В качестве следующего примера можно рассмотреть класс TextFile, от которого наследуются два класса: GZippedTextFile и BZippedTextFile. Базовый класс имеет два метода: name(), возвращающий имя файла, и read(), считывающий данные из файла. В этом случае виртуальным имеет смысл сделать только метод read, т.к. у каждого типа сжатого файла будет свой способ считывания данных:
struct TextFile { string name() const; virtual string read(size_t count); //... }; struct GZippedTextFile : TextFile { string read(size_t count); //... } struct BZippedTextFile : TextFile { string read(size_t count); //... }
Таблица виртуальных функций (Virtual Function Table)
Для каждого класса, содержащего виртуальные методы, или унаследованного от класса с виртуальными методами, создается таблица виртуальных функций. Эта таблица предназначена для вызова нужных реализаций виртуальных методов во время исполнения программы. При создании экземпляра класса, указатель на VFT этого класса помещается в самое начало созданного объекта.
Как известно, конструирование объекта происходит поэтапно и начинается созданием объекта самого первого класса в иерархии наследования. Во время этого процесса перед вызовом конструктора каждого класса указатель на VFT устанавливается равным указателю на VFT текущего конструируемого класса. Например, у нас есть 3 класса: A, B, C (B наследуется от A, C наследуется от B). При создании экземпляра С, произойдут 3 последовательных вызова конструкторов: сначала A(), затем B(), и в конце C(). Перед вызовом конструктора A() указатель на VFT будет указывать на таблицу класса A, перед вызовом B() он станет указывать на таблицу класса B() и т.д. Аналогичная ситуация при вызове деструкторов, только указатель будет менятся от таблицы самого младшего класса к самому старшему.
Из этого факта следует правило: в конструкторах и деструкторах нельзя вызывать виртуальные методы. Посмотрим, что произойдет, если нарушить это правило:
struct A { A() { f(); } virtual void f() { cout << "A::f()" << endl; } }; struct B : A { B() { f(); } virtual void f() { cout << "B::f()" << endl; } }; struct C : B { C() { f(); } virtual void f() { cout << "C::f()" << endl; } }; //... C c; //создание объекта класса C
В этом примере на экран будет выведено:
A::f()
B::f()
C::f()