- •Методические указания к лабораторным работам
- •Лабораторная работа №1 простые программы с циклами и операторами консольного ввода/вывода
- •Задание
- •Описание примера
- •Методика выполнения
- •Содержание отчета
- •Контрольные вопросы
- •Лабораторная работа №2 работа с текстовыми файлами, структурами данных и меню
- •Задание
- •Структурное программирование и функциональная декомпозиция системы
- •Функции
- •Организация меню в консольном приложении
- •Структуры данных
- •Операции с файлами
- •Методика выполнения
- •Содержание отчета
- •Контрольные вопросы
- •Лабораторная работа №3 разработка и спецификация функций и модулей программы
- •Задание
- •Модульная структура программ
- •Параметры командной строки
- •Методика выполнения
- •Содержание отчета
- •Контрольные вопросы
- •Лабораторная работа №4 разработка и спецификация структур данных, использование указателей и динамических массивов структур
- •Задание
- •Указатели
- •Методика выполнения
- •Содержание отчета
- •Контрольные вопросы
- •Лабораторная работа №5 использование объектно-ориентированного программирования в разработке приложений
- •Задание
- •Конструкторы и деструкторы
- •Конструктор по умолчанию
- •Конструктор копирования
- •Массивы объектов
- •Friend-конструкции
- •Методика выполнения
- •Содержание отчета
- •Контрольные вопросы
- •Лабораторная работа №6 использование наследования, полиморфизма и абстрактных классов
- •Задание
- •Наследование данных и методов
- •Полиморфизм и виртуальные функции
- •Абстрактный класс
- •Методика выполнения
- •Содержание отчета
- •Контрольные вопросы
- •Лабораторная работа №7
- •Сложные структуры из объектов классов
- •Цель работы - изучение организации различных структур данных и разработка методов манипулирования данными.
- •Задание
- •Методика выполнения
- •Содержание отчета
- •Контрольные вопросы
- •Лабораторная работа №8 разработка windows-интерфейса приложения
- •Задание
- •Методика выполнения
- •Содержание отчета
- •Контрольные вопросы
- •Лабораторная работа №9 разработка и использование com-сервера
- •Задание
- •Шаблоны классов
- •Использование библиотеки atl для создания серверов сом
- •Методика выполнения
- •Содержание отчета
- •Контрольные вопросы
- •Литература
Наследование данных и методов
Производный (Derived или Child) класс является основой для реализации механизма наследования свойств базового (Base или Parent) класса, а также средством подстройки класса к нуждам пользователя. Подкласс (или производный класс) наследует все данные и методы базового класса. В дополнение к ним он может приобрести новые данные и методы, не содержащиеся в базовом классе, но необходимые для конкретных целей, преследуемых при создании подкласса. Каждый объект производного класса содержит свои собственные копии данных, унаследованных от базового класса. Любой класс может стать базовым, если создать новый класс, объявив его производным от первого.
class BaseClass
{ // Компоненты базового класса
};
class SubClass: public BaseClass
{ // Компоненты производного класса
};
Имя базового класса указывается непосредственно после имени производного, до открывающей фигурной скобки. В данном случае производный класс SubClass наследует все компоненты базового BaseClass, а также содержит любые, определенные непосредственно в нем самом, компоненты.
В языке существует возможность управлять атрибутами доступа унаследованных данных и методов. Ключевое слово перед именем базового класса определяет тип наследования — в данном случае общедоступный. Тип наследования влияет на доступность компонентов базового класса в производном так, как указано в таблице.
Таблица. Правила наследования доступа
Тип наследования доступа |
Доступность в базовом классе |
Доступность компонентов базового класса в производном |
public |
public |
public |
|
protected |
protected |
|
private |
недоступны |
protected |
public |
protected |
|
protected |
protected |
|
private |
недоступны |
private |
public |
private |
|
protected |
private |
|
private |
недоступны |
По умолчанию установлен тип наследования доступа private.
В производном классе можно изменять доступность отдельных компонентов базового класса.
class BaseClass
{
public: // Общедоступные компоненты базового класса
int nPublBase;
void funcBase ();
protected: // Защищенные компоненты базового класса
int nProtBase;
private: // Частные компоненты базового, класса
int nPrivBase;
};
class Subclass: BaseClass // private-наследование по умолчанию
{
public: // Общедоступные компоненты производного класса
BaseClass::nPublBase; // Конкретно объявляем как public
int nPublSub;
void funcSub();
} clSub;
Из любого места программы становится возможен доступ к этому компоненту базового класса:
clSub.nPublBase = 7;
Полиморфизм и виртуальные функции
Метод родительского класса может быть переопределен в дочерних классах. В результате одно и то же по смыслу действие может иметь множество конкретных воплощений, форм реализации, что и означает полиморфизм. Полиморфизм позволяет пользователю иерархической структуры классов посылать сообщения общего характера объектам разных классов, не заботясь о деталях интерпретации конкретного действия внутри класса. Полиморфизм сильно облегчает задачу пользования библиотекой классов. Пользователь должен понимать лишь общий смысл или эффект воздействия метода на любой объект в иерархии классов, а не особенности его реализации для конкретного класса.
Главным моментом в понимании сути полиморфизма является момент времени, в который система определяет детали реализации метода для объекта. Если система решает, как выполнить действие во время компиляции, то этот процесс называется ранним связыванием, если решение принимается динамически во время исполнения программы, то это называется поздним связыванием. Под связыванием понимают связывание сути послания (имени метода) с конкретными кодами его реализации для данного объекта.
Раннее связывание реализуется путем переопределения в дочернем классе обычных методов родительского класса. Если метод или операция переопределены в двух или более классах, то компилятор может связать послание объекту с конкретными кодами заранее, так как он знает класс объекта, которому послано данное сообщение. Раннее связывание дает преимущество в скорости, так как компилятор имеет возможность оптимизировать код до его выполнения.
class Parent
{
public:
void print(){…}
} *pp,pobj;
class Derived: Parent
{
public:
void print(){…}
} *pd,dobj;
pobj.print (); // Будут вызваны разные print()
dobj.print(); // Полиморфизм раннего связывания
Компилятор заранее решает, каким классам должны принадлежать функции print в последних двух операторах. Очевидно, что для объекта pobj класса Parent будет вызван метод своего же класса, то есть Parent::print(). Аналогично для производного класса справедлив вызов Derived::print().
В языке C++ указатель на базовый класс может содержать адрес объекта производного класса.
рp=&pobj;
pp->print(); // Вызов метода Parent::print()
рd=&dobj;
pp=pd; // Будут вызваны разные print()
pp->print(); // Вызов метода Parent::print()
pd->print(); // Вызов метода Derived::print()
Указатели рр и pd содержат адрес объекта dobj производного класса. Оператор pd->print(); вызовет метод Derived::print(), так как pd является указателем на класс Derived и содержит в данный момент адрес объекта dobj того же класса. В случае оператора pp->print(); полиморфизм раннего связывания диктует следующее правило. Так как рр объявлен указателем на класс Parent, то оператор pp->print(); вызовет Parent::print(). Тот факт, что рр в данный момент времени содержит адрес объекта производного класса, не влияет на решение, принятое на этапе компиляции.
Позднее связывание реализуется с помощью виртуальных функций. Оно дает преимущество в гибкости и высоте абстракции задачи. Виртуальная функция определяется в базовом классе с помощью спецификатора virtual перед ее объявлением. Изменим объявление метода print в предыдущем примере.
class Parent
{
public:
virtual void print(){…}
} *pp,pobj;
class Derived: Parent
{
public:
void print(){…}
} *pd,dobj;
pobj.print (); // Вызов метода Parent::print()
dobj.print(); // Вызов метода Derived::print()
рp=&pobj;
pp->print(); // Вызов метода Parent::print()
рd=&dobj;
pp=pd;
pp->print(); // Вызов метода Derived::print()
pd->print(); // Вызов метода Derived::print()
В этих условиях изменится функционирование только одного оператора pp->print. В игру вступает полиморфизм позднего связывания, и выбор метода print производится на этапе исполнения программы в зависимости от того, на объект какого класса ссылается в данный момент времени указатель Parent *pp. Если он ссылается на объект производного класса, то будет вызван Derived::print(), если же указатель ссылается на объект базового класса, то будет вызван Parent::print(). Такая гибкость функционирования виртуальных функций является мощным средством, позволяющим выбрать нужный метод при получении указателем (на текущий объект одного из производных классов иерархии) сообщения общего характера вида print(), input() и т. д.
Цена этой гибкости — необходимость для каждого объекта производных классов хранить таблицу ссылок на конкретные воплощения виртуальных функций, чтобы на этапе выполнения программы выбрать нужную. Если эта таблица пуста, то есть виртуальная функция не переопределялась в данном производном классе, то происходит вызов одной из функций в таблице виртуальных функций класса, стоящего на одну ступень выше, и т. д. Если метод класса не является виртуальным, то таблица не создается. Только один метод из одноименных методов иерархии может быть применен к объекту данного класса (свой или ближайший унаследованный из классов-предков).
Все методы классов могут быть виртуальными, за исключением конструктора. Деструктор класса, однако, может быть объявлен виртуальным. Это позволяет вызвать требуемый деструктор с помощью указателя на базовый класс. Если указатель в данный момент ссылается на объект одного из производных классов, то будет вызван деструктор соответствующего класса. Деструктор класса, производного от класса с virtual-деструктором, сам является virtual-деструктором (так же, как и любая другая виртуальная функция). Например:
class Box
{
public: // Виртуальный деструктор
virtual ~Box() { puts(“Box”); }
}
class Menu : public Box
{
public: // ~Menu() тоже виртуальный
~Menu() { puts(“Menu”); }
}
class PullDownMenu : public Menu
{
publiс: // Этот деструктор тоже виртуальный
~PullDownMenu() { puts(“Pull”); }
}
Теперь объявим и заполним массив указателей на базовый класс:
Box *window[3];
window[0] = new Box;
window[l] = new Menu;
window[2] = new PullDownMenu;
Вызовы деструкторов порождают следующие цепочки действий:
delete window[0]; // Будет вызван ~Вох()
delete window[l]; // Будет вызван ~Меnu(), потом ~Вох()
delete window[2]; // Будет вызван ~PullDownMenu(), потом ~Меnu(), и, наконец, ~Вох()
Если убрать спецификатор virtual при объявлении деструктора ~Вох(), то во всех трех случаях будет вызван деструктор базового класса, который, возможно, некорректно сработает в последних двух случаях при уничтожении объектов классов Menu и PullDownMenu.
При определении в производных классах виртуальных функций количество и типы параметров у всех функций в разных классах иерархии должны быть одинаковы. Только в этом случае они считаются виртуальными.
Необходимо различать три варианта переопределения функций, для которых в английском языке используются различные специальные термины. Говорят, что переопределенная в производном классе виртуальная функция (overrides) преобладает (над) или заменяет одноименную функцию базового класса. Если в производном классе переопределена обычная функция, то говорят, что она скрывает (hides) функцию из базового класса с таким же именем. Если в классе определены две функции с одинаковым именем, но разным набором параметров, то говорят, что они совмещены или что вторая является перегруженной (overloaded) версией первой функции.
В случае скрытия (hiding) количество и типы аргументов не влияют на сам факт скрытия. Совмещение (overloading) функций с одним именем, но различием в аргументах возможно только в рамках одного класса и одинаковом типе возвращаемого значения. Если объявленная виртуальной в базовом классе функция имеет в производном классе другой тип или набор параметров, то она считается спрятанной (hiden) и виртуальный механизм игнорируется. Если в производном классе она имеет другой тип возвращаемого значения, то это рассматривается как ошибка.