3. ОБЪЕКТНО-ОРИЕНТИРОВАННОЕ ПРОГРАММИРОВАНИЕ И C++
Объект - это абстрактная сущность, наделенная характеристиками объектов окружающего нас реального мира. Создание объектов и манипулирование ими - это вовсе не привилегия языка C++, а скорее результат методологии программирования, воплощающей в кодовых конструкциях описания объектов и операции над ними. Каждый объект программы, как и любой реальный объект, отличается собственными атрибутами и характерным поведением. Объекты можно классифицировать по разным категориям: например, мои цифровые наручные часы "Cassio" принадлежат к классу часов. Программная реализация часов входит, как стандартное приложение, в состав операционной системы вашего компьютера.
Каждый класс занимает определенное место в иерархии классов, например, все часы принадлежат классу приборов измерения времени (более высокому в иерархии), а класс часов сам включает множество производных вариаций на ту же тему. Таким образом, любой класс определяет некоторую категорию объектов, а всякий объект есть экземпляр некоторого класса.
Объектно-ориентированное программирование (ООП) — это методика, которая концентрирует основное внимание программиста на связях между объектами, а не на деталях их реализации. В этой главе основные принципы ООП (инкапсуляция, наследование, полиморфизм, создание классов и объектов) интерпретируются и дополняются новыми понятиями и терминологией, принятыми интегрированной средой визуальной обработки C++Builder. Приводится описание расширений языка новыми возможностями (компоненты, свойства, обработчики событий) и последних дополнений стандарта ANSI C++ (шаблоны, пространства имен, явные и непостоянные объявления, идентификация типов при выполнении программы, исключения).
Глава носит обзорный характер, она призвана познакомить читателя со специальной терминологией ООП, к которой автор вынужден прибегать на протяжении всей книги. Это вызвано тем, что C++Builder является типичной системой ООП и претендует на кульминационную роль в истории его развития.
3.1 Инкапсуляция
Инкапсуляция есть объединение в едином объекте данных и кодов, оперирующих с этими данными. В терминологии ООП данные называются членами данных (data members) объекта, а коды - объектными методами или функциями-членами (methods, member functions).
Инкапсуляция позволяет в максимальной степени изолировать объект от внешнего окружения. Она существенно повышает надежность разрабатываемых программ, т.к. локализованные в объекте функции обмениваются с программой сравнительно небольшими объемами данных, причем количество и тип этих данных обычно тщательно контролируются. В результате замена или модификация функций и данных, инкапсулированных в объект, как правило, не влечет за собой плохо прослеживаемых последствий для программы в целом (в целях повышения защищенности программ в ООП почти не используются глобальные переменные).
Другим немаловажным следствием инкапсуляции является легкость обмена объектами, переноса их из одной программы в другую. Простота и доступность принципа инкапсуляции ООП стимулирует программистов к расширению Библиотеки Визуальных Компонент, входящей в состав C++Builder.
3.2 Классы, компоненты и объекты
Класс не имеет физической сущности, его ближайшей аналогией является объявление структуры. Память выделяется только тогда, когда класс используется для создания объекта. Этот процесс также называется созданием экземпляра класса (class instance).
Любой объект языка C++ имеет одинаковые атрибуты и функциональность с другими объектами того же класса. За создание своих классов и поведение объектов этих классов полную ответственность несет сам программист. Работая в некоторой среде, программист получает доступ к обширным библиотекам стандартных классов (например, к Библиотеке Визуальных Компонент C++Builder).
Обычно, объект находится в некотором уникальном состоянии, определяемом текущими значениями его атрибутов. Функциональность объектного класса определяется возможными операциями над экземпляром этого класса.
Определение класса в языке C++ содержит инкапсуляцию членов данных и методов, оперирующих с членами данных и определяющих поведение объекта. Возвращаясь к нашему примеру, отметим, что жидкокристаллический дисплей часов "Casio" представляет член данных этого объекта, а кнопки управления - объектные методы. Нажимая кнопки часов, можно оперировать с установками времени на дисплее, т.е. следуя терминологии ООП, методы модифицируют состояние объекта путем изменения членов данных.
C++Builder вводит понятие компонент (components) - специальных классов, свойства которых представляют атрибуты объектов, а их методы реализуют операции над соответствующими экземплярами компонентных классов. Понятие метод обычно используется в контексте компонентных классов и внешне не отличается от термина функция-член обычного класса. C++Builder позволяет манипулировать видом и функциональным поведением компонент не только с помощью методов (как это делают функции-члены обычных классов), но и посредством свойств и событий, присущих только классам компонент. Работая в среде C++Builder, вы наверняка заметите, что манипулировать с компонентным объектом можно как на стадии проектирования приложения, так и во время его выполнения.
Свойства (properties) компонент представляют собой расширение понятия членов данных и хотя не хранят данные как таковые, однако обеспечивают доступ к членам данных объекта. C++Builder использует ключевое слово _property для объявления свойств. При помощи событий (events) компонента сообщает пользователю о том, что на нее оказано некоторое предопределенное воздействие. Основная сфера применения методов в программах, разрабатываемых в среде C++Builder -это обработчики событий (event handlers), которые реализуют реакцию программы на возникновение определенных событий. Легко заметить некоторое сходство событий и сообщений операционной системы Windows. Типичные простые события —нажатие кнопки или клавиши на клавиатуре. Компоненты инкапсулируют свои свойства, методы и события.
На первый взгляд компоненты ничем не отличаются от других объектных классов языка C++, за исключением ряда особенностей, среди которых пока отметим следующие:
• Большинство компонент представляют собой элементы управления интерфейсом с пользователем, причем некоторые обладают весьма сложным поведением.
• Все компоненты являются прямыми или косвенными потомками одного общего класса-прародителя (TComponent).
• Компоненты обычно используются непосредственно, путем манипуляции с их свойствами; они сами не могут служить базовыми классами для построения новых подклассов.
• Компоненты размещаются только в динамической памяти кучи (heap) с помощью оператора new, а не на стеке, как объекты обычных классов.
• Свойства компонент заключают в себе RTTI - идентификацию динамических типов.
• Компоненты можно добавлять к Палитре компонент и далее манипулировать с ними посредством Редактора форм интегрированной среды визуальной разработки C++Builder.
ООП интерпретирует взаимодействие с объектами как посылку запросов некоторому объекту или между объектами. Объект, принявший запрос, реагирует вызовом соответствующего метода. В отличие от других языков ООП, таких как SmallTalk, C++ не поощряет использование понятия "запрос". Запрос - это то, что делается с объектом, а метод - это то, как объект реагирует на поступивший запрос.
При ближайшем рассмотрении метод оказывается обычной функцией-членом, которая включена в определение класса. Чтобы вызвать метод, надо указать имя функции в контексте данного класса или в обработчике некоторого события.
Именно скрытая связь метода с включающим классом выделяет его из понятия простой функции. Во время выполнения метода он имеет доступ ко всем данным своего класса, хотя и не требует явной спецификации имени этого класса. Это обеспечивается передачей каждому, без исключения, методу скрытого параметра - непостоянного указателя this на экземпляр класса. При любом обращении метода к членам данных класса, компилятор генерирует специальный код, использующий указатель this.
3.3 Наследование
Одной из самых восхитительных особенностей живой природы является ее способность порождать потомство, обладающее характеристиками, сходными с характеристиками предыдущего поколения. Заимствованная у природы идея наследования решает проблему модификации поведения объектов и придает ООП исключительную силу и гибкость. Наследование позволяет, практически без ограничений, последовательно строить и расширять классы, созданные вами или кем-то еще. Начиная с самых простых классов, можно создавать производные классы по возрастающей сложности, которые не только легки в отладке, но и просты по внутренней структуре.
Последовательное проведение в жизнь принципа наследования, особенно при разработке крупных программных проектов, хорошо согласуется с техникой нисходящего структурного программирования (от общего к частному), и во многом стимулирует такой подход. При этом сложность кода программы в целом существенно сокращается. Производный класс (потомок) наследует все свойства, методы и события своего базового класса (родителя) и всех его предшественников в иерархии классов.
При наследовании базовый класс обрастает новыми атрибутами и операциями. В производном классе обычно объявляются новые члены данных, свойства и методы. При работе с объектами программист обычно подбирает наиболее подходящий класс для решения конкретной задачи и создает одного или нескольких потомков от него, которые приобретают способность делать не только то, что заложено в родителе. Дружественные функции позволяют производному классу получить доступ ко всем членам данных внешних классов.
Кроме того, производный класс может перегружать (overload) наследуемые методы в том случае, когда их работа в базовом классе не подходит потомку. Использование перегрузки в ООП всячески поощряется, хотя в прямом понимании значения этого слова перегрузок обычно избегают. Говорят, что метод перегружен, если он ассоциируется с более чем одной одноименной функцией. Обратите внимание, что механизм вызовов перегруженных методов в иерархии классов полностью отличается от вызовов переопределенных функций. Перегрузка и переопределение - это разные понятия. Виртуальные методы используются для переопределения функций базового класса.
Чтобы применить концепцию наследования, к примеру, с часами, положим, что следуя принципу наследования, фирма "Casio" решила выпустить новую модель, дополнительно способную, скажем, произносить время при двойном нажатии любой из существующих кнопок. Вместо того, чтобы проектировать заново модель говорящих часов (новый класс, в терминологии ООП), инженеры начнут с ее прототипа (произведут нового потомка базового класса, в терминологии ООП). Производный объект унаследует все атрибуты и функциональность родителя. Произносимые синтезированным голосом цифры станут новыми членами данных потомка, а объектные методы кнопок должны быть перегружены, чтобы реализовать их дополнительную функциональность. Реакцией на событие двойного нажатия кнопки станет новый метод, который реализует произнесение последовательности цифр (новых членов данных), соответствующей текущему времени. Все вышесказанное в полной мере относится к программной реализации говорящих часов.
3.4 Разработка классов
В классы разрабатываются для достижения определенных целей. Чаще всего программист начинает с нечетко очерченной идеи, которая постепенно, по мере разработки проекта, пополняется деталями. Иногда дело заканчивается несколькими классами, весьма похожими друг на друга. Чтобы избежать подобного дублирования кодов в классах, следует разбить их на две части, определив общую часть в родительском классе, а отличающиеся оставить в производных.
Объявление класса должно предшествовать его использованию. Как правило, прикладной программист пользуется готовыми базовыми классами, причем ему вовсе не обязательно разбираться во всех спецификациях и во внутренней реализации. Однако, чтобы использовать базовый класс C++, надо обязательно знать какие члены данных и методы вам доступны (а если применяется компонента C++Builder - еще и предоставляемые свойства и события).
3.4.1 Объявление базового класса
C++Builder дает вам возможность объявить базовый класс, который инкапсулирует имена своих свойств, данных, методов и событий. Помимо способности выполнять свою непосредственную задачу объектные методы получают определенные привилегии доступа к значениям свойств и данных класса.
Каждое объявление внутри класса определяет привилегию доступа к именам класса в зависимости от того, в какой секции имя появляется. Каждая секция начинается с одного из ключевых слов: private, protected и public. Листинг 3.1 иллюстрирует обобщенный синтаксис объявления базового класса.
class className
private:
<приватные члены данных> <приватные конструкторы> <приватные методы>
protected:
<защищенные члены данных> <защищенные конструкторы> <защищенные методы>
public:
<общедоступные свойства> <общедоступные члены данных> “збщедоступные конструкторы> <общедоступный деструктор> общедоступные методы>
Листинг 3.1. Объявление базового класса.
Таким образом, объявление базового класса на C++ предоставляет следующие права доступа и соответствующие области видимости:
• Приватные private имена имеют наиболее ограниченный доступ, разрешенный только методам данного класса. Доступ производных классов к приватным методам базовых классов запрещен.
• Защищенные protected имена имеют доступ, разрешенный методам данного и производных от него классов.
• Общедоступные public имена имеют неограниченный доступ, разрешенный методам всех классов и их объектов.
Следующие правила применяются при образовании различных секций объявления класса:
1. Секции могут появляться в любом порядке, а их названия могут встречаться повторно.
2. Если секция не названа, компилятор считает последующие объявления имен класса приватными. Здесь проявляется отличие объявлений класса и структуры - последняя рассматривается по умолчанию как общедоступная.
3. По мере возможности не помещайте члены данных в общедоступную секцию, если только вы действительно не хотите разрешить доступ к ним отовсюду. Обычно их объявляют защищенными, чтобы разрешить доступ только методам производных классов.
4. Используйте методы для выборки, проверки и установки значений свойств и членов данных.
5. Конструкторы и деструкторы являются специальными функциями, которые не возвращают значения и имеют имя своего класса. Конструктор строит объект данного класса, а деструктор его удаляет.
6. Методы (так же как конструкторы и деструкторы), которые содержат более одной инструкции C++, рекомендуется объявлять вне класса.
Листинг 3.2 представляет попытку наполнить объявление базового класса некоторым конкретным содержанием. Отметим характерное для компонентных классов C++Builder объявление свойства Count в защищенной секции, а метода SetCount, реализующего запись в член данных FCount - в приватной секции.
class TPoint { private:
int FCount; // Приватный член данных void _fastcall SetCount(int Value);
protected:
_property int Count = // Защищенное свойство
{ read= FCount, write=SetCount };
double x; // Защищенный член данных
double у; // Защищенный член данных public:
TPoint(double xVal, double yVal); // Конструктор |
double getX(); |
double getY() ;
Листинг 3.2. Объявление базовой компоненты TPoint.
Объявления и определения методов хранятся в разных файлах (с расширениями .h и .срр, соответственно). Листинг 3.3 показывает, что когда методы определяются вне класса, их имена следует квалифицировать. Синтаксис такой квалификации метода, определяющей его область видимости, имеет следующий вид:
<имя класса>::<имя метода>
TPoint::TPoint(double xVal, double yVal)
( // Тело конструктора
void _fastcall TPoint::SetCount( int Value )
{
l
if ( Value i= FCount ) // Новое значение члена данных? {
FCount = Value; // Запись нового значения Update(); // Вызов метода Update } } double TPoint::getX()
// Тело метода getX, квалифицированного в классе^TPoint
}
Листинг 3.3. Определения конструктора и методов вне класса.
После того, как вы объявили класс, его имя можно использовать как идентификатор типа при объявлении объекта этого класса (например,
TPoint* MyPoint;).
3.4.2 Конструкторы и деструкторы
Как следует из названий, конструктор - это метод, который строит в памяти объект данного класса, а деструктор - это метод, который его удаляет. Конструкторы и деструкторы отличаются от других объектных методов следующими особенностями:
• Имеют имя, идентичное имени своего класса.
• Не имеют возвращаемого значения.
• Не могут наследоваться, хотя производный класс может вызывать конструкторы и деструкторы базового класса.
• Автоматически генерируются компилятором как public, если не были вами объявлены иначе.
• Автоматически вызываются компилятором, чтобы гарантировать надлежащее создание и уничтожение объектов классов.
• Могут содержать неявные обращения к операторам new и delete, если объект требует выделения и уничтожения динамической памяти.
Листинг 3.4 демонстрирует обобщенный синтаксис объявлений конструкторов и деструктора.
class className
{ public:
// Другие члены данных className(); // Конструктор по умолчанию | className(<список параметров;-);// Конструктор с аргументами | className(const className&); // Конструктор копирования
// Другие конструкторы "className(); // Деструктор
// Другие методы };
Листинг 3.4. Объявления конструкторов и деструктора.
Класс может содержать любое число конструкторов, в том числе ни одного. Конструкторы не могут быть объявлены виртуальными. Не помещайте все конструкторы в защищенной секции и старайтесь уменьшить их число, используя значения аргументов по умолчанию. Существует три вида конструкторов:
• Конструктор по умолчанию не имеет параметров. Если класс не содержит ни одного конструктора, компилятор автоматически создаст один конструктор по умолчанию, который просто выделяет память при создании объекта своего класса.
• Конструктор с аргументами позволяет инициализировать объект в момент его создания - вызывать различные функции, выделять динамическую память, присваивать переменным начальные значения и т.п.
• Конструктор копирования предназначен для создания объектов данного класса путем копирования данных из другого, уже существующего объекта этого класса. Такие конструкторы особенно целесообразны для создания копий объектов, которые моделируют динамические структуры данных. Однако, по умолчанию компилятор создает так называемые конструкторы поверхностного копирования (shallow copy constructors), которые копируют только члены данных. Поэтому если какие-то члены данных содержат указатели, сами данные не будут копироваться. Для реализации "глубокого" копирования в код конструктора надо включить соответствующие инструкции.
Класс может объявить только один общедоступный деструктор, имени которого, идентичному имени своего класса, должен предшествовать знак ~ (тильда). Деструктор не имеет параметров и может быть объявлен виртуальным. Если класс не содержит объявления деструктора, компилятор автоматически создаст его.
Обычно деструкторы выполняют операции, обратные тем, что выполняли соответствующие конструкторы. Если вы создали объект класса файл, то в деструкторе этот файл, вероятно, будет закрываться. Если конструктор класса выделяет динамическую память для массива данных (с помощью оператора new), то деструктор, вероятно, освободит выделенную память (с помощью оператора delete) и т.п.
3.4.3 Объявление производных классов
C++Builder дает возможность объявить производный класс, который наследует свойства, данные, методы и события всех своих предшественников в иерархии классов, а также может объявлять новые характеристики и перегружать некоторые из наследуемых функций. Наследуя указанные характеристики базового класса, можно заставить порожденный класс расширить, сузить, изменить, уничтожить или оставить их без изменений.
Наследование позволяет повторно использовать код базового класса в экземплярах производного класса. Концепция повторного использования имеет параллель в живой природе: ДНК можно рассматривать как базовый материал, который каждое порожденное существо повторно использует для воспроизведения своего собственного вида. <
Листинг 3.5 иллюстрирует обобщенный синтаксис объявления производного класса. Порядок перечисления секций соответствует расширений привилегий защиты и областей видимости заключенных в них элементов: от наиболее ограниченных к самым доступным.
class className : [^спецификатор доступа;”] parentClass {
<0бъявления дружественных классов>
private:
<приватные члены данных>
<приватные конструкторы>
<приватные методы> protected:
<защищенные члены данных>
<защищенные конструкторы>
<защищенные методы> public:
<общедоступные свойства>
<общедоступные члены данных>
<общедоступные конструкторы>
<общедоступный деструктор>
<общедоступные методы> _published:
•<общеизвестные свойства>
<общеизвестные члены данных>
<Объявления дружественных функций>
Листинг 3.5. Объявление производного класса.
Отметим появление новой секции с ключевым словом _published - дополнение, которое C++Builder вводит в стандарт ANSI C++ для объявления общеизвестных элементов компонентных классов. Эта секция отличается от общедоступной только тем, что компилятор генерирует информацию RTTI о свойствах, членах данных и методах объекта и C++Builder организует передачу этой информации Инспектору объектов во время исполнения программы. В главе 6 мы остановимся на этом более подробно.
Помимо способности выполнять свою непосредственную задачу объектные методы получают определенные привилегии доступа к значениям свойств и данных других классов.
Когда класс порождается от базового, все его имена в производном классе автоматически становятся приватными по умолчанию. Но его легко изменить, указав следующие спецификаторы доступа базового класса:
• private. Наследуемые (т.е. защищенные и общедоступные) имена базового класса становятся недоступными в экземплярах производного класса.
• public. Общедоступные имена базового класса и его предшественников будут доступными в экземплярах производного класса, а все защищенные останутся защищенными.
Можно порождать классы, которые расширяют возможности базового класса:
он вполне приемлем для вас, однако содержит функцию, требующую небольшой доработки. Написание заново нужной функции в производном классе является пустой тратой времени. Вместо этого надо повторно использовать код в базовом классе, расширяя его настолько, насколько это необходимо. Просто переопределите в производном классе ту функцию базового класса, которая вас не устраивает. Подобным образом можно порождать классы, которые ограничивают возможности базового класса: он вполне приемлем для вас, но делает что-то лишнее.
Рассмотрим применение методик расширения и ограничения характеристик на примере создания разновидностей объекта кнопки - типичных производных классов, получаемых при наследовании базовой компоненты TButtonControl из Библиотеки Визуальных Компонент C++Builder. Кнопки различного вида будут часто появляться в диалоговых окнах графического интерфейса ваших программ.
Рис. 3.1 показывает, что базовый класс TButtonControl способен с помощью родительского метода Draw отображать кнопку в виде двух вложенных прямоугольников: внешней рамки и внутренней закрашенной области.
Чтобы создать простую кнопку без рамки (Рис. 3.2), нужно построить производный класс SimpleButton, использовав в качестве родительского TButtonControl, и перегрузить метод Draw с ограничением его функциональности (Листинг 3.6)
class SimpleButton: public : TButtonControl { public:
SimpleButton(int x, int y) ;
void Draw() ;
-SimpleButton() { }
};
SimpleButton::SimpleButton(int x, int y) :
TButtonControl(x, y) { }
void SimpleButton::Draw()
{ ; outline->Draw();
}
Листинг 3.6. Ограничение характеристик базового класса.
Единственная задача конструктора объекта для SimpleButton - вызвать базовый класс с двумя параметрами. Именно переопределение метода SimpleButton: : Draw () предотвращает вывод обводящей рамки кнопки (как происходит в родительском классе). Естественно, чтобы изменить код метода, надо изучить его по исходному тексту базовой компоненты TButtonControl.
Теперь создадим кнопку с пояснительным названием (Рис. 3.3). Для этого нужно построить производный класс TextButton из базового TButtonControl, и перегрузить метод Draw с рас-Рис. 3.3. Кнопка с текстом, ширением его функциональности.
Листинг 3.7 показывает, что объект названия title класса Text создается конструктором TextButton, а метод
SimpleButton:-.Draw () отображает его. :
class Text { public:
Text(int x, int y, char* string) { } void Draw() { } };
class TextButton: public : TButtonControl {
Text* title;
public:
TextButton(int x, int y, char* title);
void Draw();
-TextButton() { } );
TextButton::TextButton(int x, int y, char* caption)
TButtonControl(x, y) {
title = new Text(x, y, caption);
}
void TextButton::Draw () {
TextButton::Draw() ;
title->Draw() ;
}
Листинг 3.7. Расширение характеристик базового класса.
В заключение раздела с изложением методики разработки базовых и производных классов приводится фрагмент C++ программы (Листинг 3.8), в которой объявлена иерархия классов двух простых геометрических объектов: окружности и цилиндра.
Программа составлена так, чтобы внутренние значения переменных г-радиус окружности и h-высота цилиндра определяли параметры создаваемых объектов. Базовый класс Circle моделирует окружность, а производный класс Cylinder моделирует цилиндр.
class SimpleButton: public : TButtonControl { public:
SimpleButton (int x, int y) ;
void Draw() ;
-SimpleButton() { } );
SimpleButton::SimpleButton(int x, int y) :
TButtonControl(x, y) { }
I void SimpleButton::Draw()
I { i outline->Draw();
1 )
Листинг 3.6. Ограничение характеристик базового класса.
Единственная задача конструктора объекта для SimpleButton - вызвать базовый класс с двумя параметрами. Именно переопределение метода SimpleButton: : Draw () предотвращает вывод обводящей рамки кнопки (как происходит в родительском классе). Естественно, чтобы изменить код метода, надо изучить его по исходному тексту базовой компоненты TButtonControl.
Теперь создадим кнопку с пояснительным названием (Рис. 3.3). Для этого нужно построить производный класс TextButton из базового TButtonControl, и перегрузить метод Draw с рас-Рис. 3.3. Кнопка с текстом, ширением его функциональности.
Листинг 3.7 показывает, что объект названия title класса Text создается конструктором TextButton, а метод
SimpleButton: : Draw () отображает его.
const double pi = 4 * atan(l);
class Circle { protected:
double r ;
public:
Circle (double rVal =0) : r(rVal) {}
void setRadius(double rVal) { r = rVal; }
double getRadiusO { return r; } .double Area() { return pi*r*r; }
void showData() ;
};
class Cylinder : public Circle { protected:
double h;
public:
Cylinder(double hVal = 0, double rVal = 0)
: getHeight(hVal), Circle(rVal) { }
void setHeight(double hVal) { h = hVal; }
double getHeight() { return h; }
double Area() { return 2*Circle::Area()+2*pi*r*h; }
void showData() ;
void Circle::showData() {
cout “ "Радиус окружности = " “ getRadius() “ endl
“ "Площадь круга = " “ Area О “ endl “ endl;
}
void Cylinder::showData()
{
cout “ "Радиус основания = " “ getRadius() “ endl
“ "Высота цилиндра = " “ getHeight() “ endl
“ "Площадь поверхности = " “ Area () “ endl;
}
void main()
{
Circle circle(2) ;
Cylinder cylinder(10, 1);
circle.showData () ;
cylinder.showData() ;
Листинг 3.8. Простая иерархия классов окружности и цилиндра.
Объявление класса Circle содержит единственный член данных r, конструктор и ряд методов. При создании объекта конструктор инициализирует член данных r начальным значением радиуса окружности. Отметим новый синтаксис конструктора: при вызове он может обратиться к конструктору базового класса, а также к любому члену данных, указанному после двоеточия. В нашем случае член данных r "создается" обращением к нему с параметром rVal и инициализируется нулевым значением.
Метод setRadius устанавливает, a getRadius возвращает значение члена данных г. Метод Area возвращает площадь круга. Метод showData выдает значения радиуса окружности и площади круга.
Класс Cylinder, объявленный как производный от Circle, содержит единственный член данных h, конструктор и ряд методов. Этот класс наследует член данных г для хранения радиуса основания цилиндра и методы setRadius и getRadius. При создании объекта конструктор инициализирует члены данных г и h начальными значениями. Отметим новый синтаксис конструктора: в нашем случае член данных h инициализируется значением аргумента hVal, а член данных г - вызовом конструктора базового класса Circle с аргументом rVal.
Функция setHeight устанавливает, a getHeight возвращает значение члена данных h. Circle::Area перегружает унаследованную функцию базового класса, чтобы теперь возвращать площадь поверхности цилиндра. Функция showData выдает значения радиуса основания, высоты и площади поверхности цилиндра.
Функция main создает окружность circle класса Circle с радиусом 2 и цилиндр cylinder класса Cylinder с высотой 10 и радиусом основания 1, а затем обращается к showData для вывода параметров созданных объектов:
Радиус окружности = 2 Площадь круга = 12.566
Радиус основания = 1 Высота цилиндра = 10 Площадь поверхности = 69.115
3.5 Полиморфизм
Слово полшюрфизм от греческих слов poly (много) и morphos (форма) означает множественность форм. Полиморфизм - это свойство родственных объектов (т.е. объектов, классы которых являются производными от одного родителя) вести себя по-разному в зависимости от ситуации, возникающей в момент выполнения программы. В рамках ООП программист может влиять на поведение объекта только косвенно, изменяя входящие в него методы и придавая потомкам отсутствующие у родителя специфические свойства.
Для изменения метода необходимо перегрузить его в потомке, т.е. объявить в потомке одноименный метод и реализовать в нем нужные действия. В результате в объекте-родителе и объекте-потомке будут действовать два одноименных метода, имеющие разную кодовую реализацию и, следовательно, придающие объектам разное поведение. Например, в иерархии родственных классов геометрических фигур (точка, прямая линия, квадрат, прямоугольник, окружность, эллипс и т.д.) каждый класс имеет метод Draw, который отвечает за надлежащий отклик на событие с требованием нарисовать эту фигуру.
Благодаря полиморфизму, потомки могут перегружать общие методы родителя с тем, чтобы реагировать специфическим образом на одно и то же событие.
3.5.1 Виртуальные функции
В ООП полиморфизм достигается не только описанным выше механизмом наследования и перегрузки методов родителя, но и виртуализацией, позволяющей родительским функциям обращаться к функциям потомков. Полиморфизм реализуется через архитектуру класса, но полиморфными могут быть только функции-члены.
В C++ полиморфная функция привязывается к одной из возможных одноименных функций только в момент исполнения, когда ей передается конкретный объект класса. Другими словами, вызов функции в исходном тексте программы лишь обозначается, без точного указания на то, какая именно функция вызывается. Такой процесс известен как позднее связывание. Листинг 3.9 показывает, к чему может привести не полиморфное поведение обычных функций-членов.
I class Parent { public:
double Fl(double x) { return x*x; }
double F2(double x) { return Fl(x)/2; }
class Child : public Parent { public:
double Fl(double x) { return x*x*x; } };
void main() {
Child child;
cout “ child.F2(3) “ end1;
}
Листинг 3.9. Неопределенное позднее связывание.
Класс Parent содержит функции-члены Fl и F2, причем F2 вызывает, El,. Класс Child, производный от класса Parent, наследует функцию F2, однако переопределяет функцию Fl. Вместо ожидаемого результата 13.5 программа выдаст значение 4.5. Дело в том, что компилятор оттранслирует выражение child. F2 (3) в обращение к унаследованной функции Parent: :F2, которая в свою очередь вызовет Parent: :F1, а не Child: :F1, что поддержало бы полиморфное поведение.
C++ однозначно определяет позднее связывание в момент выполнения и обеспечивает полиморфное поведение функций посредством их виртуализации. Листинг 3.10 обобщает синтаксис объявления виртуальных функций в базовом и производном классах.
jclass classNamel {
// Другие функции-члены
virtual returnType functionName(<список параметров>) ;
};
class className2 : public classNamel {
// Другие функции-члены
virtual returnType functionName(<cmicoK параметров>) ;
};
Листинг 3.10. Объявление виртуальных функции в иерархии классов.
Чтобы обеспечить полиморфное поведение функции F1 в объектах классов Parent и Child, необходимо объявить ее виртуальной. Листинг 3.11 содержит модифицированный текст программы.
class Parent {
public:
virtual double F1(double x) { return x*x; }
double F2(double x) { return Fl(x)/2; }
};
class Child : public Parent { public:
virtual double F1(double x) { return x*x*x; }
);
void main() {
Child child;
cout “ child.F2(3) “ endl;
}
Листинг 3.11. Позднее связывание виртуальных функций.
Теперь программа выдаст ожидаемый результат 13.5. Компилятор оттранслирует выражение child. F2 (3) в обращение к унаследованной функции Parent: : F2, которая в свою очередь вызовет переопределенную виртуальную функцию потомка Child: :F1.
Если функция объявлена в базовом классе как виртуальная, ее можно переопределять только в производных классах и обязательно с тем же списком параметров. Если виртуальная функция производного класса изменила список параметров, то ее версия в базовом классе (и во всех его предшественниках) станет недоступной. Поначалу такая ситуация может показаться тупиковой - и на деле оказывается таковой в языках ООП, которые не поддерживают механизм перегрузки. C++ решает проблему, допуская использовать не виртуальные, а перегруженные функции с тем же именем, но с другим списком параметров.
Функция, объявленная виртуальной, считается таковой во всех производных классах - независимо от того, объявлена ли она в производных классах с ключевым словом virtual, или нет.
Используйте виртуальные функции для реализации специфического поведения объектов данного класса. Не объявляйте все ваши методы виртуальными - это приведет к дополнительным вычислительным затратам при их вызовах. Всегда объявляйте деструкторы виртуальными. Это обеспечит полиморфное поведение при уничтожении объектов в иерархии классов.
3.5.2 Дружественные функции
Дружественные функции, хотя и не принадлежат какому-то классу, однако имеют доступ ко всем приватным и защищенным членам данных внешних классов. Листинг 3.12 обобщает синтаксис объявления дружественных функций с помощью ключевого слова friend перед указанием возвращаемого типа.
class className
( 1
public: ~
className(); // Конструктор по умолчанию // Другие конструкторы friend returnType friendFunction(<список параметров>) ;
};
Листинг 3.12. Объявление дружественных функций.
Если обычные функции-члены имеют автоматический доступ ко всем данным своего класса за счет передачи скрытого параметра - указателя this на экземпляр класса, то дружественные функции требуют явной спецификации этого параметра. Действительно, объявленная в классе Х дружественная функция F не принадлежит этому классу, а, значит, не может быть вызвана операторами х. F и xptr->F (где х- экземпляр класса X, a xptr- его указатель). Синтаксически корректными будут обращения F (& х) или F (xpt r).
Таким образом, дружественные функции могут решать задачи, реализация которых посредством функций-членов класса оказывается неудобной, затруднительной и даже невозможной.
3.6 Новые возможности языка C++
C++Builder обеспечивает не только поддержку последних нововведении стандарта ANSI C++, но и расширяет язык новыми возможностями. Компоненты, свойства, методы, обработчики событии, а также шаблоны, пространства имен, явные и непостоянные объявления, RTTI и исключения - вся мощь этих средств доступна программистам, использующим C++Builder для визуальной разработки приложений.
Важно понять, что расширения языка никогда не являются самоцелью, и вы по-прежнему сможете компилировать тексты, написанные в рамках стандартного C++. Однако, чтобы воспользоваться в полной мере преимуществами, которые предоставляет C++Builder для технологии быстрой разработки приложений (RAD), вам придется принять введенные расширения языка.
Некоторые из расширений (например, _classid) C++Builder резервирует, главным образом, для внутреннего использования. Другие расширения совершенно очевидны (_int8, _intl6 и т.д.), и здесь не рассматриваются. Наше внимание будет сфокусировано на наиболее значимых расширениях C++, которые, в основном, относятся к компонентным классам и будут постоянно встречаться как в тексте книги, так и в ваших приложениях, разрабатываемых в среде C++Builder.
3.6.1 Компоненты
Компоненты часто достигают более высокой степени инкапсуляции, нежели стандартные C++ классы. Проиллюстрируем это на простом примере разработки диалога, содержащего кнопку. В типичной C++ программе для Windows нажатие мышью на кнопку приводит к генерации сообщения WM_LBUTTONDOWN. Это сообщение должно быть "поймано" программой либо в операторе switch, либо в соответствующей строке таблицы откликов (RESPONSE_TABLE), а затем передано процедуре реакции на это сообщение. Так, приложение, написанное в рамках OWL (Object Windows Library), использует макрос
DEFINE_RESPONSE_TABLE1(TEventTestDlgClient, TDialog)
//({TEventTestDlgClientRSP_TBL_BEGIN}}
EV_BN_CLICKED(IDEVENTBUTTON, EventBNClicked),
//({TEventTestDlgClientRSP_TBL_END}}
END_RESPONSE_TABLE;
чтобы ассоциировать событие (сообщение WM_LBUTTONDOWN), генерируемое кнопкой IDEVENTBUTTON в диалоге TEventTestDlgClient, с функцией реакции EventBNClicked.
C++Builder покончил с этими трудно осваиваемыми программистскими трюками. Компонента кнопки уже запрограммирована так, чтобы реагировать на нажатие кнопки событием OnClick. Все, что надо сделать - это выбрать готовый (или написать собственный) метод и с помощью Инспектора объектов включить его в обработчик данного события.
3.6.1.1 Объявления компонентных классов
Опережающие объявления классов Библиотеки Визуальных Компонент VCL, входящей в состав C++Builder, используют модификатор _declspec:
_declspec(<спецификатор>)
Это ключевое слово может появляться в любом месте перечня объявлений, а не только непосредственно перед модифицируемым объявлением, причем спецификатор принимает одно из следующих значений:
delphiclass используется для опережающего объявления прямых или косвенных производных от VCL класса TObject. Он определяет правила совместимости VCL при обращении с RTTI, конструкторами, деструктором и исключениями.
delphireturn используется для опережающего объявления прямых или косвенных производных от VCL классов Currency, AnsiString, Variant, TDateTime и Set. Он определяет правила совместимости VCL при обращении с параметрами и возвращаемыми значениями функций-членов.
pascalimplementation указывает, что компонентный класс реализован на Объектном Паскале.
VCL класс имеет следующие ограничения:
• Запрещено наследование виртуальных базовых классов.
• Компонентные классы сами не могут служить базовыми классами для наследования.
• Компонентные объекты создаются в динамической памяти кучи с помощью оператора new.
3.6.1.2 Объявления свойств
C++BuiIder использует модификатор _property для идентификации свойств компонентных классов. Синтаксис описания свойства имеет вид:
property <тип свойства> <имя свойства> = {<список атрибутов>} ;
где список атрибутов содержит перечисление следующих атрибутов свойства:
write = < член данных или метод записи > определяет способ присваивания значения члену данных;
read = < член данных или метод чтения > определяет способ получения значения члена данных;
default = < булева константа > разрешает или запрещает сохранение значения свойства по умолчанию в файле формы с расширением .dim;
stored = < булева константа или функция > определяет способ сохранения значения свойства в файле формы с расширением .dfm.
C++BuiIder использует модификатор _published для спецификации тех свойств компонент, которые будут отображаться Инспектором объектов на стадии проектирования приложения. Если разработчик компоненты желает разрешить модификацию значения некоторого свойства, оно не объявляется как _published. Правила видимости, определяемые этим ключевым словом, не отличаются от правил видимости членов данных, методов и свойств, объявленных как public. Единственное отличие проявляется в том, что во время работы программы Инспектору объектов передается информация RTTI.
3.6.1.3 Объявления обработчиков событий '
C++BuiIder использует модификатор _closure для объявления функции обработчиков событий:
<тип> (_closure * <name>) (<список параметров>)
Это ключевое слово определяет указатель функции с именем name. В отличие от 4-байтового адресного указателя обычной функции (который передается в кодовые регистры CS:IP) 8-байтовый _closure передает еще и скрытый параметр (непостоянный указатель this на экземпляр текущего класса).
Введение 8-байтовых указателей делает возможным не только вызывать некоторую функцию определенного класса, но и обращаться к функции в определенном экземпляре этого класса. Эта способность была заимствована из Объектного Паскаля, а _closure оказался жизненно необходимым для реализации механизма событий в Библиотеке Визуальных Компонент.
3.6.1.4 Объявления автоматизированных свойств и методов
OLE Automation - это разновидность механизма связи Object Linking and Embedding, позволяющего приложениям для Windows управлять друг другом. Автоматизированный OLE контроллер является приложением, которое способно автоматизировать другое приложение - автоматизированный OLE сервер. По существу, OLE Automation представляет собой протокол обмена, посредством которого контроллер управляет действиями сервера. Все компонентные OLE объекты, экспортируемые автоматизированным сервером своим контроллерам, являются производными от базового класса TAutoObject.
При создании автоматизированного сервера необходимо определить его интерфейс с контроллером, содержащий объявления свойств и методов OLE объекта с тем, чтобы контроллер получил к ним доступ. Никогда не удаляйте уже включенные в интерфейс свойства и методы - это приведет к ошибкам в работе существующих контроллеров. C++Builder использует модификатор _automated в объявлениях автоматизированного метода (Листинг 3.13). Это объявление может заканчиваться еще одним новым ключевым словом _dispid, которое ассоциирует значение идентификатора диспетчеризации OLE Automation с данной функцией.
class MyOLEAutoClass : TAutoObject
{ _automated: void_fastcall function(void) _dispid(1000); };
Листинг 3.13. Объявление автоматизированного метода.
Правила видимости, определяемые этим ключевым словом, не отличаются от правил видимости, объявленных в секции public. Единственное отличие проявляется в том, что генерируемая компилятором информация о типах свойств и методов OLE Automation, делает возможным создание автоматизированных серверов.
C++Builder поставляется вместе с примером (удивительным по внешней простоте и лаконичности кода) взаимодействия приложений контроллера и сервера посредством механизма OLE Automation. Этот пример проливает свет на данную методику, изложенную в системной документации весьма сбивчиво и туманно. Автоматизированный сервер Autosrv демонстрирует использование:
• компонентных объектов TAutoObject и TAutoClassInfo;
• метода RegisterAutoClass автоматизированного объектного класса для регистрации сервера;
• свойств и методов, объявленных с ключевым словом _automated. Контроллер Autocon управляет сервером и демонстрирует:
• установку и выборку свойств объекта сервера;
• использование вариантов (детальная информация об типе Variant содержится в параграфе 3.6.1.6 "Расширенные типы данных Delphi".
Чтобы испытать на практике, что дает взаимодействие приложений OLE Automation, выполните следующие действия:
=> По команде главного меню File [ Open Project откройте диалог выбора проектов. => Войдите в каталог \...\CBuilder\Examples\Apps\Autosrv => Выберите проектный файл с именем Autosrv и нажмите кнопку Open.
=> Командой главного меню Run | Run запустите процесс компиляции и сборки автоматизированного сервера. => Снова откройте диалог выбора проектов, войдите в каталог
\...\CBuilder\Examples\Apps\Autocon, выберите проектный файл с именем
Autocon и нажмите кнопку Open. => Запустите процесс компиляции и сборки контроллера.
Вводя сообщения в области редактируемого ввода и нажимая кнопки контроллера, вы можете моделировать некоторые процессы управления сервером. получая результаты, которые отображены на нижеследующих рисунках:
Рис. 3.4. Контроллер готовит сообщение и посылает его серверу.
Рис. 3.5. Контроллер принимает сообщение, "обработанные" сервером.
Рис. 3.6. Контроллер снимает старое сообщение с сервера.
Чтобы узнать, как реализовано такое взаимодействие, необходимо разобраться в текстах модулей автоматизированного сервера (Листинг 3.14 и Листинг 3.15) и контроллера (Листинг 3.16 и Листинг 3.17), которые заслуживают того, чтобы привести их целиком, снабдив необходимыми комментариями. Сервер содержит единственный объект Edit1 компоненты TEdit для редактируемого ввода и приема сообщений от контроллера в свойство Text. Контроллер записывает введенное пользователем сообщение в свойство Text своего объекта Edit1, а управляет сервером посредством трех кнопок Buttoni, Button2 и Buttons компоненты TButton (с названиями "Послать", "Принять" и "Очистить").
#ifndef Auto2H
#define Auto2H
ftinclude <Classes.hpp> ftinclude <01eAuto.hpp>
#include <System.hpp>
// Класс сервера ButtonServer. производный от TAutoOb-iect class ButtonServer : public TAutoObject {
private: // Приватные свойства и методы AnsiString _fastcall GetEditStr() ;
void _fastcall SetEditStr(AnsiString NewVal);
int _fastcall GetEditNum() ;
void _fastcall SetEditNum(int NewVal);
_automated: // Автоматизированные свойства и методы property AnsiString EditStr = // свойство типа AnsiString / / с атрибутами функции чтения/записи значении {read=GetEditStr, write=SetEditStr} ;
property int EditNum = // свойство типа int / / с атрибутами функций чтения/записи значений {read=GetEditNum, write=SetEditNum); :
void _fastcall Clear (); // метод очистки сообщения void_fastcall SetThreeStr // метод составления текстового / / сообщения из строк в параметрах si, s2, s3 типа AnsiString (AnsiString si, AnsiString s2, AnsiString s3);
void _fastcall SetThreeNum // метод составления текстового / / сообщения из чисел в параметрах п 1, п2, п3 типа int (int п1, int п2, int п3) ;
public: // Общедоступные свойства и методы
_fastcall ButtonServer О; // конструктор объекта сервера
};
//----------------------------------------------------------
#endif
Листинг 3.14. Файл объявлений Auto1.h модуля автоматизированного сервера.
#include <vcl.h>
#pragma hdrstop
#undef RegisterClass
#include "Auto2.h"
#include "Autol.h"
int Initialization();
static int Initializer = Initialization();
// Создание объекта автоматизированного сервера _fastcall ButtonServer::ButtonServer() : TAutoObject()
{ }
// Чтение текстового значения автоматизированного свойства AnsiString _fastcall ButtonServer::GetEditStr() { return Forml->Editl->Text;
// Запись текстового значения автоматизированного свойства void _fastcall ButtonServer:-.SetEditStr (AnsiString NewVal) { Forml->Editl->Text = NewVal;
} // Чтение численного значения автоматизированного свойства
int _fastcall ButtonServer: :GetEditNum()
( int val;
sscanf(Forml->Editl->Text.c_str(), "%d", &val) ;
return val;
// Запись численного значения автоматизированного свойства void _fastcall ButtonServer::SetEditNum(int NewVal) { Forml->Editl->Text = NewVal;
}
// Очистка значения автоматизированного свойства void_fastcall ButtonServer::Clear() { Forml->Editl->Text = "";
// Составление текстового значения свойства из трех строк void _fastcall ButtonServer::SetThreeStr (AnsiString si, AnsiString s2, AnsiString s3) { Forml->Editl->Text = si + ", " + s2 + ", " + s3;
) // Составление текстового значения свойства из трех чисел
void _fastcall ButtonServer::SetThreeNum (int nl, int n2, int n3) { AnsiString sl(nl), s2(n2), s3(n3);
Forml->Editl->Text = si + ", " + s2 + ", " + s3;
// Регистрация объекта автоматизированного сервера void _fastcall RegisterButtonServer() { TAutoClassInfo AutoClassInfo;
// Инициализация полей структуры типа TAutoClassInfo AutoClassInfo.AutoClass = _classid(ButtonServer) ;
AutoClassInfo.ProgID = "BCBAutoSrv.EditServer" ;
AutoClassInfo.ClassID =
"{61E124E1-C869-11CF-9EA7-OOA02429B18A}";
AutoClassInfo.Description =
"Borland C++Builder AutoSrv Example Server Class";
AutoClassInfo.Instancing = acMultiInstance;
// Регистрация класса автоматизированного сервера Automation->RegisterClass(AutoClassInfo) ;
}
// Инициализация объекта автоматизированного сервера
int Initialization()
{ RegisterButtonServer() ;
return 0;
}
Листинг 3.15. Кодовый файл Auto2cpp модуля автоматизированного сервера.
#ifndef AutolH #efine AutolH
class TFormI : public TForm {
published: // IDE-managed Components
TEdit *Editl;
TButton *Buttonl
TButton *Button2
TButton *Button3
TLabel * Label 1;
void _fastcall ButtonlClick(TObject *Sender) ;
void _fastcall Button2Click(TObject * Sender);
void _fastcall Button3Click(TObject * Sender);
private: // User declarations
Variant AutoServer;
public: // User declarations
virtual _fastcall TFormI(TComponent *0wner) ;
extern TFormI *Forml;
#endif
Листинг 3.16. Файл объявлений Auto1.h, все строки которого (за исключением выделенной строки объявления варианта) C++Builder генерирует автоматически при размещении компонент на форме контроллера.
include <vcl.h> #pragma hdrstop
#include "autol.h"
#pragma resource "*.dfm" #ormI *Forml;
_fastcall TFormI::TFormI(TComponent *0wner) : TForm(Owner) { try
{ // Создание автоматизированного сервера как объекта OLE AutoServer = CreateOleObject("BCBAutoSrv.Edit Server");
}
catch (...) { // Обработка исключения
ShowMessage("Сначала компилируйте и запустите AutoSrv");
Application-terminate () ;
}
} // Обработчик события при нажатии кнопки Buttoni
void _fastcall TFormI::ButtonlClick(TObject *Sender) { // Установка автоматизированного свойства сервера AutoServer.OlePropertySet("EditStr", Editl->Text);
)
// Обработчик события при нажатии кнопки Button2
void _fastcall TFormI::Button2Click(TObject *Sender)
{ // Чтение автоматизированного свойства сервера
Editl->Text = AutoServer.OlePropertyGet("EditStr");
// Обработчик события при нажатии кнопки Button3 void _fastcall TFormI::Button3Click(TObject *Sender) { // Очистка автоматизированного свойства сервера AutoServer.OleProcedure("Clear") ;
Листинг 3.17. Кодовый файл Auto1.cpp модуля контроллера.
3.6.1.5 Быстрый вызов функций
При объявлении функций, параметры которых передаются через процессорные регистры, используется модификатор _fastcall:
<возвращаемый тип> _fastcall <name>(<список параметров^
Это ключевое слово определяет, что первые три типизированных параметра функции с именем name (слева направо по списку) передаются не через стек, а через процессорные регистры AX, BX и DX. Регистры не используются, если значение параметра не умещается в регистр, т.е. при передаче через параметр чисел с плавающей точкой, структур и функций.
Строго говоря, быстрый вызов функций не является прерогативой компилятора C++Builder. В предыдущей главе я уже обращал внимание читателя на использование _fastcall в объявлениях функций обработки событий, которые C++Builder генерирует автоматически.
3.6.1.6 Расширенные типы данных Delphi
C++Builder не позволяет посредством известного ключевого слова typedef просто переопределить некоторые сложные типы данных Объектного Паскаля. C++Builder реализует такие расширенные типы в виде обычных или шаблонных классов (template class). Каждый такой класс содержит все необходимые конструкторы, деструкторы, свойства и объектные методы. Многие компоненты VCL используют реализацию расширенных типов, а кроме того, они требуются при разработке новых компонент на базе оригиналов из Delphi.
Ниже приводится сводная таблица встроенных типов Delphi и соответствующих им типов C++Builder:
Delphi | Длина и значения | C++Builder | Реализация |
Shortint | 8-битовое целое | char | typedef |
Smallint | 16-битовое целое | short | typedef |
Longint | 32-битовое целое | long | typedef |
Byte | 8-битовое целое без знака | unsigned char | typedef |
Word | 16-битовое целое без знака | unsigned short | typedef |
Integer | 32-битовое целое | int | typedef |
Cardinal | 32-битовое целое без знака | unsigned long | typedef |
Boolean | true/false | bool | typedef |
ByteBool | true/false или 8-битовое целое без знака | unsigned char | typedef |
WordBool | true/false или 16-битовое целое без знака |
unsigned short | typedef |
LongBool | true/false или 32-битовое целое без знака |
unsigned long | typedef |
AnsiChar | 8-битовый символ без знака | unsigned char | typedef |
WideChar | Слово - символ Unicode | wchar t | typedef |
Char | 8-битовый символ | char | typedef |
String | Текстовая строка Delphi | AnsiString | typedef |
Single | 32-битовое плавающее число | float | typedef |
Double | 64-битовое плавающее число | double | typedef |
Extended | 80-битовое плавающее число | long double | typedef |
Real | 32-битовое плавающее число | float | typedef |
Comp | 64-битовое плавающее число | double | typedef |
Pointer | 32-битовый указатель | void * | typedef |
PChar | 32-битовый указатель на символы без знака | unsigned char * | typedef |
PansiChar | 32-битовый указатель на ANSI символы без знака | unsigned char * | typedef |
Set | Множество 1..32 байт | Set<type, minval, maxval> | template class |
AnsiString | Текстовая строка Delphi | AnsiString | class |
Variant | Вариантное значение, 16 байт | Variant | class |
TdateTime | Значение даты и времени, 64-битовое плавающее число | TDateTime | class |
Currency | Валюта, 64-битовое плавающее число, 4 цифры после точки | Currency | class |
Set (множество) служит для спецификации типа параметров объектных методов VCL или типа значений, возвращаемых этими методами. C++Builder реализует этот встроенный тип Delphi с помощью одноименного шаблонного класса Set<type, minval, maxval> со следующими параметрами:
type тип элементов множества (обычно, int. char или enum):
minval минимальное (положительное) значение, которое могут принимать элементы множества;
maxval максимальное (не более 255) значение, которое могут принимать элементы множества.
Подстановка разных значений параметров приводит к созданию экземпляров шаблонного класса Set различных типов, поэтому оператор сравнения if (si == s 2) объектов, описанных как
Set<char, 'A', 'C'> si;
Set<char, 'X', 'Z'> s2;
вызовет ошибку компиляции. Для создания множественных экземпляров типа Set необходимо использовать ключевое слово typedef. Например, объявив typedef Set<char, 'A','Z'> UpperCaseSet; можно создать множества UpperCaseSet si; и UpperCaseSet s2; а затем инициализировать эти объекты:
s1 “ 'А' “ 'В' “ 'С' ;
s2 “ 'X' “ 'Y' “ '?.' ;
AnsiString используется для спецификации типа текстовых строк произвольной длины, имеющих следующую характерную внутреннюю структуру:
счетчик |
длина строки |
данные |
терминатор \0 |
C++Builder реализует этот встроенный тип Delphi как одноименный класс. Если при создании экземпляров данного класса не указано начальное значение строки, конструктор AnsiString автоматически присваивает всем переменным нулевые значения. Среди методов данного класса отметим наиболее часто вызываемый метод с str (), который возвращает указатель на символьный массив, оканчивающийся 0 и содержащий копию символов, заключенных в исходном объекте типа AnsiString. Листинг 3.18 иллюстрирует "применение методов чтения и записи значения члена данных FNames свойства Names типа AnsiString в экземпляре MyFamily объявленного компонентного класса Family". Предыдущее предложение кажется полной абракадаброй, если не проникнуться терминологией объектно-ориентированного программирования. Рассматривайте его как своеобразное словесное упражнение по краткому курсу ООП.
#include <vcl/dstring.h> #include <stdio.h> class Family // объявление класса
{
private:
AnsiString FNames[10]; // массив имен AnsiString GetName(int Index); // метод чтения void SetName(int, AnsiString); // метод записи public:
_property AnsiString Names[int Index] =
{read=GetName, write=SetName} ;
Family(){} // .конструктор -Family(){) // деструктор
};
AnsiString Family::GetName(int i)
{
return FNames[i]; // GetName возвращает значение }
void Family::SetName(int i, const AnsiString s) { FNames[i]=s; // SetName присваивает значение
}
void main()
{
Family My Family; // создание объекта MyFamily // Инициализация 4-х строк массива имен методом SetName() MyFamily.Names[0]="Иван" ;
MyFamily.Names[1]="Анна" ;
MyFamily.Names[2]="Марья";
MyFami ly. Names [ 3 ] = " Андрей " ;
// Вывод 4-х строк массива имен методом GetName() for (int i=0; i<=3; i++)
puts(MyFamily.Names[i].c_str()) ;
}
Листинг 3.18. Пример использования типа AnsiString в C++ программе с компонентным классом Family (Семья).
Variant служит для спецификации значений, меняющих тип динамически. Переменная вариантного типа, в отличие от обычных статически типизированных переменных, способна менять свой тип во время исполнения программы. C++Builder объявляет этот тип Delphi как class __declspec(delphireturn) Variant: public TVarData. Заметим, чтс синтаксис вариантов, принятый в Delphi, например:
V: Variant;
V := VarArrayCreate([0,Hi9hVal,0,HighVal],varlnteger) ;
отличается от способа записи вариантного массива в C++Builder:
Variant V(OPENARRAY(int,(0,HighVal,0,HighVal)),varlnteger);
Вариант может быть сконструирован из следующих типов данных: short, int, float, double. Currency, TDateTime, bool, WordBool, Byte, AnsiString&, char *, wchar_t * const. 01e2::lDispatch* const или 01e2::IUnknown* const. Компилятор автоматически выполнит необходимые преобразования типа. При создании вариантных переменных они всегда инициализируются специальным значением Unassigned (не определено). Специальное значение Null указывает, что данные отсутствуют.
Внимание: Варианты предоставляют ООП чрезвычайную гибкость, однако требуют большей памяти, нежели статически типизированные переменные, а операции над ними выполняются заметно медленнее.
TDateTime используется для спецификации переменных даты и времени. C++Builder реализует этот встроенный тип Delphi как одноименный класс, который инкапсулирует член данных типа double, содержащий значение даты в целой части числа, а значение времени в мантиссе (считая от полудня 30 декабря 1899 года). В следующей таблице приведены значения переменной типа TDateTime и их эквиваленты в выражениях даты и времени:
Значение | Дата | Время | Примечания |
0 | 12/30/1899 | 12:00 | +0 дней, +0 часов |
2.75 | 01/01/1900 | 18:00 | +2 дня, +6 часов |
-1.25 | 12/29/1899 | 06:00 | -1 день, -б часов |
35065 | 01/01/1996 | 12:00 | +35065 дней, +0 часов |
Чтобы вычислить дробное число дней, прошедших между двумя датами, вычтите второе значение из первого. Чтобы перейти к следующему дню, сохранив время, просто добавьте 1 к текущему значению.
3.6.2 Расширения стандарта ANSI C++
Рассматриваемые расширения стандарта ANSI C++, в основном, представляют интерес для разработчиков новых классов и компонент, а также для программистов, которые работают в большом коллективе над созданием сложного проекта.
Вы можете прочитать остаток главы "по диагонали" или вовсе пропустить его, если не занимаетесь указанной деятельностью, и тонкости языка вас не интересуют.
3.6.2.1 Шаблоны
Шаблоны (параметризованные типы)позволяют конструировать семейство связанных функций или классов. Обобщенный синтаксис определения шаблона имеет вид
template <список шаблонных типов> { объявление } ;
Различают шаблоны функций и шаблоны классов.
Шаблон функции задает образец определений перегружаемых функций. Рассмотрим шаблон функции тах(х, у), которая возвращает больший из двух аргументов, которые могут быть любого, допускающего упорядочивание, типа:
template <class Т> Т тах(Т х. Ту) { return (x > у) ? х : у; } ;
причем тип данных, представленный аргументом шаблона <class T>, может быть любым. При его использовании в программе компилятор генерирует код функции тах в соответствии с фактическим типом передаваемых ей параметров:
int i;
Myclass a, b;
int j = max(i, 0); // тип аргументов int Myclass m = max(a, b); // тип аргументов Myclass
Фактические типы должны быть известны во время компиляции. Без шаблонов пришлось бы многократно перегружать функцию max - для каждого поддерживаемого типа, хотя код всех версий функции по существу был бы идентичным. Стандарт C++ настоятельно не рекомендует использовать для этой цели макрос:
#define max(x,y) ((х > у) ? х : у) из-за блокировки механизма проверки типов, который дает такие преимущества языку C++ над обычным С. Очевидно, задача функции тах(х, у) - сравнить совместимые типы. К сожалению, использование макроса допускает сравнение несовместимых типов, например, int и struct.
Шаблон классов задает образец определений семейства классов. Рассмотрим пример шаблона Vector - генератора классов одномерного массива данных:
template <class T> class Vector
Над типизированными элементами этого класса выполняются одинаковые базовые операции (вставить, вычеркнуть, индексировать и т.д.), вне зависимости от конкретного типа элементов. Если обращаться с типом как с параметром, то компилятор будет генерировать классы векторов с элементами заданного типа.
Как и в случае шаблонов функций, разрешается явно переопределять тип шаблон классов:
class Vector<char *> ( ... };
причем символ Vector должен всегда сопровождаться типом данных, заключенным в угловые скобки.
3.6.2.2 Пространства имен
Большинство нетривиальных приложений состоят из нескольких файлов с исходным текстом программы. Эти файлы могут создаваться и обслуживаться группой программистов. В конце концов, все файлы собираются вместе и проходят через финальную процедуру сборки готового приложения. Традиционно принято, чтобы все имена, не заключенные в некоторой локальной области (функции, теле класса или модуле трансляции), разделяли общие глобальные имена. Поэтому повторное определения имен, обнаруженное в процессе сборки отдельных модулей, приводит к необходимости каким-то образом различать каждое имя. Решение этой проблемы в C++ возложено на механизм пространства имен (namespace).
Этот механизм позволяет разбить приложение на ряд подсистем, причем каждая подсистема свободна в отношении выбора имен, и ее автор не должен беспокоиться о том, что такие же имена будет использовать кто-то другой. Каждая подсистема идентифицирует свое появление в общем пространстве глобальных имен уникальным идентификатором, который следует за ключевым словом namespace:
namespace <идентификатор> { [<объявления>] } Существует три способа доступа к элементам идентифицированного пространства имен:
• Явная квалификация доступа к конкретному элементу:
ALPHA :: varl; // доступ к переменной из ALPHA BETA :: Fl; // доступ к функции из BETA
• Доступ ко всем элементам:
using namespace :: ALPHA; // доступ ко всем именам из ALPHA
• Объявление нового идентификатора в локальном пространстве имен:
using :: new_name; // добавление идентификатора
3.6.2.3 Явные объявления
Обычно объектам класса, в котором объявлен конструктор с одним параметром, можно присвоить значения, тип которых автоматически (неявно) преобразуется к своему классовому типу. При объявлении конструктора можно использовать модификатор explicit:
explicit <объявление конструктора> Тогда при объявлении конструкторов данного класса с ключевым словом explicit всем объектам класса можно присвоить только те значения, тип которых явно преобразуется к классовому типу (Листинг 3.19). Другие присваивания приведут к ошибке компиляции.
class X
public:
explicit X(int);
explicit X(const char*, int = 0);
};
void f(X arg)
(
X a = X (1) ;
X b = Х("строка",0);
a = Х(2);
} :(
Листинг 3.19. Явные объявления конструкторов.
Явные объявления конструкторов требуют, чтобы значения в операторах присваивания были преобразованы к тому классовому типу, объектам которого эти значения присваиваются.
3.6.2.4 Непостоянные объявления
При объявлении переменной, которая может быть изменена фоновой задачей, обработчиком прерывания или портом ввода-вывода, используется модификатор volatile:
volatile <тип> <имя объекта>;
В C++ применение ключевого слова volatile распространяется на классы и функции-члены. Это ключевое слово запрещает компилятору делать предположения относительно значения указанного объекта, поскольку при вычислении выражений, включающих этот объект, его значение может измениться в любой момент. Кроме того. непостоянная переменная не может быть объявлена с модификатором register. Листинг 3.20 показывает пример реализации таймера, в котором переменная ticks модифицируется обработчиком временных прерываний.
volatile int ticks;
void timer( ) // Объявление функции таймера
ticks++;
void wait (int interval)
ticks = 0;
while (ticks < interval); // Цикл ожидания
}
Листччг 3.20. Изменение непостоянной переменной volatile.
Положим, что обработчик прерывания timer был надлежащим образом ассоциирован с аппаратным прерыванием от часов реального времени. Процедура wait реализует цикл ожидания, пока значение переменной ticks не станет равным интервалу времени, заданному ее параметром. Компилятор C++ обязан перезагружать значение переменной volatile ticks перед каждым сравнением внутри цикла - несмотря на то, что внутри цикла значение переменной не изменяется. Некоторые оптимизирующие компиляторы могли бы допустить эту "роковую" ошибку.
Другой вид непостоянной переменной, которая может быть изменена даже если она входит в константное выражение, объявляется с помощью модификатора mutable:
mutable <имя переменной>;
Назначение ключевого слова mutable состоит в спецификации членов данных некоторого класса, которые могут быть модифицированы константными функциями этого класса. Листинг 3.21 показывает пример, в котором член данных count модифицируется константной функцией F1.
class A {
public: mutable int count; int F1 (int p = 0) const // Объявление функции F1
count = p++ return count; //PI возвращает count
) I
void main() {
A a;
cout “ a.Fl(3) “ end.1; // main выдает значение 4 )
Листинг 3.21. Изменение непостоянной переменной mutable.
3.6.2.5 Идентификация типов RTTI
Идентификация типов при выполнении программы RTTI (Run-Time Туре Identification) позволяет вам написать переносимую программу, которая способна определять фактический тип объекта в момент выполнения даже в том случае, если программе доступен только указатель на этот объект. Это дает возможность, например, преобразовывать тип указателя на виртуальный базовый класс в указатель на производный тип фактического объекта данного класса. Таким образом, преобразование типов может происходить не только статически - на фазе компиляции, но и динамически - в процессе выполнения. Динамическое преобразование указателя в заданный тип осуществляется с помощью оператора dynamic_cast.
Механизм RTTI также позволяет проверять, имеет ли объект некоторый определенный тип, или принадлежат ли два объекта одному и тому же типу. Оператор typeid определяет фактический тип аргумента и возвращает указатель на объект класса typeinfo, который этот тип описывает.
Передавая RTTI Инспектору объектов во время выполнения, C++Builder информирует его о типах свойств и членов данного класса.
3.6.2.6 Исключения
Язык C++ определяет стандарт обслуживания исключений в рамках ООП. C++Builder предусматривает специальные механизмы для обработки исключений (ошибок), которые могут возникнуть при использовании Библиотеки Визуальных Компонент. C++Builder также поддерживает обработку исключений самой операционной системы и модель завершения работы приложения.
Когда программа встречает ненормальную ситуацию, на которую она не была рассчитана, можно передать управление другой части программы, способной справиться с этой проблемой, и либо продолжить выполнение программы, либо завершить работу. Переброс исключений (exception throwing) позволяет собрать в точке переброса информацию, которая может оказаться полезной для диагностики причин, приведших к нарушению нормального хода выполнения программы. Вы можете определить обработчик исключения (exception handler), выполняющий необходимые действия перед завершением программы. Обслуживаются только так называемые синхронные исключения, которые возникают внутри программы. Такие внешние события, как нажатие клавиш Ctrl+C, не считаются исключениями.
Блок кода, который может сгенерировать исключение, начинается ключевым словом try и заключается в фигурные скобки. Если блок try обнаруживает исключение внутри этого блока, происходит программное прерывание и выполняется следующая последовательность действий:
1. Программа ищет подходящий обработчик исключения.
2. Если обработчик найден, стек очищается и управление передается обработчику исключения.
3. Если обработчик не найден, вызывается функция terminate для завершения приложения.
Блок кода, который обрабатывает возникшее исключение, начинается ключевым словом catch и заключается в фигурные скобки. По меньшей мере один кодовый блок обработчика исключения должен следовать непосредственно за блоком try. Для каждого исключения, которое может сгенерировать программа, должен быть предусмотрен свой обработчик. Обработчики исключений просматриваются по порядку и выбирается обработчик исключения, тип которого соответствует типу аргумента в операторе catch. При отсутствии в теле обработчика операторов goto, выполнение программы будет возобновлено, начиная с точки, следующей за последним обработчиком исключений данного блока try. Листинг 3.22 демонстрирует обобщенную схему обработки исключений.
try
i
// Любой код, который может сгенерировать исключение
} 1
catch (Т X) |
{ I
// Обработчик исключения Х типа Т, которое могло быть |
// ранее сгенерировано внутри предыдущего блока try Я // Обработчики других исключений предыдущего блока try catch (...)
// Обработчик любого исключения предыдущего блока try
Листинг 3.22. Обработка исключении.
Когда возникает исключение, выражение присваивания в операторе throw <выражение> инициализирует временный объект, тип которого соответствует типу аргумента выражения. Другие копии этого объекта могут быть сгенерированы, например, при помощи конструктора копирования объекта исключения.
Следование принятой стандартной методике обработки ошибок - одна из гарантий построения надежных приложений, хотя операционная система и постарается сама "вытянуть" исполняемую программу из непредвиденных ситуаций. Осознавая важность этого элемента ООП, я включил в развернутые примеры по программированию баз данных главы 5 необходимые коды для обработки исключений.
3.7 Итоги
Мы вкратце ознакомились с терминологией объектно-ориентированного программирования на языке C++ и с некоторыми расширениями языка. ООП оперирует абстрактными понятиями классов, объектов, методов, инкапсуляции, наследования и полиморфизма. Читатель быстро научится находить аналоги терминов ООП в реальном мире, что облегчит освоение в общем-то простых понятий с довольно туманными и, зачастую, неудачными определениями.
Мы по достоинству оценили введение новых компонентных классов и подготовились к манипулированию с ними в рамках интегрированной визуальной среды быстрой разработки приложений.