Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:

OOP / Лекция 5

.pdf
Скачиваний:
25
Добавлен:
20.04.2015
Размер:
204.29 Кб
Скачать

Лекция 5

1. Повторное использование классов: наследование и агрегирование

Наследование - один из основополагающих принципов объектно-ориентированного программирования. Под наследованием понимают возможность объявления производных типов на основе ранее объявленных типов. Свойство наследования присуще только классам. Как известно, в C++ существует фиксированное множество элементарных типов. Это абсолютно независимые типы и объявление одного элементарного типа на основе другого в принципе невозможно. Спецификации объявления unsigned int или long double нельзя рассматривать как модификации элементарных типов int и double. Это полноправные элементарные типы данных со своим собственным набором свойств. В C++ также невозможно определить одну функцию на основе другой ранее определённой.

Для класса в C++ реализуется возможность наследования. Наследование прежде всего является эффективным механизмом повторного использования классов, когда новые классы строятся при необходимости на базе уже существующих, а не с нуля. При этом необходимо различать понятия наследования и агрегирования. Наследование отражает отношения между классами «это есть». Примеры наследования: автомобиль есть транспортное средство, клиент банка есть человек, прямоугольник есть геометрическая фигура. Таким образом, при наследовании базовый и производный классы выступают как, соответственно, обобщение и конкретизация некоторого объекта реального мира. В случае наследования новый класс в буквальном смысле создаётся на основе ранее объявленного класса, наследует, а возможно и модифицирует его данные и функции. Объявленный класс может служить основой (базовым классом) для новых производных классов. Производные классы наследуют данные и функции своих базовых классов и добавляют собственные компоненты. Агрегирование предполагает возможность объявления в классе отдельных членов класса на основе ранее объявленных классов. Таким образом, агрегирование отражает отношение между классами “быть частью”. Примеры агрегирования: двигатель есть часть автомобиля, лепесток есть часть цветка, цветок есть часть растения. При агрегировании классов агрегирующий класс также как и при наследовании получает возможность доступа к компонентным данным и методам агрегируемого класса (безусловно, с ограничениями, накладываемыми их областями видимости), но эти данные и методы не становятся собственностью объектов этого класса. Агрегируемый класс остается автономным объектом, что накладывает ряд ограничений на права доступа к его внутренней реализации (например,при наследовании защищенные компоненты базового класса доступны в производном, а при агрегировании – нет). Возможность повторного использования классов важна не только и зачастую не столько из-за возможности уменьшения размера исходного текста программ. Построение систем классов с использованием механизмов наследования и агрегирования позволяет точнее описать в программе предметную область поставленной задачи, быстрее модифицировать код программы при необходимости, ускорить процесс проектирования и программирования. Любое понятие предметной области несуществует изолированно, оно существует во взаимосвязи с другимипонятиями, и мощность данного понятия во многом определяется наличием таких связей. Раз класс служит для представления понятий, встает вопрос, как представить взаимосвязь понятий. Понятие производного класса и поддерживающие его языковые средства служат для представления иерархических связей, иными словами, для выражения общности между классами. Например, понятия окружности и треугольника связаны между собой, так как оба они представляют еще понятие фигуры, тоесть содержат более общее понятие. Чтобы представлять в программе окружности и треугольники и при этом не упускать из вида, что они являются фигурами, надо явно определять классы «окружность» и «треугольник» так, чтобы было видно, что у них есть общий класс – «фигура». Это можно сделать, объявив класс «фигура» базовым, а классы «окружность» и «треугольник»-унаследовать от него.

2. Объявление наследования классов в С++

Определение класса, наследуемого от некоторых, уже существующих классов, производится следующим образом:

class имя_класса: список_базовых_классов {//определение собственных компонент

//переопределение унаследованных компонент базовых классов };

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

Общие правила порождения классов:

1)количество базовых классов в списке порождения может быть любым;

2)один и тот же класс не может быть задан в списке порождения дважды;

3)базовый класс к моменту определения производного должен быть определен или описан;

4)ни базовый, ни порожденный класс не могут быть определены с помощью ключевого слова union;

Рассмотрим использование механизма наследования в С++ на конкретном примере. Определим классы А, В и С, находящиеся в отношениях наследования:

//Листинг 20. Пример простого наследования классов

struct A {int a1; public: int a2;

void funcA() };

struct B:A //наследуем класс В от А {int b1;

public: int b2;

void funcB() };

struct C:B //наследуем класс С от В {int c1;

public: int c2;

void funcC() };

В приведенном примере класс С унаследован от класса В, а тот в свою очередь унаследован от класса А. При наследовании различают прямые и косвенные базовые классы. Прямой базовый класс упоминается в списке баз производного класса. Косвенным базовым классом считается класс, который является базовым для одного из классов, упомянутых в списке прямых баз данного производного класса. Класс А является прямым базовым классом для В и косвенным базовым (непрямым базовым) для С. Для удобства анализа иерархических систем классов, их отображают графически с

использованием

направленных

ациклических

графов,

в

вершинах

которых

располагаются названия классов, а ребра

отображают

отношения

классов

«базовый»-«производный» причем

стрелка всегда

направлена

от

производного

класса

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

Рисунок 3.- Пример графического отображения иерархии классов.

Рисунок 4. - Структура производного класса при наследовании

Класс С можно разделить на 3 части – часть, косвенно унаследованную от А, часть, унаследованную от В, а также собственные компоненты класса С. Соответственно, структура класса В состоит из двух частей – унаследованной от А и собственных компонент класса. Объект-представитель класса C является единым блоком объектов и включает собственные данные-члены класса C, а также данныечлены классов B и A. При создании объектов класса С в памяти будет выделяться 8 байт под компонентные данные объекта (4 компонента типа int). Для объектов класса С будут доступны методы базовых классов, при создании этих объектов будут вызываться конструктор как непосредственно класса С, так и объектов его базовых классов. При этом вызов конструкторов строго регламентирован – сначала вызываются конструкторы базовых классов, затем – конструкторы агрегированных в класс объектов (объектов других классов, которые являются компонентами данного класса) и в последнюю очередь – конструктор производного. Если конструкторы базовых классов имеют формальные параметры, то при определении конструктора производного класса необходимо предусмотреть вызов конструкторов базовых классовых с необходимыми фактическими параметрами:

конструктор (список_форм_параметров): конструктор_базового_класса_1 (список_факт_параметров), … , конструктор_базового_класса_n (список_факт_параметров)

{тело_конструктора}

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

Рассмотрим содержимое функции funcC:

void C::funcC()

{a2=0; //также возможные варианты обращения A::a2=0; B::a2=0; B::A::a2=0; C::a2=0

b2=0; //можно также B::b2=0; C::b2=0; Однако, нельзя A::b2=0 c2=0;// можно также C::c2=0; Однако, нельзя A::c2=0; B::c2=0 funcA(); //можно A::funcA(); B::funcA(); C::funcA()

}

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

имя_класса :: имя_компонента

Использование квалифицированного имени удобно тогда, когда в базовом и производном классах определены одноименные компоненты. Например, если бы в классе А было определено компонентное данное c2, то в функции funcC() выражение с2=5 изменяло бы значение компонента, определенного непосредственно в классе С, а для того, чтобы изменить значение унаследованного от А компонента, необходимо было бы использовать выражение A::c2=5 или B::c2=5. Таким образом, можно сделать вывод о том, что, во-первых, имена компонент, определенных в производном классе могут совпадать с именами унаследованных от базовых классов компонент, а вовторых, такие одноименные компоненты существуют независимо друг от друга, могут

быть одновременно доступны в классе с использованием квалифицированных имен компонент.

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

имя_объекта.имя_класса :: имя_компонента

//Листинг 21. Пример использования квалифицированного имени компонент класса

из

//его окружения: main()

{C c; c.C::c2=0; c.B::funcB();

c.A::a2=2;

c.C::funcA();

}

Поиск компонента при обращении к нему всегда идет "снизу-вверх". Для вызова

с.С::funcA() транслятор сначала проверит наличие функции

funcA в классе С и,

если такой имеется, занесет адрес метода этого класса для

вызова. Если в классе

С нет метода с таким названием, будет рассмотрен прямой базовый класс для С (в нашем случае – класс В) и поиск метода продолжится в нем. Если метод с таким именем будет найден – его адрес будет помещен на место вызова, иначе – поиск будет продолжен на следующем уровне иерархии классов (в классе А для рассматриваемого примера). Еще одно важное свойство базовых и производных классов иллюстрируется в листинге 22.

//Листинг 22. Приведение указателей производного класса к базовому

main() {A *pta; C c; pta=&c;

pta->funcA(); }.

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

main() {A *pta; C c; pta=&c;

// pta->funcС(); Ошибка !!! Указатель pta адресует только ту часть объекта с, которая унаследована от класса А

((С*)pta)->funcC(); //правильный вызов

}

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

A *mas[100];

можно заносить в него адреса объектов классов A, В, С. При наследовании классов компоненты базового класса становятся доступны из производного. При этом компонентам базового класса в производном присваивается некоторая область видимости, которая может и не совпадать с областью видимости, заданной для него при определении базового класса. Это иллюстрирует следующий пример.

//Листинг 23. Изменение области видимости компонент при наследовании

class A { … public:

int x;

};

class B: A {… };

main()

{A a; B b;

a.x=5; //в классе А комп. данное х общедоступное

//b.x=1; ошибка!!! В классе B унаследованный от А компонент х – частный

}

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

1)области видимости компонента в базовом классе;

2)способа определения производного класса (через class или struct);

3)спецификации доступа, указанной в списке базовых классов при объявлении наследования.

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

class A: public B, private C, protected D {…};

Правила, по которым изменяются области видимости компонент класса при наследовании, приведены в таблице 1.

Таблица 1.

Изменение области видимости компонент базового класса в производном

Следующий пример иллюстрирует приведенные в таблице правила трансформации области видимости компонент при наследовании.

// Листинг 24. Примеры изменения области видимости компонент при наследовании

class A { public:

int x;

};

class B {protected:

int y;

}; class C {public:

int z;

};

class D: public A, private B, C {… };

main() {D d;

d.x=5; //компонент х в классе D имеет область видимости public (2-е правило

//в таблице 1)

 

компонент y в классе D имеет область видимости private

//d.y=1;

Ошибка!!!

// (8-е

правило

в

таблице

1)

 

 

 

 

 

//d.z=0; Ошибка!!!

компонент

z в классе D имеет область видимости private

 

// (1-е

правило

в

таблице

1)

 

 

 

 

 

}

пример

создания

и

использования

иерархии

классов

с

Рассмотрим

использованием механизма наследования.

//Листинг 25. Программа “база данных по учету студентов”, использующая механизм наследования

#include <iostream.h> #include <string.h>

class Subject //класс, описывающий свойства некоторого субъекта

{protected:

char name[20]; //имя субъекта

int age;

//возраст

char adress[30]; //адрес

public:

//функция ввода информации о субъекте с клавиатуры

void Read();

void Write();

//функция вывода информации о субъекте на экран

};

 

class Student:public Subject //класс, описывающий свойства студента

{ char group[7];

//название группы, в которой учится студент

char numb[8];

//номер его зачетной книжки

int balls[10];

//оценки, полученные на экзамене

static int n;

//количество экзаменов в сессию

protected:

//рейтинг студента (среднее по баллам, полученным на

float rait;

экзаменах)

 

public:

//функция ввода баллов по предметам

void Exam();

void CalcRait(); //функция вычисления рейтинга студента

void ReadSt();

// функция ввода информации о студенте с клавиатуры

void WriteSt();

// функция вывода информации о студенте на экран

};

 

class DayStud:public Student //класс, описывающий свойства студента

дневной

 

// формы обучения

{int stip;

//стипендия студента

public:

//функция вычисления стипендии студента

void CalcStip();

void WriteSt();

//переопределенная функция вывода информации о студенте

};

 

// Определение методов классов void Subject::Read()

{ cout<<" Введите информацию\n Имя"; cin>>name;

cout<<"\n Возраст"; cin>> age; cout<<"\nАдрес"; cin>>adress;

}

void Subject::Write()

{cout<<" Имя "<<name<<" Возраст "<<age<<" Адрес "<<adress;} int Student::n=4;

void Student::ReadSt()

{Read();

cout<<"\nНомер зач.книжки"; cin>>numb; cout<<"\nГруппа"; cin>>group;

}

void Student::WriteSt() { Write();

cout<<"Номер зач.книжки "<<numb<<"Группа "<<group<<" Рейтинг "<<rait<<"\n";

}

void Student::CalcRait() { rait=0;

for(int i=0;i<n;i++) rait+=balls[i]; rait/=n;

}

void Student::Exam()

{ for (int j=0;j<n;j++)

{ cout<<"\nПредмет N"<<j+1; cin>>balls[j];

}}

void DayStud::CalcStip() {if (rait>=90) stip=300; else if (rait>=76)

stip=200; else stip=0;

}

void DayStud::WriteSt() { Student::WriteSt(); cout<<"Стипендия"<<stip;

}

//пример использования определенных выше классов main()

{ const int m=10; //будем работать с 10-ю студентами int i;

DayStud gr[m]; for(i=0;i<m;i++)

gr[i].ReadSt(); //вводим информацию о каждом студенте for(i=0;i<m;i++)

{ cout<<"Экзамены"<<i+1<<" студента";

gr[i].Exam(); //проводим экзамены (вводим информацию о баллах,

полученных //каждым студентом на экзаменах

}

for(i=0;i<m;i++)

gr[i].CalcRait(); //вычисляем рейтинг каждого студента for(i=0;i<m;i++)

gr[i].CalcStip(); //вычисляем стипендию каждого из студентов for(i=0;i<m;i++)

gr[i].WriteSt(); //выводим информацию о каждом студенте на экран

}

В приведенном выше примере определены три класса. Класс Subject является

корнем всей системы классов, в нем объединены свойства и методы, описывающие «субъекта», то есть свойства, присущие каждому человеку: имя, возраст, адрес, и методы, позволяющие обрабатывать эту информацию (вводить с клавиатуры, выводить на экран).

От класса Subject порожден класс Student, в котором определены свойства, присущие каждому студенту: номер зачетки, название группы, в которой учится студент, оценки, полученные им на экзаменах, рейтинг студента, вычисленный по результатам сессии. В классе также определен ряд методов, позволяющих изменять перечисленные свойства: вводить с клавиатуры, рассчитывать, выводить на экран. При этом, некоторые методы класса Student вызывают методы, унаследованные от родительского класса Subject. Так, например, для ввода информации о студенте в классе определена функция ReadSt, в которой непосредственно вводятся с клавиатуры лишь те компонентные данные, которые определены в классе Student. Для ввода значения компонент, унаследованных от Subject (очевидно, что для каждого студента необходимо хранить имя, возраст, адрес) вызывается унаследованный метод Read.

Третий класс называется DayStud является конкретизацией класса Student в плане описания свойств студента дневного отделения. В частности, для студента

Рисунок 5.- Иерархия классов программы ”база данных по учету студентов”. дневного отделения определено компонентное данное stip (стипендия), значение которого вычисляется в компонентной функции этого же класса CalcStip в зависимости от текущего рейтинга студента. Схема иерархии классов программы изображена на рис.5.

Может показаться несущественным отличие класса DayStud от класса Student, и возникнуть желание объединить их в одном классе. Однако, предложенная схема иерархии классов позволяет легко модифицировать программу, добавлять в нее новые классы, отличающиеся от уже определенных небольшими деталями реализации без значительных усилий со стороны программиста. Так, например, можно определить класс EvnStud , описывающий студента-вечерника просто унаследовав его от класса Student, так как все компоненты этого класса в полной мере относятся и к студентам вечерней формы обучения. При этом в класс EvnStud можно добавить некоторые компонентные данные, присущие только студентам-вечерникам (например, место постоянной работы). Можно пойти дальше и определить класс Teacher, описывающий преподавателя, и опять этот класс может появиться не на «ровном месте», а быть унаследован от класса Subject, так как все перечисленные для «субъекта» свойства и методы имеют отношение и к преподавателям. Возможная схема иерархии информационной системы ВУЗа приведена на рис. 6.

Рисунок 6. -Возможная иерархия классов для программы ”информационная система ВУЗа ”.

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

вступают в отношения обобщения-конкретизации. Однако, если мы захотим добавить в программу класс Group, описывающий учебную группу, то в отношение наследования с классом Student такой класс вступить не сможет: мы не можем сказать, что группаэто студент. Класс Student правильно будет агрегировать в класс Group (напомним

– для агрегирования отношения классов проверяются словосочетанием «состоит из»: группа состоит из студентов).

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

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

//Листинг26. Пример множественного наследования

class A {int a1; public: int a2;

void funcA() };

class B {int b1; public: int b2;

void funcB() };

class C: public A, public B //наследуем класс С от A и B {int c1;

public: int c2;

void funcC() };

Схема иерархии классов, определенных в последнем примере, изображена на рис.7

Рисунок 7. - Множественное наследование классов

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

class C: public B, public A { … };

то порядок следования компонент в объекте класса С изменится – в младших адресах будут располагаться компоненты объекта класса В, затем – объекта класса А. При множественном наследовании один и тот же класс не может быть дважды указан как прямой базовый, однако, косвенным базовым классом один и тот же класс может быть и более одного раза.

class A {public: int x; void funcA(); …}; class B: public A {…};

class D: public A{…};

class C: public B, public D {…};

Рисунок 8. - Множественное наследование с дублированием косвенного базового класса

Дублирование косвенного базового класса приводит к включению в производный класс нескольких объектов базового класса. Для класса С в последнем примере это означает, что компонентное данное x будет существовать в объектах данного класса в двух экземплярах – один унаследован через класс В, другой – через класс D. Структура объекта класса С изображена на рис. 9.

Рисунок 9. - Структура производного класса при множественном наследовании с дублированием косвенного базового класса.

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

main() { C c;

c.x=6; // Ошибка!!!

}

Попытка доступа к члену данных x для объекта с приводит к ошибке транслятора “Member is ambiguous A::x and A::x”. Эта ошибка означает, что транслятор не может определить, какому из двух компонент x класса необходимо присвоить новое значение. Неразрешимыми именами для транслятора будут также следующие

с.C::x и c.A::x.

Решением проблемы является использование квалифицированных имен компонент с использованием имен классов B и D. Для транслятора однозначно различаются следующие имена компонент:

с.B::x (компонента, унаследованная через класс В) и c.D::x (компонента, унаследованная через класс D).

Рассмотрим пример программы, реализующей множественное наследование. В программе реализованы класс Window, описывающий окно в текстовом режиме, и класс Text, описывающий буфер для хранения текстовой информации, а на их основе определен класс WinText, описывающий окно в текстовом режиме с возможностью отображения в нем текста.

//Листинг 27. Программа, использующая множественное наследование классов

#include<stdio.h>

#include<string.h> #include <conio.h>

class Window

//класс «окно в текстовом режиме»

{ protected:

//координаты левого верхнего угла окна

int x,y;

int dx,dy;

//размеры окна

int color,backcolor; //основной и фоновый цвета окна public:

Соседние файлы в папке OOP