учебники, программирование, основы, введение в,

 

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

Важнейшим свойством объектно-ориентированного программирования является наследование. Для того, чтобы показать, что класс В наследует класс A (класс B выведен из класса A), в определении класса B после имени класса ставится двоеточие и затем перечисляются классы, из которых B наследует:
class A
{
public:
A();
~A();
MethodA();
};
class B : public A
{
public:
B();
. . .
};
Термин "наследование" означает, что класс B обладает всеми свойствами класса A, он их унаследовал. У объекта производного класса есть все атрибуты и методы базового класса. Разумеется, новый класс может добавить собственные атрибуты и методы.
B b;
b.MethodA();  // вызов метода базового класса
Часто выведенный класс называют подклассом, а базовый класс – суперклассом. Из одного базового класса можно вывести сколько угодно подклассов. В свою очередь, производный класс может служить базовым для других классов. Изображая отношения наследования, их часто рисуют в виде иерархии или дерева.
Иерархия классов может быть сколь угодно глубокой. Если нужно различить, о каком именно классе идет речь, класс C называют непосредственным или прямым базовым классом класса D, а класс A – косвенным базовым классом класса D.
Предположим, что для библиотечной системы, которую мы разрабатываем, необходимо создать классы, описывающие различные книги, журналы и т.п., которые хранятся в библиотеке. Книга, журнал, газета и микрофильм обладают как общими, так и различными свойствами. У книги имеется автор или авторы, название и год издания. У журнала есть название, номер и содержание – список статей. В то же время книги, журналы и т.д. имеют и общие свойства: все это – "единицы хранения" в библиотеке, у них есть инвентарный номер, они могут быть в читальном зале, у читателей или в фонде хранения. Их можно выдать и, соответственно, сдать в библиотеку. Эти общие свойства удобно объединить в одном базовом классе. Введем класс Item, который описывает единицу хранения в библиотеке:
class Item
{
public:
Item();
~Item();
// истина, если единица хранения на руках

   bool IsTaken() const;
// истина, если этот предмет имеется
// в библиотеке

bool IsAvailable() const;
long GetInvNumber() const;  
// инвентарный номер 

void Take();      // операция "взять"
void Return();    // операция "вернуть"

private:
// инвентарный номер — целое число
long invNumber;
// хранит состояние объекта —
// взят на руки
bool taken;
}; 
Когда мы разрабатываем часть системы, которая имеет дело с процессом выдачи и возврата книг, вполне достаточно того интерфейса, который представляет базовый класс. Например:
// выдать на руки
void
TakeAnItem(Item& i)
{
. . .
if (i.IsAvailable())
i.Take();
}
Конкретные свойства книги будут представлены классом Book.
class Book : public Item
{
public: 
String Author(void) const;
String Title(void) const;
String Publisher(void) const;
long YearOfPublishing(void) const;
String Reference(void) const;

private:
String author;
String title;
String publisher;
short year;
};    // автор
// название
// издательство
// год выпуска
// полная ссылка
// на книгу 
Для журнала класс Magazin предоставляет другие сведения:
class Magazin : public Item
{
public: 
String Volume(void) const;
short Number(void) const;
String Title(void) const;
Date DateOfIssue() const;
private:
String volume;
short number;
String title;
Date date;
}; 
// том
// номер
// название
// дата выпуска 
Ключевое слово public перед именем базового класса определяет, что внешний интерфейс базового класса становится внешним интерфейсом порожденного класса. Это наиболее употребляемый тип наследования. Описание защищенного и внутреннего наследования будет рассмотрено чуть позже.
У объекта класса Book имеются методы, непосредственно определенные в классе Book и методы, определенные в классе Item.
Book b;
long in = b.GetInvNumber();
String t = b.Reference();
Производный класс имеет доступ к методам и атрибутам базового класса, объявленным во внешней и защищенной части базового класса, однако доступ к внутренней части базового класса не разрешен. Предположим, в качестве части полной ссылки на книгу решено использовать инвентарный номер. Метод Reference класса Book будет выглядеть следующим образом:
String
Book::Reference(void) const
{
String result = author + "\n"
+ title + "\n"
+ String(GetInvNumber());
return result;
(Предполагается, что у класса String есть конструктор, который преобразует целое число в строку.) Запись:
String result = author + "\n"
+ title + "\n"
+ String(invNumber);
не разрешена, поскольку invNumber – внутренний атрибут класса Item. Однако если бы мы поместили invNumber в защищенную часть класса:
class Item
{
. . .
protected:
long invNumber;
};
то методы классов Book и Magazin могли бы непосредственно использовать этот атрибут.
Назначение защищенной (protected) части класса в том и состоит, чтобы, закрыв доступ "извне" к определенным атрибутам и методам, разрешить пользоваться ими производным классам.
Если одно и то же имя атрибута или метода встречается как в базовом классе, так и в производном, то производный класс перекрывает базовый.
class A
{
public:
. . .
int foo();
. . .
};
class B : public A
{
public:
int foo();
void bar();
};
void
B::bar()
{
x = foo();  
// вызывается метод foo класса B
}
Однако метод базового класса не исчезает. Просто при поиске имени foo сначала просматриваются атрибуты и методы самого класса. Если бы имя не было найдено, начался бы просмотр имен в базовом классе, затем просмотр внешних имен. В данном случае имя foo существует в самом классе, поэтому оно и используется.
С помощью записи A::foo() можно явно указать, что нас интересует имя, определенное в классе A, и тогда запись:
x = A::foo();
вызовет метод базового класса.
Вообще, запись класс::имя уже многократно нами использовалась. При поиске имени она означает, что имя относится к заданному классу.

Виртуальные методы
В обоих классах, выведенных из класса Item, имеется метод Title, выдающий в качестве результата заглавие книги или название журнала. Кроме этого метода, полезно было бы иметь метод, выдающий полное название любой единицы хранения. Реализация этого метода различна, поскольку название книги и журнала состоит из разных частей. Однако вид метода – возвращаемое значение и аргументы – и его общий смысл один и тот же. Название – это общее свойство всех единиц хранения в библиотеке, и логично поместить метод, выдающий название, в базовый класс.
class Item
{
public:
virtual String Name(void) const;
. . .
};
class Book : public Item
{
public:
virtual String Name(void) const;
. . .
};
class Magazin : public Item
{
public:
virtual String Name(void) const;
. . .
};
Реализация метода Name для базового класса тривиальна: поскольку название известно только производному классу, мы будем возвращать пустую строку.
String
Item::Name(void) const
{
return "";
}
Для книги название состоит из фамилии автора, названия книги, издательства и года издания:
String
Book::Name(void) const
{
return author + title + publisher +
String(year);
}
У журнала полное название состоит из названия журнала, года и номера:
String
Magazin::Name(void) const
{
return title + String(year) +
String(number);
}
Методы Name определены как виртуальные с помощью описателя virtual, стоящего перед определением метода. Виртуальные методы реализуют идею полиморфизма в языке Си++. Если в программе используется указатель на базовый класс   Item и с его помощью вызывается метод Name:
Item* ptr;
. . .
String name = ptr->Name();
то по виду вызова метода невозможно определить, какая из трех приведенных выше реализаций Name будет выполнена. Все зависит от того, на какой конкретный объект указывает указатель ptr.
Item* ptr;
. . .
if (type == "Book")
ptr = new Book;
else if (type == "Magazin")
ptr = new Magazin;
. . .
String name = ptr->Name();
В данном фрагменте программы, если переменная type, обозначающая тип библиотечной единицы, была равна "Book", то будет вызван метод Name класса Book. Если же она была равна "Magazin", то будет вызван метод класса Magazin.
Виртуальные методы позволяют программировать действия, общие для всех производных классов, в терминах базового класса. Динамически, во время выполнения программы, будет вызываться метод нужного класса.
Приведем еще один пример виртуального метода. Предположим, в графическом редакторе при нажатии определенной клавиши нужно перерисовать текущую форму на экране. Форма может быть квадратом, кругом, эллипсом и т.д. Мы введем базовый класс для всех форм Shape. Конкретные фигуры, с которыми работает редактор, будут представлены классами Square (квадрат), Circle (круг), Ellipse (эллипс), производными от класса Shape. Класс Shape определяет виртуальный метод   Draw для отображения формы на экране.
class Shape
{
public:
Shape();
virtual void Draw(void);
};
//
// квадрат
//
class Square : public Shape
{
public:
Square();
virtual void Draw(void);
private:
double length;   // длина стороны
};
//
// круг
//
class Circle : public Shape
{
public:
Circle();
virtual void Draw(void);
private:
short radius;
};
. . .
Конкретные классы реализуют данный метод, и, разумеется, делают это по-разному. Однако в функции перерисовки текущей формы, если у нас имеется указатель на базовый класс, достаточно лишь записать вызов виртуального метода, и динамически будет вызван нужный алгоритм рисования конкретной формы в зависимости от того, к какому из классов (Square, Circle и т.д.) принадлежит объект, на который указывает указатель shape:
Repaint(Shape* shape)
{
shape->Draw();
}
Виртуальные методы и переопределение методов
Что бы изменилось, если бы метод Name не был описан как виртуальный? В таком случае решение о том, какой именно метод будет выполняться, принимается статически, во время компиляции программы. В примере с методом Name, поскольку мы работаем с указателем на базовый класс, был бы вызван метод Name класса Item. При определении метода как virtual решение о том, какой именно метод будет выполняться, принимается во время выполнения.
Свойство виртуальности проявляется только тогда, когда обращение к методу идет через указатель или ссылку на объект. Указатель или ссылка могут указывать как на объект базового класса, так и на объект производного класса. Если же в программе имеется сам объект, то уже во время компиляции известно, какого он типа и, соответственно, виртуальность не используется.
func(Item item)
{
item.Name();
}
func1(Item& item)
{
item.Name();

 

  // вызывается метод Item::Name()

 

  // вызывается метод в соответствии
// с типом того объекта, на который
// ссылается item 
Преобразование базового и производного классов
Объект базового класса является частью объекта производного класса. Если в программе используется указатель на производный класс, то его всегда можно без потери информации преобразовать в указатель на базовый класс. Поэтому во многих случаях компилятор может выполнить такое преобразование автоматически.
Circle* pC;
. . .
Shape* pShape = pC;
Обратное не всегда верно. Преобразование из базового класса в производный не всегда можно выполнить. Поэтому говорят, что преобразование
Item* iPtr;
. . .
Book* bPtr = (Book*)iPtr;
небезопасно. Такое преобразование можно выполнять только тогда, когда точно известно, что iPtr указывает на объект класса Book.
Внутреннее и защищенное наследование
До сих пор мы использовали только внешнее наследование. Однако в языке Си++ имеется также внутреннее и защищенное наследование. Если перед именем базового класса ставится ключевое слово private, то наследование называется внутренним.
class B : private A
{
. . .
};
В случае внутреннего наследования внешняя и защищенная части базового класса становятся внутренней частью производного класса. Внутренняя часть базового класса остается для производного класса недоступной.
Если перед именем базового класса поставить ключевое слово protected, то будет использоваться защищенное наследование. При нем внешняя и защищенная части базового класса становятся защищенной частью производного класса. Внутренняя часть базового класса остается недоступной для производного класса.
Фактически, при защищенном и внутреннем наследовании   производный класс исключает из своего интерфейса интерфейс базового класса, но сам может им пользоваться. Разницу между защищенным и внутренним наследованием почувствует только класс, выведенный из производного.
Если в классе A был определен какой-то метод:
class A
{
public:
int foo();
};
то запись
B b;
b.foo();
недопустима, так же, как и
class C
{
int m() {
foo();
}
};
если класс B внутренне наследует A. Если же класс B использовал защищенное наследование, то первая запись b.foo() также была бы неправильной, но зато вторая была бы верна.

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

Вернемся к примеру наследования, который мы рассматривали раньше. Мы ввели базовый класс   Item, который представляет общие свойства всех единиц хранения в библиотеке. Но существуют ли объекты класса Item? То есть существует ли в действительности "единица хранения" сама по себе? Конечно, каждая книга (класс Book), журнал (класс Magazin) и т.д. принадлежат и к классу 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 создать невозможно. Однако деструктор его определен и будет вызван при уничтожении объектов производных классов (о порядке выполнения конструкторов и деструкторов см. ниже).

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

В языке Си++ имеется возможность в качестве базовых задать несколько классов. В таком случае производный класс наследует методы и атрибуты всех его родителей. Пример иерархии классов в случае множественного наследования приведен на следующем рисунке.
В данном случае класс 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 дважды.
Кроме того, что подобная ситуация отражает нерациональное использование памяти, никаких неудобств в данном случае она не вызывает. Возможную неоднозначность можно разрешить, явного указав класс:

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

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

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

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

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

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

 
На главную | Содержание | < Назад....Вперёд >
С вопросами и предложениями можно обращаться по nicivas@bk.ru. 2013 г.Яндекс.Метрика