
- •Немного истории программирования.
- •Введение в ООП.
- •Объект.
- •Инкапсуляция.
- •Наследование (Inheritance)
- •Полиморфизм.
- •Класс
- •Дружественные (friend) функции
- •Перегружаемые (overload) функции и операторы.
- •Виртуальные (virtual) функции, раннее и позднее связывание
- •Абстрактные классы
- •Конструкторы и деструкторы.
ООП |
Лекция 1 |
9 |
нована реализация одной из особенностей полиморфизма. Заметим, что транслятор не различает функции по типу возвращаемого значения.
Следует отметить, что в языках ООП существуют определенные ограничения на применение замещенных функций и операторов. Эти ограничения зависят от конкретного языка ООП.
Виртуальные (virtual) функции, раннее и позднее связывание
Прежде чем коснуться самого применения виртуальных функций необходимо рассмотреть такие понятия как раннее и позднее связывание. Сравним два подхода к покупке, к примеру, килограмма апельсинов. В первом случае мы заранее знаем, что нам надо купить 1 кг. апельсинов. Поэтому мы берем небольшой пакет, не много, но достаточно денег, чтобы хватило на этот килограмм. Во втором случае, мы, выходя из дома, не знаем что и как много нам надо купить. Поэтому мы берем машину (а вдруг будет много всего и тяжелое), запасаемся пакетами больших и малых размеров и берем как можно больше денег. Едем на рынок и выясняется, что надо купить только 1 кг. апельсинов.
Приведенный пример в определенной мере отражает смысл применения раннего и позднего связывания, соответственно. Очевидно, что для данного примера первый вариант оптимален. Во втором случае мы слишком много всего предусмотрели, но нам это не понадобилось. С другой стороны, если по дороге на рынок мы решим, что апельсины нам не нужны и решим купить 10 кг. яблок, то
впервом случае мы уже не сможем этого сделать. Во втором же случае – легко.
Атеперь рассмотрим этот пример с точки зрения программирования. При применении раннего связывания, мы как бы говорим компилятору: "Я точно знаю, чего я хочу. Поэтому жестко(статически) связывай все вызовы функций". При применении механизма позднего связывания мы как бы говорим компилятору: "Я пока не знаю чего я хочу. Когда придет время, я сообщу что и как я хочу".
Таким образом, во время раннего связывания вызывающий и вызываемый методы связываются при первом удобном случае, обычно при компиляции.
При позднем связывании вызываемого метода и вызывающего метода они не могут быть связаны во время компиляции. Поэтому реализован специальный механизм, который определяет как будет происходить связывание вызываемого и вызывающего методов, когда вызов будет сделан фактически.
Очевидно, что скорость и эффективность при раннем связывании выше, чем при использовании позднего связывания. В то же время, позднее связывание обеспечивает некоторую универсальность связывания.
Наконец-то мы добрались и до самих виртуальных функций и методов. К сожалению, для иллюстрации виртуальных методов достаточно сложно провести какую-либо аналогию с физическим миром. Поэтому сразу рассмотрим этот вопрос с точки зрения программирования.
Итак, для чего же применяются виртуальные методы. Виртуальные методы существуют для того, чтобы "наследник" вел себя отлично от "предка", сохраняя при этом свойство совместимости с ним.
Приведем определение виртуальных методов:
Виртуальный метод – это метод, который, будучи описан в потомках, замещает собой соответствующий метод везде, даже в методах, описанных для предка, если он вызывается для потомка.
Адрес виртуального метода известен только в момент выполнения программы. Когда происходит вызов виртуального метода, его адрес берется из таблицы виртуальных методов своего класса. Таким образом вызывается то, что нужно.
Преимущество применения виртуальных методов заключается в том, что при этом используется именно механизм позднего связывания, который допускает обработку объектов, тип которых неизвестен во время компиляции.
Для иллюстрации применения виртуальных методов приведу пример на языке С++, который я позаимствовал из одного C++ Tutorial. Даже если вы не очень разбираетесь в этом языке, надеюсь, что мои пояснения хоть как-то объяснят его смысл.
#include <iostream.h> // подключение стандартной библиотек С++, в // которой описаны некоторый функции, применяемые в программе
class vehicle // класс "транспортное средство"
{
int wheels; float weight;
public: // начало публичного(открытого) раздела класса virtual void message(void) {cout << "Транспортное средство\n";}
ООП |
Лекция 1 |
10 |
//описание виртуальной функции message класса vehicle и реализация этой
//функции. При вызове функции message класса vehicle на экран монитора
//будет выведена строка "Транспортное средство"
};
class car : public vehicle // класс "легковая машина", унаследованный из // класса "транспортное средство"
{
int passenger_load;
public: // начало публичного(открытого) раздела класса void message(void) {cout << "Легковая машина\n";}
//описание виртуальной функции message класса car и реализация этой
//функции. При вызове функции message класса car на экран монитора
//будет выведена строка " Легковая машина "
};
class truck : public vehicle // класс "грузовая машина", унаследованный из // класса "транспортное средство"
{
int passenger_load; float payload;
public: // начало публичного(открытого) раздела класса int passengers(void) {return passenger_load;}
};
class boat : public vehicle // класс "лодка", унаследованный из
// класса "транспортное средство"
{
int passenger_load;
public: // начало публичного(открытого) раздела класса int passengers(void) {return passenger_load;}
void message(void) {cout << "Лодка\n";}
//описание виртуальной функции message класса boat и реализация этой
//функции. При вызове функции message класса boat на экран монитора
//будет выведена строка "Лодка"
};
void main() // основной исполняемый блок программы
{
vehicle *unicycle; // описываем переменной unicycle как указатель на // объект класса vehicle
unicycle = new vehicle; // Создаем объект класса vehicle, // указатель unicycle указывает на этот объект
unicycle->message(); // вызываем метод message объекта delete unicycle; // удаляем объект unicycle
//Все последующие блоки по 3 строки абсолютно идентичны первому
//блоку с той лишь разницей, что изменяется класс создаваемого объекта
//на car, truck, boat
unicycle = new car; unicycle->message(); delete unicycle;
ООП |
Лекция 1 |
11 |
unicycle = new truck; unicycle->message(); delete unicycle;
unicycle = new boat; unicycle->message(); delete unicycle;
}
Результаты работы программы(вывод на экран): Транспортное средство Легковая машина Транспортное средство Лодка
Рассмотрим приведенный пример. У нас есть три класса car, truck и boat, которые являются производными от базового класса vehicle. В базовом классе vehicle описана виртуальная функция message. В двух из трех классов(car, boat) также описаны свои функции message, а в классе truck нет описания своей функции message. Все строки, к которым я не приводил комментарии, не имеют принципиального для данного примера значения. Теперь пробежимся по основному блоку программы - функции main(). Описываем переменную unicycle, как указатель на объект типа vehicle. Не буду вдаваться в подробности, почему именно указатель на объект. Так надо. В данном случае воспринимайте работу с указателем, как с самим объектом. Подробности работы с указателями можно найти в описаниях конкретного языка ООП. Затем, создаем объект класса vehicle, переменная unicycle указывает на этот объект. После этого вызываем метод message объекта unicycle, а в следующей строке удаляем этот объект. В следующих трех блоках по 3 строки проводим аналогичные операции, с той лишь разницей, что работаем с объектами классов car, truck, boat. Применение указателя позволяет нам использовать один и этот же указатель для всех производных классов. Нас интересует вызов функции message для каждого из объектов. Если бы мы не указали, что функция message класса vehicle является виртуальной(virtual), то компилятор статически(жестко) связал бы любой вызов метода объекта указателя unicycle с методом message класса vehicle, т.к. при описании мы сказали, что переменная unicycle указывает на объект класса vehicle. Т.е. произвели бы раннее связывание. Результатом работы такой программы был бы вывод четырех строк "Транспортное средство". Но за счет применения виртуальной функции в классе мы получили несколько другие результаты.
При работе с объектами классов car и boat вызываются их собственные методы message, что и подтверждается выводом на экран соответствующих сообщений. У класса truck нет своего метода message, по этой причине производится вызов соответствующего метода базового класса vehicle.
Очень часто класс, содержащей виртуальный метод называют полиморфным классом. Самое главное отличие заключается в том, что полиморфные классы допускают обработку объектов, тип которых неизвестен во время компиляции. Функции, описанные в базовом классе как виртуальные, могут быть модифицированы в производных классах, причем связывание произойдет не на этапе компиляции (то, что называется ранним связыванием), а в момент обращения к данному методу (позднее связывание).
Виртуальные методы описываются с помощью ключевого слова virtual в базовом классе. Это означает, что в производном классе этот метод может быть замещен методом, более подходящим для этого производного класса. Объявленный виртуальным в базовом классе, метод останется виртуальным для всех производных классов. Если в производном классе виртуальный метод не будет переопределен, то при вызове будет найден метод с таким именем вверх по иерархии классов (т.е. в базовом классе).
Последнее, о чем необходимо рассказать, говоря о виртуальных функциях, это понятие абстрактных классов. Но мы это рассмотрим на следующем шаге.
Абстрактные классы
Очень часто в базовом классе определяется виртуальная функция, которая не выполняет ка- ких-либо значимых действий. Как я уже упоминал при объяснении понятия объекта, чем ниже находится класс в лестнице иерархии, тем он более конкретен. Т.е. базовый класс обычно не определяет законченный тип. Очень часто получается, что базовый класс не имеет какой-либо практической ценности для его непосредственного применения в программе. Однако он имеет ценность для формирования производных классов. Рассмотрим наш пример. Мы использовали в программе создание объекта класса vehicle только для придания примеру большей наглядности. В реальной же программе(также как и мире) нам вряд ли пригодится объект класса vehicle(транспортное средство). Нас больше интересует конкретная реализация возможностей этого класса, которую нам дают производ-
ООП |
Лекция 1 |
12 |
ные из него классы car, truck, boat. Рассматривая работу функции message класса vehicle, мы заметим, что при выполнении нашей программы только один раз (если не принимать в расчет первый вызов процедуры message, когда был определен объект класса vehicle) из трех вызывалась функция message класса vehicle. Если же в класс truck добавить свою функцию message, то реализация функции message в классе vehicle не будет представлять никакой ценности.
Таким образом, мы можем сделать два очень важных на данном этапе вывода:
класс vehicle нам ценен только как базовый класс
функция message класса vehicle стала нужна только для того, чтобы ее переопределили в производных классах.
Чисто виртуальным методом называют такой метод, который не определяется в базовом классе. У него нет тела, а есть только декларация о его существовании.
К примеру, если мы хотим представить класс vehicle в виде абстрактного класса, нам необходимо сделать две вещи: добавить метод message в класс truck и убрать тело функции в базовом классе vehicle. Тогда описание класса vehicle будет выглядеть следующим образом:
class vehicle
{
int wheels; float weight; public:
virtual void message(void) = 0; };
Класс, содержащий хотя бы один чистый виртуальный метод, называется абстрактным клас-
сом.
Абстрактные классы не бывают изолированными, т.е. всегда абстрактный класс должен быть наследуемым. Поскольку у чисто виртуального метода нет тела, то создать объект абстрактного класса невозможно. Кроме того, во избежание появления ошибки при вызове чистого виртуального метода, производный класс должен содержать декларирование и тело чистого виртуального метода.
Абстрактным классом можно назвать класс, специально определенный для обеспечения наследования характеристик порожденными классами.
Конструкторы и деструкторы.
Если Вы хотя бы в какой-то мере пытались разобраться в работе ООП, Вы обязательно натыкались на такие понятия, как Конструкторы и Деструкторы. Впрочем эти структуры могут иметь и другое название, к примеру, как в Visual Basic. Но это не имеет принципиального значения. Так что же означают эти понятия ? Я рассмотрю общие правила применения конструкторов и деструкторов в
С++.
К сожалению, а может быть и к счастью, понятия в ООП тесно взаимосвязаны между собой. Невозможно объяснить одно понятие отдельно от других. В ООП есть понятие области видимости объекта, переменной. Это понятие я возможно затрону по подробнее, но не сейчас. Переводя это понятие на нормальный человечекий язык - это время жизни чего(или кого)-либо. К примеру, человек зачат (извиняюсь за такие интимные подробности), рождается, живет, учится, работает, что-то делает и, к сожалению, умирает, его тело превращается в прах. Аналогично и с объектами. Проводя дальнейшую аналогию, конструктор работает на отрезке времени с момента зачатия до момента рождения. На этом этапе происходит формирование скелета объекта (инициализация). Также можно провести аналогию с таким детским конструктором как Лего. У вас есть кубики или части определенной конфигурации и из них собираете какую-то конструкцию (фигуру, замок и т.д.). Таким образом, конструктор отвечает за начальное формирование(инициализацию) объекта на этапе его создания.
Очень часто конструктор заимствуется из родительского класса, но далеко не всегда.
В языке С++ конструктор имеет тоже самое имя, что и имя класса и не может иметь какой-либо определенный возвращаемый тип, поскольку имеет заранее определенный возвращаемый тип - указатель на инициализируемый объект.
Следует отметить, что функция конструктора запускается на этапе описания (декларирования) объекта. Т.е. декларирование объекта MyObject класса CMyObject следующей строкой
CMyObject MyObject;
приводит к вызову конструктора этого класса для данного объекта. Казалось бы, мы еще ничего не сделали с объектом, только описали, а какая-то функция уже выполняется !
Для тех, кто собирается работать с языком С++ на заметку: Если декларирование объекта произведено до выполнения основного модуля, то функция конструктора будет выполнена до выполнения этого модуля.
ООП |
Лекция 1 |
13 |
Продолжая аналогию с челоческой жизнью, Деструктор работает на отрезке времени, когда человек уже умер, но еще не превратился в прах. Т.е. вернуть, к сожалению, его уже нельзя, но сделать какой-то завершающий аккорд еще можно. Чаще всего этот этап используется для "зализывания ран" - корректного освобождения блоков памяти, выделенных при работе с этим объектом. Дело в том, что при "умирании" объекта, "умирают" и ссылки на все блоки памяти, которые были выделены объекту в процессе работы. Если мы не освободим выделенные объекту блоки памяти, то никто и не подумает их освобождать. В результате получится, что мы храним данные, которые нам уже абсолютно не нужны и мы даже не имеем к ним доступа ! В больших программах это может привести не только к краху самой программы, но и к замедлению работы или даже зависанию операционной системы. Применение деструктора в этом случае помогает избежать утечек памяти(memory leaks).
Итак, когда объект уже "представился" и готов уйти в небытие, автоматически вызывается деструктор, естесственно, если он существует.
В языке С++ Деструктор имеет тоже имя, что и имя класса с предшествующей ему тильдой. Деструктор не имеет никакого возвращаемого типа.
Ниже я привел пример класса с Конструктором и Деструктором в С++. Этот код не имеет никакого реального смысла, но наглядно показывает как можно использовать конструктор и деструктор.
сlass CMyClass
{
public:
CMyClass(); // Конструктор класса CMyClass ~CMyClass(); // Деструктор класса CMyClass
private:
int MyInt; // переменная типа integer (целое число)
int *point; // переменная типа указатель на integer (целое число)
};
CMyClass::CMyClass() // Конструктор
{
MyInt=10; // На этапе инициализации объекта класса CMyClass присваиваем // переменной этого объекта MyInt значение 10
point = new int; // Выделяем блок памяти под целое число, на которое // будет указывать указатель
*point = 20; // Сохраняем в этот выделенный блок памяти число 20
}
CMyClass::~CMyClass() // Деструктор
{
MyInt=0; // Объект класса CMyClass уже фактически прекратил существование, // но мы присваиваем переменной класса MyInt значение 0
delete point; // Используем указатель на число для того, чтобы освободить
//блок памяти, выделенный под это число.
//Если мы этого здесь не сделаем, никто за нас это не сделает
}
И последнее, что хотелось бы отметить, реализация конструкторов и деструкторов в конкретных языках может отличаться или могут существовать какие-либо специфические особенности их реализации. Я сознательно коснулся только общих принципов их работы. Для более полного обзора данных структур конкретного языка программирования лучше всего обращается к описанию его разработчика. К примеру, в языке С++ существует ряд других, не указанных мной, особенностей реализации и автоматического генерирования (при их отстутствии) конструкторов.