Приветствую Вас Гость | RSS

ALLDev

Суббота, 21.12.2024, 15:56
Главная » Статьи » Программирование » C/C++/C#

Учебник C++. Производные классы, наследование. Часть 3

Абстрактные классы

Вернемся к примеру наследования, который мы рассматривали раньше. Мы ввели базовый класс   Item, который представляет общие свойства всех единиц хранения в библиотеке. Но существуют ли объекты класса Item? То есть существует ли в действительности "единица хранения" сама по себе? Конечно, каждая книга (класс Book ), журнал (класс Magazine ) и т.д. принадлежат и к классу Item, поскольку они выведены из него, однако объект самого базового класса вряд ли имеет смысл. Базовый класс – это некое абстрактное понятие, описывающее общие свойства других, конкретных объектов.

Тот факт, что в данном случае объекты базового класса не могут существовать сами по себе, обусловлен еще одним обстоятельством. Некоторые методыбазового класса не могут быть реализованы в нем, а должны быть реализованы в порожденных классах. Возьмем, например, тот же метод Name. Его реализация в базовом классе довольно условна, она не имеет особого смысла. Было бы логичнее вообще не реализовывать этот метод в базовом классе, а возложить ответственность за его реализацию на производные классы.

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

Такая ситуация складывается довольно часто в объектно-ориентированном программировании. (Вспомните пример с различными формами в графическом редакторе: рисование некой обобщенной формы невозможно.) В подобных случаях используется механизм абстрактных классов. Запишем базовый класс  Item немного по-другому:

class Item
{
public:
 . . .
 virtual String Name() const = 0;
};

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

Если метод объявлен чисто виртуальным, значит, он должен быть определен во всех классах, производных от Item. Наличие чисто виртуального методазапрещает создание объекта типа Item. В программе можно использовать указатели или ссылки на тип Item. Записи

Item it;
Item* itptr = new Item;

не разрешены, и компилятор сообщит об ошибке. Однако можно записать:

Book b;
Item* itptr = &b;
Item& itref = b;

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

Если по каким-либо причинам в производном классе   чисто виртуальный метод не определен, то этот класс тоже будет абстрактным, и любые попытки создать объект данного класса будут вызывать ошибку. Таким образом, забыть определить чисто виртуальный метод просто невозможно. Абстрактный   базовый класс навязывает определенный интерфейс всем производным из него классам. Собственно, в этом и состоит главное назначение абстрактных классов – в определении интерфейса для всей иерархии классов. Разумеется, это не означает, что в абстрактном классе не может быть определенных методов или атрибутов.

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

class A
{
public:
 virtual ~A() = 0;
};
A::~A()
{
. . .
}

Класс A – абстрактный, и объект типа A создать невозможно. Однако деструктор его определен и будет вызван при уничтожении объектов производных классов (о порядке выполнения конструкторов и деструкторов см. ниже).

Множественное наследование

В языке Си++ имеется возможность в качестве базовых задать несколько классов. В таком случае производный класс наследует методы и атрибуты всех его родителей. Пример иерархии классов в случае множественного наследования приведен на следующем рисунке.

Иерархия классов при множественном наследовании.

Рис. 10.2.  Иерархия классов при множественном наследовании.

В данном случае класс C наследует двум классам, A и B.

Множественное наследование – мощное средство языка. Приведем некоторые примеры использования множественного наследования.

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

В графическом редакторе для некоторых фигур может быть предусмотрен пояснительный текст. При этом все алгоритмы форматирования и печати пояснений работают с классом Annotation. Тогда те фигуры, которые могут содержать пояснение, будут представлены классами, производными от двух базовых классов:

class Annotation
{
public:
 String GetText(void);
private:
 String annotation;
};
class Shape
{
public:
 virtual void Draw(void);
};
class AnnotatedSquare : public Shape, 
 public Annotation
{
public:
 virtual void Draw();
};

У объекта класса AnnotatedSquare имеется метод GetTextунаследованный от класса Annotation, он определяет виртуальный метод   Drawунаследованный от класса Shape.

При применении множественного наследования возникает ряд проблем. Первая из них – возможный конфликт имен методов или атрибутов нескольких базовых классов.

class A
{
public:
 void fun();
 int a;
};
class B
{
public:
 int fun();
 int a;
};
class C : public A, public B
{
};

При записи

C* cp = new C;
cp->fun();

невозможно определить, к какому из двух методов fun происходит обращение. Ситуация называется неоднозначной, и компилятор выдаст ошибку. Заметим, что ошибка выдается не при определении класса C, в котором заложена возможность возникновения неоднозначной ситуации, а лишь при попытке вызова метода fun.

Неоднозначность можно разрешить, явно указав, к которому из базовых классов происходит обращение:

cp->A::fun();

Вторая проблема заключается в возможности многократного включения базового класса. В упомянутом выше примере интеграции библиотечной системы и системы кадров вполне вероятна ситуация, при которой классы для работников библиотеки и для студентов были выведены из одного и того же базового класса   Person:

class Person
{
public:
 String name();
};
class Student : public Person
{
. . .
};
class Librarian : public Person
{
. . .
};

Если теперь создать класс для представления студентов, подрабатывающих в библиотеке

class StudentLibrarian : public Student, 
 public Librarian
{
};

то объект данного класса будет содержать объект базового класса   Person дважды (см. рисунок 10.3).

Структура объекта StudentLibrarian.

Рис. 10.3.  Структура объекта StudentLibrarian.

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

StudentLibrarian* sp;
// ошибка – неоднозначное обращение, 
// непонятно, к какому именно экземпляру 
// типа Person обращаться 
sp->Person::name();
// правильное обращение
sp->Student::Person::name();

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

Виртуальное наследование

Базовый класс можно объявить виртуальным базовым классом, используя запись:

class Student : virtual Person
{
};
class Librarian : virtual Person
{
};

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

Структура объекта StudentLibrarian при виртуальном множественном наследовании.

Рис. 10.4.  Структура объекта StudentLibrarian при виртуальном множественном наследовании.

Категория: C/C++/C# | Добавил: artkil (23.09.2012)
Просмотров: 5949 | Теги: инкапсуляция, С++, полиморфизм, наследование | Рейтинг: 0.0/0
Всего комментариев: 0
Имя *:
Email *:
Код *: