МетодРисование_квадрата интерфейс 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);}