Наследование классов
Объекты разных классов и сами классы могут находиться в отношении наследования, при котором формируется иерархия объектов, соответствующая заранее предусмотренной иерархии классов.
Иерархия классов позволяет определять новые классы на основе уже имеющихся. Имеющиеся классы обычно называют базовыми (иногда порождающими), а новые классы, формируемые на основе базовых, - производными (порожденными), иногда классами-потомками или наследниками. Производные классы "получают наследство" - данные и методы своих базовых классов - и, кроме того, могут пополняться собственными компонентами (данными и собственными методами). Наследуемые компоненты не перемещаются в производный класс, а остаются в базовых классах. Сообщение, обработку которого не могут выполнить методы производного класса, автоматически передается в базовый класс. Если для обработки сообщения нужны данные, отсутствующие в производном классе, то их пытаются отыскать автоматически и незаметно для программиста в базовом классе (рис. 8.1).
Рис. 8.1. Схема обработки сообщений в иерархии объектов;
1 - обработка сообщения методами производного класса;
2 - обработка сообщения методами базового класса.
При наследовании некоторые имена методов (компонентных функций) и (или) компонентных данных базового класса могут быть по-новому определены в производном классе. В этом случае соответствующие компоненты базового класса становятся недоступными из производного класса. Для доступа из производного класса к компонентам базового класса, имена которых повторно определены в производном, используется операция ' :: ' указания (уточнения) области видимости.
Любой производный класс может, в свою очередь, становиться базовым для других классов, и таким образом формируется направленный граф иерархии классов и объектов. В иерархии производный объект наследует разрешенные для наследования компоненты всех базовых объектов. Другими словами, у объекта имеется возможность доступа к данным и методам всех своих базовых классов.
Наследование в иерархии классов может отображаться и в виде дерева, и в виде более общего направленного ациклического графа. Допускается множественное наследование - возможность для некоторого класса наследовать компоненты нескольких никак не связанных между собой базовых классов. Например, класс "окно на экране" и класс "сообщение" совместно могут формировать новый класс объектов "сообщение в окне".
При наследовании классов важную роль играет статус доступа (статус внешней видимости) компонентов. Для любого класса все его компоненты лежат в области его действия. Тем самым любая принадлежащая классу функция может использовать любые компонентные данные и вызывать любые принадлежащие классу функции. Вне класса в общем случае доступны только те его компоненты, которые имеют статус public.
В иерархии классов соглашение относительно доступности компонентов класса следующее.
Собственные (private) методы и данные доступны только внутри того класса, где они определены.
Защищенные (protected) компоненты доступны внутри класса, в котором они определены, и дополнительно доступны во всех производных классах.
Общедоступные (public) компоненты класса видимы из любой точки программы, т.е. являются глобальными.
Если считать, что объекты, т.е. конкретные представители классов, обмениваются сообщениями и обрабатывают их, используя методы и данные классов, то при обработке сообщения используются, во-первых, общедоступные члены всех классов программы; во-вторых, защищенные компоненты базовых и рассматриваемого классов и, наконец, собственные компоненты рассматриваемого класса. Собственные компоненты базовых и производных классов, а также защищенные компоненты производных классов недоступны для сообщения и не могут участвовать в его обработке.
Еще раз отметим, что на доступность компонентов класса влияет не только явное использование спецификаторов доступа (служебных слов) - private (собственный), protected (защищенный), public (общедоступный), но и выбор ключевого слова class, struct, union, с помощью которого объявлен класс.
Определение производного класса. В определении и описании производного класса приводится список базовых классов, из которых он непосредственно наследует данные и методы. Между именем вводимого (нового) класса и списком базовых классов помещается двоеточие. Например, при таком определении
class S: X, Y, Z { ... };
класс S порожден классами X, Y, Z, откуда он наследует компоненты. Наследование компонента не выполняется, если его имя будет использовано в качестве имени компонента в определении производного класса s. Как уже говорилось, по умолчанию из базовых классов наследуются методы и данные со спецификаторами доступа - public (общедоступные) и protected (защищенные).
В порожденном классе эти унаследованные компоненты получают статус доступа private, если новый класс определен с помощью ключевого слова class, и статус доступа public, если новый класс определен как структура, т.е. с помощью ключевого слова struct. Таким образом, при определении класса struct J: X, Z { ... ); любые наследуемые компоненты классов X, Z будут иметь в классе J статус общедоступных (public). Пример:
class B {
protected: int t;
public: char u;
};
class E: В { ... }; // t, u наследуются как private
struct S: В { ... }; // t, u наследуются как public
Явно изменить умалчиваемый статус доступа при наследовании можно с помощью спецификаторов доступа - private, protected и public. Эти спецификаторы доступа указываются в описании производного класса непосредственно перед нужными именами базовых классов. Если класс в определен так, как показано выше, то можно ввести следующие производные классы:
class M: protected В { ... }; // t, u наследуются как protected
class P: public В {...}; // t - protected, u – public
class D: private В { ... }; // t и u наследуются как private
struct F: private В {...}; // t и u наследуются как private
struct G: public В {...); // t - protected, u - public
Соглашения о статусах доступа при разных сочетаниях базового и производного классов иллюстрирует таблица, приведённая ниже.
Статусы доступа при наследовании
Доступ в базовом классе | Спецификатор доступа перед базовым классом | Доступ в производном классе | |
struct | class | ||
public | отсутствует | public | private |
protected | отсутствует | protected | private |
private | отсутствует. | недоступны | недоступны |
public | public | public | public |
protected | public | protected | protected |
private | public | недоступны | недоступны |
public | protected | protected | protected |
protected | protected | protected | protected |
private | protected | недоступны | недоступны |
public | private | private | private |
protected | private | private | private |
private | private | недоступны | недоступны |
Обратите внимание на тот факт, что ни базовый класс, ни производный не могут быть объявлены с помощью ключевого слова union. Таким образом, объединения не могут использоваться при построении иерархии классов.
Чтобы проиллюстрировать некоторые особенности механизма наследования, построим на основе класса Figure производный класс Square (квадрат). При этом Square унаследует следующие компоненты класса Figure:
общедоступные (public, наследование интерфейса):
· show() функция отображения фигуры на экране (строка 1.3);
· hide() функция стирания фигуры с экрана (строка 1.4);
· move(int x, int y) функция изменений координаты фигуры (строка 1.5);
· Figure() конструктор класса (строка 1.2);
· InitGraphic() статическая функция инициализации графики (строка 1.6). защищённые (protected, наследование реализации):
· static HDC hdc статический дескриптор графики;
· display() функция выводит имя класса на экран (строка 1.1);
частные (private):
· static HWND hwnd.
Как видно из определения функций класса Figure, некоторые функции пустые, так как предполагается, что класс Figure описывает абстрактное понятие геометрической фигуры, которое будет конкретизировано с помощью классов наследников. Действительно, как мы видим ниже в программе, класс Figure является базовым для классов Square, ClsEllipse. В этих производных классах переопределены (наполнены конкретным содержанием) интерфейсные функции show(), hide(), move(int x, int y), (строки 1.11 – 1.13, 1.16-1.18) добавлены компонентные данные, необходимые для хранения данных о координатах квадрата и эллипса на экране.
Таким образом, на наследование можно смотреть как на способ добавления компонентных функций и компонентных данных к уже существующему классу. При этом класс наследник как бы уточняет, специализирует базовый класс. Говорят, что при этом мы переходим от общего (обобщённого понятия Figure) к частному (конкретному Square и ClsEllipse).
При наследовании употребляют два термина – наследование интерфейса и наследование реализации. В данном случае, поскольку при определении класса Square перед базовым классом Figure стоит ключевое слово public, то класс Square унаследовал от Figure интерфейс, т.е. все общедоступные (интерфейсные) функции Figure стали интерфейсными функциями и класса Square. Кроме того, класс Square унаследовал от класса Figure реализацию функции display(). Действительно, функция display() не была интерфейсной в классе Figure, но в классе Square мы её переопределяем (строка 1.10) и делаем интерфейсной, причем в её теле вызываем ещё и (Figure::display()) функцию базового класса. Другими словами мы в классе Square воспользовались унаследованной реализацией функции display() из базового класса.
MFC
// Figura.cpp:
#include "stdafx.h"
#include "afxwin.h"
#include "iostream"
using namespace std ;
class Figure{
static HWND hwnd;
protected:
static HDC hdc;
void display(){cout<<" \n Figure ";} //1.1
public:
Figure(){/*cout<<"\n Figure()";*/} //1.2
void show(){} //1.3
void hide(){} //1.4
void move(int x, int y){} //1.5
static void InitGraphic(){hwnd=FindWindow(_T("ConsoleWindowClass"),NULL);
hdc=GetWindowDC(hwnd);} //1.6
static void CloseGraphic(){ReleaseDC(hwnd, hdc); CloseHandle(hwnd);} //1.7
~Figure(){/*cout<<"\t ~Figure()";*/} //1.8
};
HWND Figure::hwnd = 0;
HDC Figure::hdc = 0;
//
//определение класса Square производного от Figure (наследование)
class Square: public Figure {
POINT pt[5]; //Координаты вершин квадрата на экране
public:
//Конструктор с одним параметром, который может также играть роль конструктора //преобразования типа
Square(POINT* p){for(int i =0 ; i <5; i++){pt[i].x = p[i].x;pt[i].y = p[i].y;}} //1.9
void display(){cout<<" \n Square ->"; Figure:: display(); } //1.10
void show(){ //1.11
CPen pen(PS_SOLID,2,RGB(255,0,0));
SelectObject(hdc,pen);
Polyline(hdc,pt,5 );
}
void hide(){ //1.12
CPen pen(PS_SOLID,2,RGB(0,0,0));
SelectObject(hdc,pen);
Polyline(hdc,pt,5 );
}
void move(int x, int y){for(int i = 0; i<5;i++){ pt[i].x+=x;pt[i].y+=y;} } //1.13
~Square(){/*cout<<"\t ~Square()";*/} //1.14
};
//
//определение класса ClsEllipse производного от Figure (наследование)
class ClsEllipse: public Figure {
CPoint pt1, pt2; //Координаты эллипса на экране
public:
ClsEllipse(){/*cout<<"\t ClsEllipse()";*/ //1.15
pt1.x=100; pt1.y=100;
pt2.x=200; pt2.y=200;
}
void show(){ //1.16
CPen pen(PS_SOLID,2,RGB(0,255,0));
SelectObject(hdc,pen);
Arc(hdc,pt1.x,pt1.y,pt2.x,pt2.y,100,200,0,100);
}
void hide(){ //1.17
CPen pen(PS_SOLID,2,RGB(0,0,0));
SelectObject(hdc,pen);
Arc(hdc,pt1.x,pt1.y,pt2.x,pt2.y,100,200,0,200);
}
void move(int x, int y){ pt1.x+=x,pt1.y+=y,pt2.x+=x,pt2.y+=y; } //1.18
~ClsEllipse(){/*cout<<"\t ~ClsEllipse()";*/} //1.19
};
//
//Определение класса MyObject (включение объектов)
class MyObject{
Square sq1, sq2; //Композиция (агрегирование по значению) //1.20
ClsEllipse& elp; //Агрегация (агрегирование по ссылке) //1.21
public:
MyObject(const Square& p1,const Square& p2,ClsEllipse& el):sq1(p1),sq2(p2), elp(el) //1.22 {/*cout<<"\t MyObject()";*/}
void show(){sq1.show(); sq2.show();elp.show();} //1.23
void move(int x, int y){sq1.move(x,y); sq2.move(x,y);elp.move(x,y);}//1.24
void hide(){sq1.hide(); sq2.hide(); elp.hide();} //1.25
~MyObject(){/*cout<<"\n ~MyObject()";*/} //1.26
};
//
// Определение класса Heir (множественное наследование)
class Heir: public Square, public ClsEllipse{ //1.27
public:
Heir(POINT *p):Square(p),ClsEllipse(){/*cout<<"\t Heir()";*/ } //1.28
void show(){Square::show(); ClsEllipse::show();} //1.29
void move(int x, int y){Square::move(x,y); ClsEllipse::move(x,y);}//1.30
void hide(){Square::hide(); ClsEllipse::hide();} //1.31
~Heir(){/*cout<<"\n ~Heir()";*/} //1.32
};
void ShowMyObject(MyObject obj){ //1.33
for(int i = 0 ; i <100 ; i++){obj.show(); Sleep(24); obj.hide(); obj.move(4,0);}
}
Кроме наследования в программировании используется также приём агрегирования (включения в себя). Например, класс MyObject агрегирует два объекта класса Square по значению (это называется композицией) и один объект класса ClsEllipse по ссылке (строки 1.20, 1.21). Разный способ агрегирования говорит о том, что объекты класса Square будут уничтожены во время уничтожения объекта MyObject, а объект класса ClsEllipse может продолжать существовать.
Способ агрегирования зависит от нашего представления (интерпретации) решаемой задачи. Например, пусть под объектами класса MyObject мы подразумеваем автомобиль, у которого есть две двери (объекты класса Square) и прицеп объект класса ClsEllipse. Двери являются частью автомобиля и без него существовать не могут, поэтому объекты класса Square мы включаем в объект MyObject по значению, а вот прицеп может существовать и без автомобиля, поэтому объект класса ClsEllipse агрегируется в MyObject по ссылке. Она даёт возможность объекту MyObject посылать сообщения (управлять) объектом ClsEllipse, но при уничтожении объекта (машины) MyObject, объект ClsEllipse (прицеп) остаётся целым и возможно будет прицеплен к другому объекту MyObject.
Как мы видим из программы, в классе MyObject также переопределены функции show(), hide(), move(int x, int y) строки 1.23-1.25. В теле этих функций вызываются функции с таким же именем, но для объектов Square и объекта ClsEllipse. Это можно интерпретировать как управление (по средствам посылки сообщений) объектом MyObject его составными частями, чтобы они «двигались» синхронно как единое целое.
Таким образом, агрегирование - это способ взаимодействия классов при возникновении между ними отношения включения части в состав целого.
В программе выше определён ещё один класс Heir, он является производным от классов Square, ClsEllipse (строка 1.27). Поскольку базовых классов несколько, то такое наследование называют множественным. Более подробно множественное наследование мы будем рассматривать в следующем параграфе. Тем не менее, хотелось бы отметить, что множественное наследование стоит применять тогда, когда происходит объединение несколько равноправных понятий в единое производное понятие. Например, пусть под Square мы понимаем рамку картины, а под ClsEllipse холст. Тогда Heir это картина в целом. Поскольку картина на экране монитора должна двигаться как единое целое, то мы в классе Heir также переопределяем функции show(), hide(), move(int x, int y) строки 1.29-1.31, в теле которых происходит вызов этих же самых функций, но для объектов Square и ClsEllipse. Таким образом, Heir управляет своими частями.
Существует мнение, что множественное наследование — это неверная концепция, порождённая неверным анализом и проектированием, и в нашем случае лучше было бы создать класс Heir (картина), который бы агрегировал по значению объекты Square (рамка) и по ссылке ClsEllipse (холст), а не наследовал их.
void main(){
POINT pt1[5]; //2.1
pt1[0].x = 40;pt1[0].y=40;
pt1[1].x = 40;pt1[1].y=140;
pt1[2].x = 140;pt1[2].y=140;
pt1[3].x = 140;pt1[3].y=40;
pt1[4].x = 40;pt1[4].y=40; //2.2
Figure::InitGraphic(); //2.3
{ //2.4
Square sq1(pt1); ClsEllipse elp; //2.5
for(int i = 0 ; i <100 ; i++){ //2.6
sq1.show(); elp.show(); Sleep(24);
sq1.hide(); elp.hide(); //2.7
sq1.move(1,1); elp.move(2,2); //2.8
} //2.9
} //2.10
ClsEllipse elp;
Square sq2(pt1);
sq2.move(20,20);
MyObject obj(pt1, sq2, elp); //2.11
getchar();
ShowMyObject(obj); //2.12
{
Heir hr(pt1); //2.13
getchar();
for(int i = 0 ; i <100 ; i++){
hr.show(); Sleep(24); hr.hide(); hr.move(0,3);
}
}
Figure::CloseGraphic(); //2.14
}
Рассмотрим функцию main, в которой создаются объекты классов, определённых выше. В строках с 2.1 по 2.2 создаётся и инициализируется массив pt1 элементов типа POINT, который будет использован в качестве фактического параметра в конструкторах объектов класса Square. В строке 2.3 вызывается статическая функция, которая инициализирует графику.
Строки, начиная с 2.4 по 2.10 Заключены в блок. Это сделано для того, чтобы изучить последовательность вызовов конструкторов при создании объектов класса Square, ClsEllipse в строке 2.5 и деструкторов при выходе из блока.
В строке 2.5 создаются два объекта sq1, и elp типа Square и ClsEllipse. Созданием объектов занимаются конструкторы (строки 1.9 и 1.15). Поскольку у конструктора Square(POINT* p) один параметр, то он является одновременно и конструктором преобразования типа. Именно в таком качестве он отрабатывает в строке 2.11 для первого фактического параметра pt1 типа POINT при создании объекта obj. Рассмотрим подробно на примере объекта sq1 процесс его формирования. При выполнении оператора Square sq1(pt1), как уже говорилось выше, вызывается конструктор:
Square(POINT* p)/*Точка вызова конструктора по умолчанию Figure() базового класса*/ {for(int i =0 ; i <5; i++){pt[i].x = p[i].x;pt[i].y = p[i].y;}},
которому в качестве параметра передаётся указатель на массив элементов типа POINT.
Прежде чем начнут выполняться операторы тела конструктора Square, произойдёт вызов конструктора по умолчанию базового класса Figure() (строка 1.2). Таким образом, объект конструируется снизу-вверх, т.е. сначала вызывается конструктор самого нижнего (базового класса), потом поднимаясь выше по иерархии, конструктор следующего класса и т.д. Последним начнёт выполняться конструктор класса создаваемого объекта. Как видно из определения конструктора Square конструктор для базового класса Figure вызывается неявно. Это можно сделать и явно с помощью списка инициализации. В этом случае конструктор Square будет выглядеть следующим образом
Square(POINT* p): Figure() {for(int i =0 ; i <5; i++){pt[i].x = p[i].x;pt[i].y = p[i].y;}}
Список инициализации полезен, так как с помощью него до формирования объекта можно вызвать не только конструкторы по умолчанию для базового класса, но и просто конструкторы. Например, так сделано в конструкторе класса MyObject
MyObject(const Square& p1,const Square& p2,ClsEllipse& el):sq1(p1),sq2(p2), elp(el){}
В списке инициализации вызывает конструкторы с параметрами (не по умолчанию) для объектов, которые являются его составными частями. Если бы список инициализации отсутствовал, то код конструктора выглядел бы следующим образом:
MyObject(const Square& p1,const Square& p2,ClsEllipse& el)
/*Неявный вызов конструкторов по умолчанию для создания объектов sq1, sq2, elp */
{
/*Поскольку нас не устраивает те значения, которые получат объекты sq1, sq2 и elp при вызове конструкторов по умолчанию, то мы вынуждены написать следующий код */
for(int i =0 ; i <5; i++){ sq1.pt[i] = p1[i]; sq2.pt[i] = p2[i];}
elp.pt1= el.pt1; elp.pt2= el.pt2;
/*Фактически нам пришлось повторить тело конструкторов с параметрами для объектов sq1, sq2, elp*/
}
Ещё один вариант списка инициализации можно увидеть в конструкторе класса Heir
Heir(POINT *p):Square(p),ClsEllipse(){/*cout<<"\t Heir()";*/ }
Как мы видим, в нём вызываются конструктор и конструктор по умолчанию для заполнения полей данных, унаследованных Heir от классов Square, ClsEllipse. Причём конструктор по умолчанию для ClsEllipse() явно можно было и не вызывать, он вызвался бы и не явно самостоятельно.
С помощью строк программы с 2.6 по 2.9 объекты sq1 и elp плавно передвигаются по экрану монитора. Это организовано с помощью цикла for, в котором последовательно вызываются функции show() для отображения объектов, sleep(24) для задержки выполнения программы на 24мс, hide() для стирания объектов с экрана и move(2,2) для изменения координат объектов, и затем снова вывод на экран на следующей итерации цикла for.
В строке 2.10 мы входим из блока, следовательно, локальные объекты sq1 и elp должны быть уничтожены. Эту операцию выполняют деструкторы. Причём при использовании наследования последовательность вызовов деструкторов противоположна последовательности вызова конструкторов, т.е. сначала вызывается деструктор для производного класса, а потом для его базового класса и аналогично ниже по цепочке иерархии.
Таким образом, для нашего примера в строке 2.10 будет вызван сначала деструктор ~Square(), а затем ~Figure() для уничтожения объекта sq1. Аналогично для уничтожения объекта elp будут вызваны деструкторы ~ClsEllipse(), а затем ~Figure().
В любом классе могут быть в качестве компонентов определены другие классы. В этих классах будут свои деструкторы, которые при уничтожении объекта охватывающего (внешнего) класса выполняются после деструктора охватывающего класса. Например, в строке 2.11 создаётся объект obj типа MyObject, процесс создания которого с помощью вызовов конструкторов мы уже описали. При выходе из тела функции main, локальный объект obj должен быть уничтожен. Для этого неявно вызываются деструктор ~MyObject(), а затем для уничтожения агрегированных в него по значению объектов sq1, sq2 деструкторы ~Square()->~Figure(),~Square()->~Figure(). Для ссылки elp деструктор не вызывается, так как ссылка объектом не является.
Вызовы деструкторов для объектов класса и для базовых классов выполняются неявно и не требуют никаких действий программиста. Однако вызов деструктора того класса, объект которого уничтожается в соответствии с логикой выполнения программы, может быть явным. Например, класс MyObject в своём деструкторе мог бы через ссылку elp на класс ClsEllipse, явно уничтожить объект этого класса. Это выглядело бы так: ~MyObject(){elp.~ ClsEllipse();} Обычно этого не делают, так как, если это сделать, то встаёт вопрос, зачем связывали классы MyObject с ClsEllipse через ссылку elp, а не по значению.
Ещё один интересным фрагментом программы, является вызов функции ShowMyObject(obj); в строке 2.12. Поскольку из определения функции (строка 1.33) видно, что данные в функцию передаются по значению, то, следовательно, для объекта obj будет вызван конструктор копии, который в свою очередь вызовет конструкторы копии для агрегированных по значению в класс MyObject объектов sq1, sq2 (строка 1.20) класса Square. Для агрегированного с MyObject по ссылке elp класса ClsEllipse (строка 1.21) конструктор копии естественно вызываться не будет.
Дата добавления: 2020-12-11; просмотров: 399;