Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Абстрактный класс.docx
Скачиваний:
12
Добавлен:
25.03.2015
Размер:
42 Кб
Скачать

Абстрактный класс в объектно-ориентированном программировании — базовый класс, который не предполагает создания экземпляров. Абстрактные классы реализуют на практике один из принципов ООП - полиморфизм. Абстрактный класс может содержать (и не содержать[1]) абстрактные методы и свойства. Абстрактный метод не реализуется для класса, в котором описан, однако должен быть реализован для его неабстрактных потомков. Абстрактные классы представляют собой наиболее общие абстракции, то есть имеющие наибольший объем и наименьшее содержание.

В С++ виртуальные функции (virtual functions) позволяют использовать полиморфизм (polymorhpism) классов. Так как виртуальные функции могут использоваться только внутри классов, то иногда их называют виртуальными методами (virtual methods). Прежде чем воспользоваться виртуальными методами, мы рассмотрим работу обычных методов класса.

Статическое или раннее связывание (static/early binding)

Давайте разберёмся, как происходит вызов обычных функций и методов классов. Вызов обычных функций и методов происходит через механизм, называемый статическим (статичным) связыванием (static binding) или ранним связыванием (early binding).

Раннее связывание использовалось во всех функциях и методах наших программ за исключением тех случаев, где мы использовали указатели на функции.

Когда мы запускаем сборку (building) программы, компилятор просматривает исходный код и превращает все операторы в команды процессора. Допустим, в коде встречается вызов какой-нибудь функции:

someFunction(arg); // some - какой-то

Если это обычная функция (не указатель на функцию), то при вызове используется механизм раннего связывания.

Во время компиляции для кода (определения) функции выделяется память, и назначаются адреса для каждого оператора. Первый адрес в определении (теле функции) является адресом функции. При вызове someFunction, процессор будет переходить на адрес функции и начнёт выполнять тело функции. Самое важное здесь то, что адрес функции назначается во время компиляции, и именно этот адрес используется при вызове функции. Это и есть раннее или статичное связывание. Т.е. имя функции крепко привязано к адресу функции.

Полиморфизм (polymorphism) и полиморфные типы (polymorphic types)

Рассмотрим гипотетическую ситуацию: в игре есть несколько типов монстров. Все монстры могут атаковать (attack) и перемещаться (move). При этом каждый вид монстров делает это по своему: у кого-то есть только когти, а у кого-то за пазухой припрятан гранатомёт. move и attack мы можем поместить в базовый для всех монстров класс:

Неплохо было бы иметь возможность хранить объекты всех этих классов вместе и использовать одинаковый синтаксис для вызова методов этих классов. Это и есть полиморфизм (polymorphism) - много (от греческого поли) форм (от греческого морф). Т.е. объекты этих классов должны храниться в одном массиве.

Понятно, что для этого не годится массив объектов, так как в таком массиве для элементов выделяется фиксированное количество памяти. Соответственно, для поддержки полиморфизма нужно использовать массив указателей. Какой тип указателей выбрать? Как мы выяснили раньше, в указателе на базовый тип можно хранить объект любого производного типа - базовый и производный классы являются совместимыми по типу. Почему используется указатели именно на базовый тип? Потому что это более общий класс и от него наследуют все производные классы.

Классы, используемые для получения эффекта полиморфизма, называют полиморфными типами (polymorphic types).

В C++ полиморфизм реализуется через виртуальные функции. Но прежде чем добавлять виртуальные функции к классам, мы рассмотрим динамическое связывание.

Позднее/динамическое связывание (late/dynamic binding)

Поздним связыванием в C++ обладают указатели на функции (function pointers). Мы их уже разбирали, поэтому сложностей возникнуть не должно. Сразу пример:

int someFunction (int arg);

int (*functionPointer)(int arg);

functionPointer = someFunction.

someFunction обладает ранним связыванием. Т.е. на этапе компиляции для этой функции выделяется участок памяти, а первый адрес этого участка становится адресом функции. Адрес функции жёстко привязан к имени функции - их нельзя отделить.

functionPointer обладает динамическим (dynamic) или поздним связыванием (late binding). На какую функцию указывает этот указатель, становится известно только во время выполнения программы.При этом functionPointer может указывать на любую функцию, т.е. значение указателя functionPointer может меняться во время выполнения программы. Это и есть позднее связывание.

Ещё одним примером позднего связывания в C++ являются виртуальные функции (virtual functions). На самом деле виртуальные методы - это обычные указатели на функции. Но об этом чуть позже.

Виртуальные функции/методы (virtual functions/methods)

Чтобы объявить функцию как виртуальную, необходимо добавить ключевое слово virutal перед именем возвращаемого типа:

class Base

{

public:

virtual void Method ()

{

cout << "Базовый класс\n";

}

};

Вносить изменения в производные классы не нужно. Хотя можно и там добавить ключевое слово virtual (это не обязательно). Теперь посмотрим на наш код:

Base* b = new Derived;

Derived* d = new Derived;

b->Method();

d->Method();

//-------- Вывод:

Производный класс

Производный класс

То что нужно! Теперь вызывается метод того класса, на который на самом деле указывает указатель. Наконец-то мы можем создать массив указателей на базовый класс и размещать там объекты любого производного класса:

BaseMonster* monsters[3];

monsters[0] = new MonsterA;

monsters[1] = new MonsterB;

monsters[2] = new MonsterC;

for (int i=0;i<3;++i)

monsters[i]->attack();

Несколько замечаний по виртуальным функциям:

  1. Виртуальные функции используются только в классах. Поэтому часто используется название - виртуальные методы.

  2. В массивах указателей на базовый класс можно хранить объекты только полиморфных типов (базовый и все производные).

  3. В массив нужно объединять только те объекты, которые обладают методами с одинаковыми названиями, но разной реализацией.

Как видите, пользоваться виртуальными методами очень просто. Теперь давайте разберёмся, какие механизмы стоят за виртуальными функциями.

Таблица виртуальных функций (virtual function table)

Рассмотрим простой код:

class Base

{

public:

virtual void vf ()

{

cout << "Базовый класс\n";

}

};

class Derived : public Base

{

public:

void vf () // это тоже виртуальная функция

{

cout << "Производный класс\n";

}

};

Функции Base::vf и Derived::vf являются виртуальными. Об этом говорит ключевое слово virtual в базовом классе. А производный класс наследует это свойство для своего метода.

Для виртуальных методов память выделяется точно так же, как и для обычных: на этапе компиляции под эти методы выделяются участки памяти, первые адреса которых являются адресами методов. Но так как методы виртуальные, то фактические адреса метода не привязывается к именам: Base::vf и Derived::vf. Адрес метода, который назначается на этапе компиляции при выделении памяти, будем называть настоящим (или фактическим) адресом.

Когда в базовом классе объявляется хотя бы одна виртуальная функция, то для всех полиморфных классов создаётся таблица виртуальных функций (virtual function table).

Встречаются разные названия этой таблицы: virtual function table, virtual method table, vtable, vftable.

Таблица виртуальных функций - это одномерный массив указателей на функции. Количество элементов в массиве равно количеству виртуальных функций в классе.

Для каждого полиморфного класса (базового и всех производных) создаётся своя таблица виртуальных методов. Количество элементов во всех этих таблицах одинаковое.

Именно в таблице виртуальных функций записываются настоящие адреса методов, т.е. элемент таблицы является указателем на функцию. Для всех полиморфных классов таблицы виртуальных функций будут содержать разные значения. Для каждого класса здесь будут записаны адреса методов данного класса.

Помимо создания виртуальной таблицы функций, в базовом классе объявляется поле __vfptr - указатель на vtable. Конечно же, этот указатель наследуется всеми производными классами. __vfptr можно увидеть при отладке.

__vfptr объекта указывает на vtable класса, которому принадлежит объект.

Рассмотрим пример. Допустим, в базовом классе определено две функции: f - не виртуальная и vf - виртуальная:

Base* object = new Derived;

object->f();

В данном случае компилятор не обращает внимания, объект какого типа на самом деле хранится в object. Компилятор смотрит на тип укзаталя и вызывает соответствующий метод - Base::f().

Base* object = new Derived;

object->vf();

В данном случае процессор видит, что vf - виртуальный метод. Поэтому он ищет в таблице виртуальных функций нужную запись. Но адрес таблицы виртуальных функций он узнаёт через __vfptr, а этот указатель указывает на таблицу своего класса. Соответственно, будет вызван метод того класса, чей объект вызывает метод vf.

Обратите внимание, что в обоих случаях компилятор отдыхает - он даже не пытается проверить тип объекта, на который указывает указатель. Просто при раннем и позднем связывании методы классов вызываются по-разному.

Виртуальный деструктор (virtual destructor)

Как мы видели в примере выше, при использовании обычных функций вызывается функция базового класса. Это же относится и к деструктору. Если в коде вы используете полиморфизм, то всегда объявляйте деструктор базового класса виртуальным. Иначе, при уничтожении всех объектов будет вызываться деструктор базового класса. Вот так правильно:

class Base

{

public:

virtual void method ()

{ cout << "Базовый класс\n";}

virtual ~Base()

{}

};

Хотя, в данном примере это не имеет значения - в данном деструкторе (и в деструкторах производных классов) ничего не происходит.

Абстрактные классы (abstract classes) и чистые виртуальные функции (pure virtual functions)

Очень часто в программах не требуется создавать объекты базовых классов. Т.е. базовые классы нужны только для того, чтобы построить иерархию классов и определить общие свойства для производных классов. Такие классы можно сделать абстрактными (abstract class). При попытке создания объекта абстрактного класса, компилятор выдаст ошибку.

Чтобы сделать класс абстрактным, нужно объявить одну из виртуальных функций чистой.

Чистая виртуальная функция (pure virtual function) как бы намекает, что она будет реализована в производных классах.

Чтобы сделать виртуальную функцию чистой (pure), нужно добавить после заголовка функции символы =0 (знак равенства и ноль):

class Base

{

public:

virtual void method () =0;

virtual ~Base() =0;

};

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

Символы =0 необязательно добавлять ко всем виртуальным функциям, достаточно добавить к одной.

Заключение

Работу функций трудно объяснить, используя язык программирования высокого уровня. Наиболее просто это сделать с помощью ассемблера, которого мы пока ещё не знаем.

В заключении я попытаюсь кратко резюмировать материал урока.

При вызове обычной функции во время выполнения программы, подставляется её адрес, который был присвоен на этапе компиляции. Это раннее или статическое связывание (early/static binding).

При использовании указателя на функцию, в нём хранится адрес фактического местоположения реальной функции. Этот адрес был назначен на этапе компиляции (абзац выше), но указатель может менять своё значение во время выполнении программы. Это позволяет вызывать с помощью указателя разные функции. Это пример позднего/динамического связывания (late/dynamic binding). Ещё одним примером позднего связывания являются виртуальные функции.

Виртуальные функции объявляются с помощью ключевого слова virtual в базовом классе. При этом для базового класса и для всех производных создаётся таблица указателей на функции - виртуальная таблица методов/функций (virtual function table или vtable). Для каждого класса создаётся своя таблица. Количество элементво в таблице равно количеству виртуальных методов. В таблице хранятся фактические адреса методов, определённых в классах. Также в базовом классе объявляется дополнительное поле __vfptr (наследуется всеми производными классами) - указатель на таблицу виртуальных функций класса. Т.е. когда создаётся объект самого класса или любого производного, в нём __vfptr присваивается адрес таблицы виртуальных функций этого класса (или производных).

Виртуальные функции нужны в C++ для поддержки полиморфизма. Полиморфизм позволяет использовать одинаковый синтаксис для разных классов:

Base* array[3];

array[0] = new Derived1;

array[1] = new Derived2;

array[2] = new Derived3;

for (int i=0;i<3;++i)

array[i]->method();

Виртуальные функции

Функция-член класса может содержать спецификатор virtual. Такая функция называется виртуальнойСпецификатор virtual может быть использован только в объявления нестатических функций-членов класса.

Если некоторый класс содержит виртуальную функцию, а производный от него класс содержит функцию с тем же именем и типами формальных параметров, то обращение к этой функции для объекта производного класса вызывает функцию, определённую именно в производном классе. Функция, определённая в производном классе, вызывается даже при доступе через указатель или ссылку на базовый класс. В таком случае говорят, что функция производного класса подменяет функцию базового класса. Если типы функций различны, то функции считаются разными, и механизм виртуальности не включается. Ошибкой является различие между функциями только в типе возвращаемого значения.

Виртуальную функцию можно использовать, даже если у её класса нет производных классов. Производный класс, который не нуждается в собственной версии виртуальной функции, не обязан её реализовывать.

Интерпретация вызова виртуальной функции зависит от типа объекта, для которого она вызывается, в то время как интерпретация вызова невиртуальной функции-члена класса зависит от типа указателя или ссылки, указывающей на этот объект.

Этот механизм делает производные классы и виртуальные функции ключевыми понятиями при разработке программ на С++. Базовый класс определяет интерфейс, для которого производные классы обеспечивают набор реализаций. Указатель на объект класса может передаваться в контекст, где известен интерфейс, определённый одним из его базовых классов, но этот производный класс неизвестен. Механизм виртуальных функций гарантирует, что этот объект всё равно будет обрабатываться функциями, определёнными для него, а не для базового класса.

Спецификатор virtual предполагает принадлежность функции классу, поэтому виртуальная функция не может быть ни глобальной функцией, ни статическим членом класса, поскольку вызов виртуальной функции нуждается в конкретном объекте для выяснения того, какую именно функцию следует вызывать.

Подменяющая функция в производном классе также считается виртуальной, даже при отсутствии спецификатора virtual.

Виртуальная функция может быть объявлена дружественной в другом классе.

Такое поведение, когда функции базового класса подменяются функциями производного класса, независимо от типа указателя или ссылки, называется полиморфизмом. Тип, имеющий виртуальные функции, называется полиморфным типом. Для достижения полиморфного поведения в языке С++ вызываемые функции-члены класса должны быть виртуальными, и доступ к объектам должен осуществляться через ссылки или указатели. При непосредственных манипуляциях с объектом (без использования указателя или ссылки) его точный тип известен компилятору, и поэтому полиморфизм времени выполнения не требуется.

Для реализации механизма виртуальности используются таблицы виртуальных функций. Каждый объект класса, имеющего виртуальные функции, содержит таблицу виртуальных функций, в которой хранятся адреса виртуальных функций, определённых для того класса, к которому реально принадлежит объект. Поскольку при создании объекта его тип известен, компилятор может определить адреса виртуальных функций этого класса и записать их в таблицу виртуальных функций.

При вызове виртуальной функции её адрес определяется не на этапе компиляции, а во время выполнения программы. Из таблицы виртуальных функций берётся элемент с определённым номером и вызывается функция, находящаяся по этому адресу. Таким образом, для вызова виртуальной функции требуется одна дополнительная операция выбора значения из таблицы виртуальных функций.

Соображения эффективности ни в коем случае не должны считаться препятствием для использования виртуальных функций. Для реализации механизма виртуальности требуется количество памяти, равное размеру указателя, на каждую виртуальную функции и одна операция выбора значения из памяти на каждый вызов виртуальной функции. Однако другие методы, реализующие подобный механизм, также потребуют накладных расходов. Например, при использовании поля типа также требуется память и время на выполнение проверок, причем если количество памяти невелико, то временные затраты, особенно при большом количестве производных классов, могут быть несопоставимо больше. Кроме того, механизм виртуальных функций позволяет легко модифицировать программы и добавлять новые классы.