Лекция 29 Наследование классов 


Мы поможем в написании ваших работ!



ЗНАЕТЕ ЛИ ВЫ?

Лекция 29 Наследование классов



Цели лекции: Изучение реализации создания классов на С++ с использованием принципа наследования и реализации полиморфного поведения классов.

 

Наследование классов в языке С++.

Наследование - один из основополагающих принципов объектно-ориентированного программирования. Под наследованием понимают возможность объявления производных типов на основе ранее объявленных типов.

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

Агрегирование предполагает возможность объявления в классе отдельных членов класса на основе ранее объявленных классов. Таким образом, агрегирование отражает отношение между классами “быть частью”. Примеры агрегирования: двигатель есть часть автомобиля, лепесток есть часть цветка, цветок есть часть растения. Ранее мы уже встречали примеры агрегирования классов – когда класс bank содержал список объектов класса client. При агрегировании классов агрегирующий класс также как и при наследовании получает возможность доступа к компонентным данным и методам агрегируемого класса (безусловно, с ограничениями, накладываемыми их областями видимости), но эти данные и методы не становятся собственностью объектов этого класса. Агрегируемый класс остается автономным объектом, что накладывает ряд ограничений на права доступа к его внутренней реализации (например, при наследовании защищенные компоненты базового класса доступны в производном, а при агрегировании – нет).

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

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

- Специализация. Дочерний класс является более конкретным, частным или специализированным случаем родительского класса. Другими словами, дочерний класс является подтипом родительского класса.

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

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

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

- Расширение. Дочерний класс добавляет новые функциональные возможности к родительскому классу, но не меняет наследуемое поведение.

- Ограничение. Дочерний класс ограничивает использование некоторых методов родительского класса.

- Варьирование. Дочерний и родительский классы являются вариациями на одну тему, и связь «класс—подкласс» произвольна.

- Комбинирование. Дочерний класс наследует черты более чем одного родительского класса.

 

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

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

class имя_класса: список_базовых_классов

{//определение собственных компонент

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

};

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

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

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

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

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

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

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

 

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

struct A

{private:

int a1;

public:                             

int a2;

void funcA()

};

//наследуем класс В от А

struct B:A  

{private:

int b1;

public:

int b2;

void funcB()

};

 

//наследуем класс С от В

struct C:B  

{Iprivate:

int c1;

public:

int c2;

void funcC()

};

 

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

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

Класс С можно разделить на три части – часть, косвенно унаследованную от А, часть, унаследованную от В, а также собственные компоненты класса С. Соответственно, структура класса В состоит из двух частей – унаследованной от А и собственных компонент класса. Объект-представитель класса 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. Таким образом, можно сделать вывод о том, что, во-первых, имена компонент, определенных в производном классе могут совпадать с именами унаследованных от базовых классов компонент, а во-вторых, такие одноименные компоненты существуют независимо друг от друга, могут быть одновременно доступны в классе с использованием квалифицированных имен компонент. Квалифицированное имя компонента класса может быть использовано и вне его компонентных функций. Для доступа в таком случае можно использовать выражение вида имя_объекта. имя_класса:: имя_компонента Пример использования квалифицированного имени компонент класса из его окружения: main(){  C c; c.C::c2=0; c.B::funcB(); c.A::a2=2; c.C::funcA(); }

Поиск компонента при обращении к нему всегда идет "снизу-вверх". Для вызова с.С::funcA() транслятор сначала проверит наличие функции funcA в классе С и, если такой имеется, занесет адрес метода этого класса для вызова. Если в классе С нет метода с таким названием, будет рассмотрен прямой базовый класс для С (в нашем случае – класс В) и поиск метода продолжится в нем. Если метод с таким именем будет найден – его адрес будет помещен на место вызова, иначе – поиск будет продолжен на следующем уровне иерархии классов (в классе А для рассматриваемого примера).

Еще одно важное свойство базовых и производных классов иллюстрируется в примере 3.

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

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, В, С.

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

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

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

{…};

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

 


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

Область видимости в базовом классе

Спецификатор доступа в списке порождения

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

 

производный класс объявлен через struct производный класс объявлен через class
public нет public private
public public public public
public protected protected protected
public private private private
protected нет public private
protected public protected protected
protected protected protected protected
protected private private private
private *

не доступны

 

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

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

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)

//d.y=1; Ошибка!!! компонент y в классе D имеет область

//видимости private (восьмие правило в таблице 1)

//d.z=0; Ошибка!!! компонент z в классе D имеет область

//видимости private (1-е правило в таблице 1)

}

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

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

#include <iostream>

#include <string.h>

using namespace std

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 в плане описания свойств студента дневного отделения. В частности, для студента дневного отделения определено компонентное данное stip (стипендия), значение которого вычисляется в компонентной функции этого же класса CalcStip в зависимости от текущего рейтинга студента. Схема иерархии классов программы изображена на рисунке 29.2.

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

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

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

 

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

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

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

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()

};

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

 

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

 

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

class B: public A {…};

class D: public A{…};

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

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

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). Именно из-за сложности управления одноименными унаследованными компонентами класса множественное наследование реализаций было запрещено в языках программирования, появившихся после С++ (например, в C# и Java).

Еще один вариант множественного наследования классов в языке С++ - использование виртуальных базовых классов. Если компоненты косвенного базового класса не должны дублироваться в классе-потомке, то он объявляется виртуальным:

class D {…};

class A: public virtual D {…};

class B: public virtual D {…};

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

Диаграмма классов в этом случае будет выглядеть как на рисунке 29.5б. Компоненты косвенного базового класса присутствуют в классе С в единственном экземпляре, проблемы неоднозначности доступа к ним не возникает.

 



Поделиться:


Последнее изменение этой страницы: 2021-02-07; просмотров: 364; Нарушение авторского права страницы; Мы поможем в написании вашей работы!

infopedia.su Все материалы представленные на сайте исключительно с целью ознакомления читателями и не преследуют коммерческих целей или нарушение авторских прав. Обратная связь - 3.142.197.198 (0.179 с.)