МетодРисование_квадрата интерфейс 4 страница

<вид наследования><имя базового класса n>{...};

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

<имя конструктора производного класса>(<список аргументов>):

<имя конструктора базового класса 1>(<список аргументов>),

<имя конструктора базового класса n>(<список аргументов>) {<тело конструктора производного класса>}

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

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

Пример 3.17Наследование от двух базовых классов. В данном примере класс twonasl наследуется от классов integral и rational и включает объект класса fixed (рис. 3.3).

Объект класса twonasl состоит из следующих полей:

• поля numfx, унаследованного от класса integral (описанного public, наследованного private, следовательно, private) и инициализированного случайным числом в диапазоне от -50 до 49;

• полей num и с, унаследованных от класса rational (описанных public, наследованных public, следовательно, public) и инициализированных числами 20 и символом «R», причем инициализация поля с в конструкторе класса rational не предусмотрена, поэтому она выполняется в теле конструктора класса twonasl;

Рис. 3.3. Диаграмма классов с наследованием от двух классов
и объектным полем

• объекта класса fixed с именем numfix, включающего внутреннее поле numfx, которое недоступно в классе twonasl и инициализируется числом 50.

 

#include <iostream.h>

#include <conio.h>

#include <stdlib.h>

class fixed

{ int numfx;

public: fixed(int v):numfx(v) {cout<<" вызов конструктора fixed\n";}

};

class integral

{ public: int numfx;

integral(int va):numfx(va) {cout<<" вызов конструктора integ\n"; }

};

class rational

{public: char c; int num;

rational (int vn):num(vn) { cout<< " вызов конструктора rational\n ";}

};

class twonasl:private integral, // наследование в защищенном режиме

public rational // наследование в открытом режиме

{ private: fixed numfix; // объектное поле

public:

twonasl (int nfx,int nm,char vc,int pnfx):

integral(nfx), rational(nm), numfix(pnfx)

{ cout<< " вызов конструктора twonasl\n "; c=vc; } /* инициализация поля базового класса в теле конструктора производного класса */

int get(void) // метод, возвращающий значение внутреннего поля,

{ return numfx;} // унаследованного от класса integral по private

};

void main()

{ clrscr(); randomize;

int r=random(100)-50; twonasl aa(r,20, 'R',50);

cout<<aa.get()<<" "<<aa.c<<" "<<aa.num<<endl; getch();}

 

В результате выполнения программы мы получаем следующую цепочку обращений к конструкторам:

вызов конструктора integ

вызов конструктора rational

вызов конструктора fixed

вызов конструктора twonasl

и содержимое полей numfx (класса integral), с и num, доступных в классе twonasl:

-49 R 20

Виртуальное наследование. При множественном наследовании базовый класс не может быть указан в производном классе более одного раза. Однако возможна ситуация, когда производный класс при наследовании от потомков одного базового класса многократно наследует одни и те же компоненты базового класса (рис. 3.4). Иными словами, производный класс будет содержать несколько копий полей одного базового класса.

Рис. 3.4. Иерархия с многократным наследованием

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

class <имя производного класса>:

virtual <вид наследования><имя базового класса>{...};

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

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

Пример 3.18 Виртуальное наследование. Реализуем иерархию классов, представленную на рис. 3.5. Класс derived наследуется от двух наследников класса fixed. Чтобы исключить удваивание полей, описанных в классе fixed, необходимо использовать виртуальное наследование.

 

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

#tinclude <iostream.h>

class fixed

{protected:

int Fix;

public: fixed(void) // конструктор без параметров

{cout<<"вызов конструктораfixed\n";}

fixed(int fix):Fix(fix) /* конструктор с параметром */

{cout<< "вызов конструктора fixed int\n ";}

class derived_1: virtual public fixed // виртуальное наследование

{public: int One;

derived_1 (void) { cout<< "вызов конструктора l\n";}

};

class derived_2: virtual private fixed // виртуальное наследование

{public: int Two;

derived_2(void) { cout<< " вызов конструктора 2\n";}

};

class derived: public derived_1, public derived_2 /* объявление производного класса - непрямого потомка */

{ public:

derived(void){ cout<<" вызов конструктора derived \n";}

derived(int fix) :fixed(fix)

{ cout<<" вызов конструктора derived (int) \n";}

void Out( ) { cout<< " поле Fix = "<< Fix;}

};

main()

{ derived Var(10);

derived Var(10);

Var.Out(); }

В результате работы программы получаем

вызов конструктора fixed int

вызов конструктора 1

вызов конструктора 2

вызов конструктора derived.int

поле Fix=10

В том случае, если бы наследование не было виртуальным, поле Fix было бы включено в объект класса derived дважды:

derived l::Fix и derived 2::Fix.


4.

5.

6. ОБЪЕКТНАЯ МОДЕЛЬ C++ BUILDER

Объектная модель C+ + Builder несколько отличается от первоначальной объектной модели C++, описанной в гл. 3. Прежде всего эта среда базируется на современной усложненной модели, используемой в последних версиях языка C++, т. е. поддерживает пространства имен, исключения и специальные средства преобразования типов.

Кроме того, C++ Builder использует библиотеку классов VCL, разработанную для среды Delphi и основывающуюся на объектной модели Delphi Pascal. Следовательно, при создании C++ Builder необходимо было согласовать конструкции C++ и Pascal Delphi, а также механизмы реализации этих конструкций, обращая особое внимание на те возможности, которые есть в Delphi Pascal, но отсутствуют в C++. В результате в объектную модель C++ были добавлены:

- возможность создания специальных секций для описания опубликованных элементов класса и элементов, реализующих OLE-механизм;

- средства объявления свойств;

- определения специальных классов, моделирующих стандартные типы данных Delphi Pascal (множества, строки и т. д.);

- возможность определения указателей на методы (__closure);

- специальный модификатор (__declspec), посредством которого реализуются, например, динамические методы Delphi Pascal.

Различие объектных моделей и их реализаций в C++ и Delphi Pascal не позволяет обеспечить полную совместимость классов, разработанных в этих языках. Поэтому разработчики C++Builder обеспечили возможность создания двух типов классов: обычные классы C++ с расширенными возможностями и VCL-совместимые классы - для работы с библиотекой визуальных компонент VCL.

6.1. Расширение базовой объектной модели языка C++

Как уже говорилось выше, объектная модель C++ Builder включает ряд новых (по сравнению с Borland C++ 3.1) средств. Это средства:

- определения пространств имен;

- описания указателей на методы;

- определения и переопределения типа объекта;

- описания свойств.

 

Пространство имен.Большинство сколько-нибудь сложных приложений состоит более чем из одного исходного файла. При этом возникает вероятность дублирования имен, что препятствует сборке программы из частей. Для снятия проблемы дублирования имен в C++ был введен механизм логического разделения области глобальных имен программы, который получил название пространства имен.

Пространство имен описывается следующим образом:

namespace [<имя>] {<объявления и определения>}

Имя пространства имен должно быть уникальным или может быть опущено.

Примечание. Если имя пространства опущено, то считается, что определено неименованное пространство имен, локальное внутри единицы трансляции. Для доступа к его ресурсам используется внутреннее имя $$$.

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

Например:

namespace ALPHA // ALPHA - имя пространства имен

{ long double LD; // объявление переменной

float f(float у) { return у; } // описание функции

}

 

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

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

Доступ к элементам других пространств имен может осуществляться тремя способами:

- с использованием имени области в качестве квалификатора доступа, например:

ALPHA:: LD ALPHA::f()

- с использованием объявления using, которое указывает, что некоторое имя доступно в другом пространстве имен:

namespace BETA {

using ALPHA::LD; /* имя ALPHA::LD доступно в BETA*/}

- с использованием директивы using, которая объявляет все имена одного пространства имен доступными в другом пространстве:

namespace BETA {

using ALPHA; /* все имена ALPHA доступны в BETA*/}

 

Каждое объявление класса в C++ образует пространство имен, куда входят все общедоступные компоненты класса. Для доступа к ним принято использовать квалификаторы доступа <имя класса>::.

Директиву using внутри класса использовать не разрешается. Применение же объявления using допустимо и может оказаться весьма полезным.

Пример 6.1. Переопределение метода потомка перегруженным методом базового класса (с использованием объявления using). Описание базового и производного классов в соответствии с правилами модульного программирования в C++ выполним в файле-заголовке Object.h:

#ifndef ObjectH

#define ObjectH

class A

{ public:

void func(char ch,TEdit *Edit);

};

class В : public A

{ public:

void func (char *str,TEdit *Edit);

using A::func; // перегрузить B::func

};

#endif

 

Реализацию методов классов поместим в файле Object.cpp:

 

#include <vcl.h>

#pragma hdrstop

#include "Object.h"

void A::func(char ch, TEdit *Edit) // метод базового класса

{Edit->Text=AnsiString("символ"); }

void B::func(char *str, TEdit *Edit) // метод производного класса

{ Edit->Text=AnsiString("cmpoка");}

#pragma package(smart_init)

 

Вызов нужного метода, как это принято для перегруженных функций, определяется типом фактического параметра:

B b;

b.func('c', Edit); // вызов A::func(), так как параметр - символ

b.func("c",Edit); //вызов В::func(), так как параметр – строка

 

Указатель на метод. Делегирование.В стандартном C++ существует возможность объявления указателей на функции. Аналогично можно объявлять указатели на методы как компонентные функции определенного класса. Такое объявление должно содержать квалификатор доступа вида <имя класса>::. Вызов метода по адресу осуществляется с указанием объекта, для которого вызывается метод.

Например, если описан класс base:

class base

{ public: void func(int x, TEdit *Edit);

};

то можно определить указатель на метод этого класса и обратиться к этому методу, используя указатель:

base A;

void (base::*bptr)(int,TEdit *); // указатель на метод класса

bptr = & base::func; // инициализация указателя

(A.*bptr)(l,ResultEdit); // вызов метода через указатель

 

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

class derived: public base

{ public: void new _func( int i, TEdit *Edit);

};

…..

bptr =&derived:.new_func; // ошибка при компиляции !!!

 

С помощью описателя __closure в C++ Builder объявляется специальный тип указателя -указатель на метод. Такой указатель, помимо собственно адреса функции, содержит адрес объекта, для которого вызывается метод. В отличие от обычных указателей на функции, указатель на метод не приписан к определенному классу и потому может быть инициализирован адреса ми методов различных классов.

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

<тип> (__closure* <идентификатор> ) (<список параметров>);

Например, для классов, описанных выше, можно выполнить следующие объявления:

base A; derived В;

void (__closure *bptr)(int,TEdit*); // указатель на метод

bptr = &A.func; /* инициализация указателя адресом метода базового класса и адресом объекта А */

bptr(l,ResultEdit); // вызов метода по указателю

bptr = &B.new_func; /* инициализация указателя адресом метода производного класса и адресом объекта В*/

bptr(l,ResultEdit); // вызов метода по указателю

 

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

Например:

class base

{ public: virtualvoid func_poly(TEdit *Edit);

};

class derived: public base

{ public: virtualvoid func_poly(TEdit *Edit);

};

….

base *pB;

pB=new derived;

void (__closure*bptr)(TEdit *); // указатель на метод

bptr = &pB->func_poly; /* инициализация указателя адресом полиморфного метода и адресом объекта производного класса, нужный аспект определяется во время выполнения программы */

bptr(ResultEdit); // вызов метода по указателю

 

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

Пример 6.2. Делегирование методов (графический редактор «Окружности и квадраты»).Делегирование методов проиллюстрируем на примере разработки графического редактора «Окружности и квадраты», рассмотренного в § 5.5 (см. пример 5.6). Сначала опишем класс TFigure в файле Object.h:

 

#iifhdef FigureH

#define FigureH

typedef void (__closure*type_pMetod) (TImage *);

class TFigure

{ private: int x,y,r;

type_pMetod fDraw;

public:

_property type _pMetod Draw = {read=fDraw, write =fDraw};

TFigure(int, int, int, TImage *, TRadioGroup * );

void DrawCircle(TImage *);

void DrawSquare (TImage *);

void Clear(TImage *);

};

#endif

 

Описание методов класса поместим в файл Object.cpp:

 

#include <vcl.h>

#pragma hdrstop

#include "Figure.h"

TFigure::TFigure(int X, int Y, int R, TImage *Image, TRadioGroup *RadioGroup)

{ x=X; y=Y; r=R;

switch (RadioGroup->ItemIndex) // определить метод рисования

{case 0: Draw=DrawCircle;

break;

case 1: Draw=DrawSquare;}

Draw(Image); // нарисовать фигуру

}

void TFigure::DrawCircle(TImage *Image)

{Image->Canvas->Ellipse(x-r, y-r, x+r, y+r); }

void TFigure: .DrawSquare (TImage *Image)

{Image->Canvas->Rectangle(x-r, y-r, x+r, y+r);}

void TFigure::Clear(TImage *Image)

{ Image->Canvas->Pen->Color=clWhite;

Draw(Image); // вызов метода по адресу, указанному в свойстве

Image->Canvas->Pen->Color=clBlack; }

#pragma package (smart_init)

Объекты класса Figure будем создавать при нажатии клавиши мыши:

 

void __fastcall TMainForm::ImageMouseDown(TObject *Sender,

TMouseButton Button, TShiftState Shift, int X, int Y)

{ if (Figure != NULL) delete Figure; // если объект создан, то уничтожить

Figure=new TFigure(X,Y, 10,Image,RadioGroup); // создать объект

}

 

При переключении типа фигуры будем стирать уже нарисованную фигуру и рисовать фигуру другого типа:

 

void __fastcall TMainForm::RadioGroupClick(TObject *Sender)

{ if (Figure != NULL) / если фигура нарисована, то

{ Figure- > Clear (Image); // стереть ее

switch (RadioGroup->ItemIndex) // делегировать метод

{ case 0: Figure->Draw=Figure->DrawCircle; break;

case 1: Figure->Draw=Figure->DrawSquare; }

Figure-> Draw (Image); } II нарисовать фигуру другого типа

}

 

Операторы определения и переопределения типа объекта.Эти операторы были включены в C++, чтобы обезопасить операцию переопределения (приведения) типов, которая программировалась следующим образом:

(<имя типа>)<имя переменной>

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

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

class A { public:

void func(char ch); };

class В : public A

{ public: void func (char *str); };

…..

B b;

b.func("c"); //вызвать B::func()

(A)b.func('c'); // вызвать A::func(); (A)b - восходящее приведение типа

 

При выполнении нисходящего приведения типов необходима проверка, гак как никакой гарантии, что указатель ссылается на адрес объекта именно данного производного класса, у нас нет. Используется же это преобразование при работе с полиморфными объектами постоянно, в связи с тем, что это единственный способ обеспечить видимость полей производного класса при работе с объектом через указатель на базовый класс (см. § 1.6).

В последних версиях C++ приведение типов выполняется с использованием специальных операторов.

Рассмотрим эти операторы.

Динамическое приведение типа: dynamic_cast <T>(t).

Операнды: Т - указатель или ссылка на класс или void*, t - выражения типа указателя, причем оба операнда либо указатели, либо ссылки.

Приведение типа осуществляется во время выполнения программы. Предусмотрена проверка возможности преобразования, использующая RTTI (информацию о типе времени выполнения), которая строится в C++ только для полиморфных объектов.

Применяется для нисходящего приведения типов полиморфных объектов, например:

class A {virtual~А(){}};/*класс обязательно должен включать виртуальный метод, так как для выполнения приведения требуется RTTI*/

class В: public A

{ virtual-В(){}}:

void func(A& a) /* функция, работающая с полиморфным объектом*/

{ В& b=dynamic_cast<B&>(a); II нисходящее приведение типов

}

void somefunc()

{ B b:

func(b): // вызов функции с полиморфным объектом

}

 

Если вызов dynamic_cast осуществляется в условной конструкции, то ошибка преобразования, обнаруженная на этапе выполнения программы, приводит к установке значения указателя равным NULL (0), в результате чего активизируется ветвь «иначе». Например:

if (Derived* q=dynamic_cast<Derived* p>)

{<если преобразование успешно, то ...>}

else {<если преобразование неуспешно, то ...>}

 

В данном случае осуществляется преобразование указателя на объекты базового класса в указатель на объекты производного класса с проверкой правильности на этапе выполнения программы (с использованием RTTI). Если преобразование невозможно, то оператор возвращает NULL, и устанавливается q=NULL, в результате чего управление передается на ветвь else.

Если вызов осуществляется в операторе присваивания, то при неудаче генерируется исключение bad_cast. Например:

Derived* q=dynamic_cast<Derived* p>;

 

Статическое приведение типа: static_cast<T>(t)

Операнды: Т - указатель, ссылка, арифметический тип или перечисление; t - аргумент типа, соответствующего Т. Оба операнда должны быть определены на этапе компиляции. Операция выполняется на этапе компиляции без проверки правильности преобразования.

Может преобразовывать:

1) целое число в целое другого типа или в вещественное и обратно:

int i; float f=static_cast<float>(i); /* осуществляет преобразование без проверки на этапе компиляции программы */

2) указатели различных типов, например:

int *q=static_cast<int>(malloc(100)); /* осуществляет преобразование без проверки на этапе компиляции программы */

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

 

class A {...}; // класс не включает виртуальных функций

class В: public A {}; // не используется виртуальное наследование

void somefunc()

{А а; В b;

В& ab=static_cast<B&>(a); // нисходящее приведение

А& ba=static_cast<A&>(b); // восходящее приведение

}

Примечание. Кроме описанных выше были добавлены еще два оператора приведения, которые напрямую с объектами обычно не используются. Это оператор const_cast<T>(t) -для отмены действия модификаторов const или volatile и оператор reinterpret<T>(t) - для преобразований, ответственность за которые полностью ложится на программиста.

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

 

Свойства.Механизм свойств был заимствован C++ Builder из Delphi Pascal и распространен на все создаваемые классы. В Delphi Pascal свойства использовались для определения интерфейса к отдельным полям классов (см. § 5.3). Синтаксис и семантика свойств C++ Builder полностью аналогичны синтаксису и семантике свойств Delphi Pascal.

Так же, как в Delphi Pascal, различают: простые свойства, свойства-массивы и индексируемые свойства.

 

Простые свойства определяются следующим образом:

__property <тип свойства> <имя> = {<список спецификаций>};

 

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

read = <переменная или имя функции> - определяет имя поля, откуда читается значение свойства, или метода чтения, которая возвращает это значение; если данный атрибут опущен, то свойство не доступно для чтения из программы;

write = <константа или имя функции> - определяет имя поля, куда записывается значение свойства, или метода записи, используемой для записи значения в поле; если данный атрибут опущен, то значение свойства в программе менять нельзя;

stored = <константа или имя функции логического типа> - определяют, должно ли сохраняться значение свойства в файле формы, этот атрибут используется для визуальных и невизуальных компонентов;

default = <константа> или nodefault - определяет значение по умолчанию или его отсутствие.

Пример 6.3. Простые свойства (класс Целое число). Пусть требуется разработать класс для хранения целого числа. Этот класс должен обеспечивать возможность чтения и записи значения числа. Опишем доступ к полю, используемому для хранения значения, с помощью свойства Number целого типа. Поместим это описание в файле Object.h:

class TNumber

{ private:

int fNum;

int GetNum(); // метод чтения свойства

void SetNum(aNum); // метод записи свойства

public:

TNumber(int aNum); // конструктор

__property int Number={read=GetNum,write=SetNum}; // свойство

};

 

Соответственно, реализацию методов этого класса поместим в файл Object.cpp:

 

#include "Object.h"

#pragma package (smart_init)

TNumber::TNumber(int aNum) { SetNum(aNum);}