Добавил:
Upload Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Методичка ООП.doc
Скачиваний:
22
Добавлен:
08.11.2018
Размер:
1.4 Mб
Скачать

Свойства

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

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

Для примера определим классы, входящие в иерархию наследования, показанную на рис. 8.

Interface type TPerson = class(TObject) //организуем свойства фамилия и номер паспорта private FName: string; FPassportNumber: string; procedure SetName(value: string); procedure SetPassportNumber(value: string); public property Name: string read FName write SetName; property PassportNumber: string read FPassportNumber write SetPassportNumber; ... end;

TStudent = class(TPerson) //организуем свойства номер зачетки, группа, специальность //свойства фамилия и номер паспорта унаследованы private FTestBookNumber: integer; FGroup: string; FSubject: string; procedure SetTestBookNumber value: integer); procedure SetGroup (value: string); procedure SetSubject value: string); public property TestBookNumber: integer read FTestBookNumber write SetTestBookNumber; property Group: string read FGroup write SetGroup; property Subject: string read FSubject write SetSubject; ... end;

TWorker = class(TPerson) //организуем свойство табельный номер //свойства фамилия и номер паспорта унаследованы private FTableNumber: integer; procedure SetTableNumber(value: integer); public property TableNumber: integer read FTableNumber write SetTableNumber; ... end;

TEmployee = class(TWorker) //организуем свойство должность //свойства фамилия, номер паспорта и табельный номер //унаследованы private FAppointment: string; procedure SetAppointment(value: string); public property Appointment: string read FAppointment write SetAppointment; ... end;

TTeacher = class(TWorker) //организуем свойства кафедра и преподаваемые дисциплины //свойства фамилия, номер паспорта и табельный номер //унаследованы private FChair: string; FSubjects: TStrings3; procedure SetChair(value: string); procedure SetSubjects(value: TStrings); public property Chair: string read Fchair write SetChair; property Subjects: TStrings read FSubjects write SetSubjects; ... end;

В этом примере каждый класс наследует общее от своего предка и определяет собственное.

Конструкторы и деструкторы класса-предка

Вернемся к вопросу создания и уничтожения объектов. Известно, что в конструкторе должна быть выполнена инициализация полей объекта. Но, конструктор класса-потомка знает только про поля потомка. То есть для инициализации полей предка в конструкторе потомка должен быть вызван конструктор предка. При этом также как и для обычного перекрытого метода используется директива inherited:

Constructor TNewObject.Create; begin //вызываем конструктор предка inherited Cretae; ... end;

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

Таким образом, первым оператором в конструкторе обычно является вызов конструктора его предка. Конструктор класса-предка, инициализирует поля, объявленные в классе-предке, конструктор класса-потомка, соответственно, инициализирует поля, объявленные в классе-потомке. Конструктор класса-предка, возможно, в свою очередь вызывает конструктор своего класса-предка и т.д., но разработчику класса-потомка знать об этом не обязательно. Например:

interface

type

TSampleOb1 = class(TObject) private F1: Extended; public //конструктор по умолчанию constructor Create; overload; //конструктор инициализации constructor Create(Value: Extended); overload; ... end;

TSampleOb2 = class(TSampleOb1) private F2: integer; public //конструктор по умолчанию constructor Create; overload; //конструктор инициализации constructor Create(Value1: extended; Value2: integer); overload; ... end;

...

implementation

//конструктор по умолчанию класса-предка инициализирует поля //класса-предка значениями по умолчанию constructor TSampleOb1.Create; begin inherited Create; //вызывается конструктор TObject F1 := 0.1; end;

//конструктор инициализации класса-предка инициализирует поля //класса-предка переданными значениями constructor TSampleOb1.Create(Value: extended); begin inherited Create; //вызывается конструктор TObject F1 := Value; end;

//конструктор по умолчанию класса-потомка вызывает конструктор //предка и инициализирует поля класса-потомка значениями //по умолчанию constructor TSampleOb2.Create; begin inherited Create; F2 := 100; end;

//конструктор инициализации класса-потомка вызывает конструктор //предка предавая ему соответствующие параметры и //инициализирует поля класса-потомка переданными значениями constructor TSampleOb2.Create(Value1: extended; Value2: integer); begin inherited Create(Value1); F2 := Value2; end;

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

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

Например:

interface

type

TSampleOb1 = class(TObject) ... destructor Destroy; ... end;

TSampleOb2 = class(TSampleOb1) ... destructor Destroy; ... end;

implementation

destructor TSampleOb1.Destroy; begin ...; //полезные действия inherited Destroy; //вызов деструктора TObject end;

destructor TSampleOb2.Destroy; begin ...; //полезные действия inherited Destroy; //вызов деструктора предка end;

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

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

interface

type

TSampleOb1 = class(TObject) private F1: Extended; public constructor Create; overload; constructor Create(Value: extended); overload; destructor Destroy; ... end;

TSampleOb2 = class(TSampleOb1) private F2: integer; public constructor Create; overload; constructor Create(Value1: extended); overload; constructor Create(Value1: extended; Value2: integer); overload; destructor Destroy; ... end;

...

implementation

constructor TSampleOb1.Create; begin inherited; //вызывается конструктор TObject //имя конструктора может быть опущено F1 := 0.1; end;

constructor TSampleOb1.Create(Value: extended); begin inherited Create; //вызывается конструктор TObject //имя конструктора опускать нельзя, //так как не совпадают параметры F1 := Value; end;

destructor TSampleOb1.Destroy; begin ... inherited; //имя деструктора может быть опущено end;

constructor TSampleOb2.Create; begin inherited; //вызывается конструктор TSample1.Create //имя конструктора может быть опущено F2 := 100; end;

constructor TSampleOb2.Create(Value1: extended); begin inherited; //вызывается конструктор TSample1.Create(Value: extended) //имя конструктора может быть опущено F2 := 100; end;

constructor TSampleOb2.Create(Value1: extended; Value2: integer); begin inherited Create(Value1); //имя конструктора опускать нельзя, //так как не совпадают параметры F2 := Value2; end;

destructor TsampleOb2.Destroy; begin ... inherited; //имя деструктора может быть опущено end;

Будьте внимательны! Следующая запись:

constructor TSomeObject.Create(value: TSomeType); begin inherited; end;

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

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

    1. Множественное наследование в C++

Язык C++ допускает множественное наследование, то есть класс-потомок может наследовать как от одного, так и от нескольких базовых классов. При объявлении производного класса список базовых классов, разделенных запятой, указывается через двоеточие справа от имени класса:

class CBase1 { ... };

class CBase2 { ... };

class CDerived : CBase1, CBase2 { ... };

При этом производный класс наследует все поля и методы своих предков и может добавлять собственные. Одноименные поля потомка статически перекрывают поля предка. Функции-элементы могут перекрываться статически и как виртуальные. Виртуальные функции-элементы будут рассмотрены позднее. Класс, объявленный как union, не может быть использован в качестве базового класса.

В методах класса-потомка можно свободно использовать не перекрытые поля и методы предка, если это позволяет их область видимости. Для обращения к перекрытым элементам базовых классов в функциях-элементах производного класса используется оператор доступа к области действия «::» перед которым указывается имя нужного базового класса. Например:

//базовый класс class CBase { private: int a; public: int GetA(); void SetA(int x); };

//производный класс class CDerived: CBase { public: void SetA(int x);// статическое перекрытие //метод GetA() не перекрыт };

int CBase::GetA() { return a; }; void CBase::SetA(int x) { a = x; };

void CDerived::SetA(double x) { if (x > GetA()) //вызов не перекрытого метода CBase::GetA CBase::SetA(x); //вызов перекрытого метода CBase::SetA };

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

Конфликт имен возникает, когда в двух или большем числе классов-предков определено поле или метод с одинаковым именем. И при использовании этого имени в методах класса-потомка непонятно, метод или поле какого из предков имеется в виду. В С++ этот конфликт решается явно – при использовании таких имен в методах потомка надо указать имя нужного класса-предка. В ином случае компилятор выдаст ошибку. Например:

//базовые классы class CBase1 { ... public: void Method1(int); ... };

class CBase2 { ... public: void Method1(double); ... };

//производный класс class CDerived: CBase1, CBase2 { ... public: void SomeMethod(void); ... };

void CDerived::SomeMethod(void) { //Method1(1); такой вызов не допустим, т.к. не понятно, //метод, определенный в каком из предков имеется в виду CBase1::Method1(1); //вызывается метод класса CBase1 CBase2::Method1(0.0001);//вызывается метод класса CBase2 }

В языке C++ нельзя указать какой-либо либо класс в списке классов-предков более чем один раз:

class CBase { ...};

class CDerived: CBase, CBase { ... }; // недопустимо

То есть один класс не может непосредственно наследовать другому классу несколько раз. Однако один класс может наследовать другому классу несколько раз опосредовано. Например, когда класс наследует двум классам, а они порознь наследуют одному и тому же четвертому (рис. 9).

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

//базовый класс class CBase { ... };

//промежуточные потомки class CMediator1: public CBase //назначение спецификатора public будет объяснено позднее //в разделе «Области видимости в C++» { ... };

class CMediator2: public CBase { ... };

//производный класс class CDerived: public CMediator1, public CMediator2 //допустимо //в класс CDerived будет входить две копии класса CBase { ... };

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

//базовые классы class CBase { ... };

//промежуточные потомки class CMediator1: virtual public CBase { ... };

class CMediator2: virtual public CBase { ... };

//производный класс class CDerived: public CMediator1, public CMediator2 //в класс CDerived будет входить только одна копия класса CBase { ... };

При множественном наследовании часто используется прием создания классов-примесей. Идея примесей – можно комбинировать (смешивать) небольшие классы, чтобы строить классы с более сложным поведением. Синтаксически примесь ничем не отличается от обычного класса, но их использование различно. Примесь не предназначена для порождения самостоятельно используемых объектов – она смешивается с другими классами. Классы, сконструированные целиком из примесей, называют агрегатными.

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

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

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

//базовые классы class CBase1 { int x; public: CBase1(int i); ... };

class CBase2 { int y; public: CBase2(int i); ... };

//производный класс class CTop: public CBase1, public CBase2 { int a,b; public: CTop(int i, int j); };

CTop::CTop(int i, int j): CBase1(i*5), CBase2(j+i), a(i) //список инициализаторов – вызываются конструкторы //инициализации базовых классов CBase1 и CBase2, им передаются //значения параметров, преобразованные из полученных //конструктором CTop; также инициализируется поле a { b = j; //поле b инициализируется в теле конструктора };

CBase1::CBase1(int i) { x = i; };

CBase2::CBase2(int i) { y = i; };

Базовые классы инициализируются перед конструированием любых элементов производных классов в последовательности объявления, независимо от расположения в списке инициализации. Например:

class CDerived : CBase { int a; public: CDerived(int i): a(i*10), CBase(a){...}; //значение а к моменту вызова CBase(a) не определено ... };

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