При использовании ООП все объекты являются в некотором смысле обособленными друг от друга, и возникают определенные трудности в передаче информации от объекта к объекту. В ООП для передачи информации между объектами используется механизм обработки событий.
События лучше всего представить себе как пакеты информации, которыми обмениваются объекты и которые создаются объектно-ориентированной средой в ответ на те или иные действия пользователя. Нажатие на клавишу или манипуляция мышью порождают событие, которое передается по цепочке объектов, пока не найдется объект, знающий, как обрабатывать это событие. Для того чтобы событие могло передаваться от объекта к объекту, все объекты программы должны быть объединены в группу. Отсюда следует, что прикладная программа должна быть объектом-группой, в которую должны быть включены все объекты, используемые в программе.
Таким образом, объектно-ориентированная программа – это программа, управляемая событиями. События сами по себе не производят никаких действий в программе, но в ответ на событие могут создаваться новые объекты, модифицироваться или уничтожаться существующие, что и приводит к изменению состояния программы. Иными словами все действия по обработке данных реализуются объектами, а события лишь управляют их работой.
Принцип независимости обработки от процесса создания объектов приводит к появлению двух параллельных процессов в рамках одной программы: процесса создания объектов и процесса обработки данных.
Это означает, что действия по созданию, например, интерактивных элементов программы (окон, меню и пр.) можно осуществлять, не заботясь о действиях пользователя, которые будут связаны с ними.
И наоборот, мы можем разрабатывать части программы, ответственные за обработку действий пользователя, не связывая эти части с созданием нужных интерактивных элементов.
Событие с точки зрения языка С++ – это объект, отдельные поля которого характеризуют те или иные свойства передаваемой информации, например:
struct Event { int what; union { MouseEventType mouse; KeyDownEvent keyDown; MessageEvent message; };
Объект Event состоит из двух частей. Первая (what) задает тип события, определяющий источник данного события. Вторая задает информацию, передаваемую с событием. Для разных типов событий содержание информации различно. Поле what может принимать следующие значения:
struct MouseEventType { int buttons; int doubleClick; TPoint where; };где buttons указывает нажатую клавишу; doubleClick указывает был ли двойной щелчок; where указывает координаты мыши.
struct KeyDownEvent { union { int keyCode; union { char charCode; char scanCode; }; }; };
struct MessageEvent { int command; void *infoPtr; };
Следующие методы необходимы для организации обработки событий (названия произвольны).
GeEvent – формирование события;
Execute реализует главный цикл обработки событий. Он постоянно получает событие путем вызова GeEvent и обрабатывает их с помощью HandleEvent. Этот цикл завершается, когда поступит событие «конец».
HandleEvent – обработчик событий. Обрабатывает каждое событие нужным для него образом. Если объект должен обрабатывать определенное событие (сообщение), то его метод HandleEvent должен распознавать это событие и реагировать на него должным образом. Событие может распознаваться, например, по коду команды (поле command).
ClearEvent очищает событие, когда оно обработано, чтобы оно не обрабатывалось далее.
Получив событие (структуру типа Event), обработчик событий для класса DerivedClass обрабатывает его по следующей схеме:
void DerivedClass::HandleEvent(Event &event) { // Вызов обработчика событий базового класса BaseClass::handleEvent(event); if (event.what == evCommand) // Если обработчик событий базового // класса событие не обработал { switch (event.message.command) { case cmCommand1: // Обработка команды cmCommand1 // Очистка события СlearEvent(event); break; case cmCommand2: // Обработка команды cmCommand2 СlearEvent(event); break; ... case cmCommandN: // Обработка команды cmCommandN СlearEvent(event); break; default: // событие не обработано break; } } }
Обработчик событий группы вначале обрабатывает команды группы, а затем, если событие не обработано, передает его своим элементам, вызывая их обработчики событий.
void Group::HandleEvent(Event &event) { if (event.what == evCommand) { switch (event.message.command) { // обработка событий объекта-группы default: // событие группой не обработано // получить доступ к первому элементу группы while ((event.what != evNothing) || (/* просмотрены не все элементы */) { // вызвать HandleEvent текущего элемента // перейти к следующему элементу группы } break; } } }
ClearEvent очищает событие, присваивая полю event.What значение evNothing.
Главный цикл обработки событий реализуется в методе Execute главной группы-объекта "прикладная программа" по следующей схеме:
int MyApp::Execute(void) { do { endState = 0; GeEvent(event); // получить событие HandleEvent(event); // обработать событие if (event.what != evNothing) // событие осталось не обработано EventError(event); } while (!Valid()); endState; }
Метод HandleEvent программы обрабатывает событие "конец работы", вызывая метод EndExec. EndExec изменяет значение private – переменной EndState. Значение этой переменной проверяет метод–функция Valid, возвращающая значение true, если "конец работы". Такой несколько сложный способ завершения работы программы связан с тем, что в активном состоянии могут находиться несколько элементов группы. Тогда метод Valid группы, вызывая методы Valid своих подэлементов, возвратит true, если все они возвратят true. Это гарантирует, что программа завершит свою работу, когда завершат работу все ее элементы.
Если событие осталось не обработанным, то вызывается метод EventError, которая в простейшем случае может просто выдать сообщение.
Рассмотрим простейший калькулятор, воспринимающий команды в командной строке.
Формат команды:
знак параметр
Знаки +, –, *, /, =, ?, q
Параметр – целое число
Константы-команды
const int evNothing = 0; const int evMessage = 100; сonst int cmSet = 1; // занести число сonst int cmGet = 2; // посмотреть значение const int cmAdd = 3; // добавить и т.д. const int cmQuit = 101; // выход
struct Event { int what; union { int evNothing; union { int command; int a; } } };
class Int { public: Int(int x1); virtual ~Int(); virtual void GeEvent(Event &event); virtual int Exicute(void); virtual void HandleEvent(Event &event); virtual void ClearEvent(Event &event); int Valid(void); void EndExec(void); int GetX(void); void SetX(int newX); void AddY(int Y); ... protected: int x; int EndState; };
Рассмотрим возможную реализацию основных методов.
void Int::GeEvent (Event &event) { char *OpInt = "+-*/=?q"; //строка содержит коды операций char s[20]; char code; cout << '>'; cin >>s code = s[1]; if (Test(code, OpInt) // Функция Test проверяет, входит ли // символ code в строку OpInt { event.what = evMessage; switch (code) { case '+': event.command = cmAdd; break; ... case 'q': event.command = cmQuit; break; } // выделить второй параметр, перевести его в тип int и присвоить полю A } else { event.what = evNothing; } }; int MyApp::Execute(void) { do { endState = 0; GeEvent(event); // получить событие HandleEvent(event); //обработать событие if (event.what != evNothing) // событие осталось не обработано { // обработать событие } } while (!Valid()); return endState; } void Int::HandleEvent(Event &event) { if (event.what == evMessage) { switch (event.message.command) { case cmAdd: AddY(event.A); СlearEvent(event); break; ... case cmQuit: EndExec(); СlearEvent(event); break; } } } int Int::Valid(void) { if (EndState) return 1; else return 0; } void Int::ClearEvent(Event &event) { event.what = evNothing; } void Int::EndExec(void) { EndState = 1; } void Int::AddY(int Y) { x += Y; } ... int main(void) { Int MyApp; MyApp.Execute(); return 0; }
Пространство имен (namespace) является фундаментальной концепцией C++. Пространство имен - это группа имен, в которой имена не совпадают. Исключением являются имена перегружаемых функций и переменные с различными областями действия. Имена в различных пространствах имен не конфликтуют. Например, можно использовать имя my_adress в двух различных классах (это же относится к структурам и объединениям). C++ также позволяет определить пространства имен при помощи ключевого слова namespace . Такие пространства имен вводятся для снижения вероятности конфликта имен и полезны в случае использования имен из нескольких различных библиотек.
Спецификатор namespace определяет пространство имен функций, классов и переменных, находящихся в отдельной области видимости. Вообще namespace определяет группу символов, представляющих собой названия или имена функций, классов или переменных. Примером такого определения области видимости является область видимости членов класса.
Синтаксис:
namespace name { // объявления и определения имен }
Аргументом спецификатора namespace (то есть name ) является идентификатор пространства имен.
Чтобы обратиться к чему-нибудь в пространстве namespace_name, достаточно обратиться непосредственно к члену, используя оператор определения области видимости (::): namespace_name::member .
Однако, при многократном использовании члена member такая запись становится слишком громоздкой. Инструкция using заставляет компилятор признавать дальнейшее использование этому члену пространства namespace_name без дополнительного определения имени этого пространства:
using namespace_name::member;
Инструкция using namespace заставляет компилятор признавать все члены пространства имен namespace_name:
using namespace namespace_name;
В приведенном ниже примере глобальные переменные Cary и Hugh объявляются в пространстве имен grants:
namespace grants { int Cary = 0; int Hugh = 1; }
Вне пространства имен grants переменные могут быть упомянуты как grants::Cary и grants::Hugh. Но можно сократить эту запись, если добавить такую инструкцию:
using grants::Cary;
Теперь имя Cary может использоваться в последующих инструкциях без дополнительного определения. Чтобы иметь возможность ссылаться на обе переменные без определения имени пространства, достаточно написать
using namespace grants;
Операторы приведения типов (cast operators) позволяют изменять тип выражения: они вычисляют значение выражения, изменяют его тип и присваивают значение нового типа результату. В некоторых случаях это может привести к изменению внутреннего формата данных (изменению вида данных в памяти компьютера). Ниже приведены основные операторы приведения типов и способы их использования.
Операторы приведения типов языка C поддерживают все эти операции, однако синтаксис является другим. Оператор приведения dynamic_cast является новым элементом ANSI C++.
Таким образом, ANSI C++ предоставляет четыре разных оператора приведения, каждый из которых обладает специфическими возможностями. Операторы C оставлены в C++ для обеспечения преемственности.
Язык C и ранние версии C++ предоставляют всего один оператор приведения типов практически для всех ситуаций. В ANSI C++ этот важный оператор приведения также сохранен. Следующее выражение имеет то же значение, что и выражение expr, но оно приведено к типу, определенному в скобках:
(type) expr
Хотя выражение (type) expr теоретически имеет то же значение, что и выражение expr , действие оператора приведения типа может привести к преобразованию данных, которые изменят внутреннее представление числа в памяти.
Оператор приведения типа старого стиля также можно определить, используя альтернативный синтаксис:
type(expr)
Приведенный ниже пример позволяет вывести на печать текущее значение целого i в формате с десятичной точкой:
int i = 11; cout << (double) i;
В целом существует очень мало ситуаций, когда оператор приведения типов может как-то повлиять на поведение программы. Одна из таких ситуаций показана в следующем примере. Присваивая значения и вызывая функции, компилятор автоматически переводит целые числа в действительные, поэтому в таком случае явное выполнение приведения типов не нужно.
Операторы приведения типов языка C можно использовать во всех ситуациях, в которых используются операторы приведения типов ANSI C++ за исключением dynamic_cast. Два следующих оператора выполняют одни и те же действия:
char *p = (char *) malloc(n); char *p = reinterpret_cast<char *>(malloc(n));
Следующая пара операторов, которые подавляют предупреждение компилятора, также выполняют одно и те же действие:
bool flag1 = (bool) 12; bool flag1 = static_cast<bool>(12);
Оператор приведения типа const_cast предусмотрен для облегчения вызова функций, которые должны использовать модификатор const, но не используют его. Такие функции работают с параметром типа указатель и никогда не изменяют данные, на которые ссылается этот указатель, даже если программист не определил параметр как const. Правила C++ запрещают передачу константного указателя в качестве фактического параметра такой функции, оператор const_cast предоставляет возможность обойти этот запрет.
Следующее выражение имеет то же значение, что и expr. Обозначенный тип type должен быть таким же, что и тип expr, кроме отсутствия модификатора const или volatile. Как правило, type - это тип указателя:
const_cast<type>(expr)
В следующем примере функция display_num использует в качестве параметра указатель, но не данные, на которые он указывает (*p):
void display_num(double *p) { printf("The value is %6.3f\n", *p); }
Так как заданные посредством указателя p данные не изменяются, должна быть возможность передачи в качестве параметра const-указателя. Правила C++ запрещают это из-за несоответствия типов указателей:
const double x = 11.2; display_num(&x); // ошибка
Чтобы обойти этот запрет, нужно лишить указатель модификатора const. Следующая операция позволяет устранить запрет:
const double x = 11.2; display_num(const_cast<double *>(&x));
При использовании оператора const_cast необходимо быть уверенным, что данные, на которые указывает указатель, не изменяются. При задании оператора const_cast и одновременной попытке изменить данные результаты выполнения программы будут непредсказуемы.
Оператор dynamic_cast предоставляет возможность проверить, что указатель базового класса указывает на объект определенного производного класса. Оператор dynamic_cast проверяет тип объекта во время выполнения программы, используя информацию RTTI.
Следующее выражение должно возвращать ссылку на заданный тип type. Во время выполнения задачи объект, указанный в аргументе expr, должен иметь определенный тип type или тип, производный от типа type, иначе приведение типа не выполняется и возвращается нулевой указатель:
dynamic_cast<type *>(expr)
Оператор dynamic_cast также можно использовать со ссылочными типами. Если приведение типа не выполняется из-за того, что expr имеет тип , не совпадающий с типом type или производным от него, то вырабатывается исключение bad_cast .
Для этого оператора приведения типов существует единственное ограничение: при использовании его для приведения к указателю на производный класс (что является его главным назначением), класс выражения expr должен иметь, по крайней мере, одну виртуальную функцию. По этой причине оператор dynamic_cast часто называется оператором полиморфного приведения типа (polymorphic cast). Также возможно применение оператора dynamic_cast для приведения к указателю базового класса без каких-либо ограничений. Если типы не связаны вообще, компилятор не допускает преобразования.
Наиболее полезным в операторе dynamic_cast является то, что указатель базового класса может указывать на производные классы. Некоторые из этих классов поддерживают функции, которых нет в других классах.
Например, следующий фрагмент программы определяет базовый класс B и производный класс D, а затем присваивает его адрес указателю базового класса:
class B { public: virtual void func1(int); }; class D { public: void func2(void); }; // ... D od; B *pb = &od;
В последней строке этого примера указателю на класс B присваивается адрес объекта производного класса D.
Следующая функция использует оператор dynamic_cast, чтобы проверить, действительно ли параметр типа B * указывает на объект класса D. Если это так, то вызывается функция func2, которая определена только в классе D и классах, производных от D:
void process_B(B *arg) { D *pd; pd = dynamic_cast<D *>(arg); if (pd) pd->func2(void); // ... }
Если arg указывает на объект класса D или класса, производного от D, то преобразование будет выполнено, и функция process_B вызовет функцию D::func2 через указатель pd. Иначе операция преобразования не выполняется и pd присваивается значение NULL.
Оператор reinterpret_cast используется для приведения указателя к другому типу. Новый тип не обязательно должен быть связан со старым, этот же оператор позволяет приводить типы между указателями и целыми числами.
Следующий оператор возвращает то же значение выражения, что и выражение expr, но приведенное к заданному типу type. Этот тип, который обычно является типом указателя, может отличаться от типа выражения expr только интерпретацией (способом использования). Сами данные и их внутреннее представление в памяти при этом не изменяются.
reinterpret_cast<type>(expr)
Этот оператор нельзя использовать для удаления модификаторов const и volatile из выражения expr, так как это могло бы привести к возможности приведения типов с помощью оператора reinterpret_cast между указателями, а также между указателями и целыми числами без каких-либо ограничений.
Вообще оператор reinterpret_cast никогда не изменяет внутреннее представление данных, на которые ссылается указатель, определяемый выражением expr . Тем не менее, когда тип указателя и тип данных, на которые он указывает, не соответствуют друг другу, результат может быть просто драматичен.
Наиболее часто этот оператор используется для преобразования значения, возвращаемого как void *, к более конкретному типу. В C++ нельзя присвоить указатель, объявленный как void*, указателю другого типа без приведения типа. Например,
char *p = reinterpret_cast<char *>(malloc(100));
Хотя возвращенное значение функции malloc не обязательно переводить в другой тип указателя немедленно, такое приведение должно быть выполнено перед первым обращением к выделенной области памяти. Одним из преимуществ оператора new перед функцией malloc является отсутствие необходимости приведения типов.
Оператор reinterpret_cast может быть также полезен при определении функции, когда функция получает указатель типа void * в качестве параметра.
Оператор static_cast - это оператор приведения типов между родственными объектами или типами указателей. Участвующие в этой операции классы должны быть связаны через наследственность, конструктор или функцию преобразования. Оператор static_cast также работает с простейшими числовыми типами данных.
Следующее выражение имеет то же значение, что и выражение expr, но позволяет переводить его в тип type:
static_cast<type>(expr)
Хотя численное значение результата не изменяется, при выполнении этого оператора может быть изменено внутреннее представление данных.
Оператор static_cast используется в следующих случаях:
В принципе, можно использовать оператор static_cast всегда, когда допускается автоматическое преобразование в обратном направлении. Например, целые числа автоматически переводятся в действительные. Чтобы выполнить обратное преобразование, требуется оператор static_cast .
В основном оператор static_cast используется для приведения сложных типов к простым: при работе с простыми типами он подавляет предупреждение компилятора:
long j = 17; short i = static_cast<short>(j);
Этим как бы говорится: "Да, я на самом деле хочу это сделать". При этом компилятор не выдает предупреждения о возможной потере данных. При этом необходимо быть уверенным в том, что данные не будут слишком велики для приведения в другой тип.
Таким же образом возможно преобразование типов из указателя базового класса в производный без каких-либо ограничений. Но так как никакой проверки во время выполнения программы не делается, то забота о поддержке данных в должном виде возлагается на программиста (производный класс содержит все члены базового плюс члены производного класса). Например, в следующем примере B является базовым классом класса D:
B *pb; // ... D *pd = static_cast<D *>(pb);
В данном случае объект, на который указывает pb, фактически имеет тип D или производный от него. Однако при этом значения всех членов класса D, не входящих в класс B, не определены, и обращение к ним приведет к возникновению ошибок. В такой ситуации программист сам должен определить все недостающие члены с помощью присваивания. Можно пойти и в обратном направлении - выполнить присваивание указателю на базовый класс. В этом случае два следующих оператора эквивалентны.
pb = static_cast<D *>(pd); pb = pd;
Другие случаи, в которых оператор static_cast может быть полезен, редко встречаются в программах. Например, с его помощью можно последовательно выполнить несколько преобразований типов данных. В следующем примере A и B - такие классы, в которых A имеет функцию преобразования в B, а B - функцию преобразования к типу int. Тогда объект типа A может быть преобразован в тип int следующим образом:
A oa; int i = static_cast<int>(static_cast<B>(oa));
Без использования этой цепочки преобразований (и при отсутствии функции преобразования A в тип int) преобразование oa в целое было бы невозможным.
На первый взгляд кажется, что при использовании оператора static_cast программисту придется выполнять больше работы, чем при программировании старыми методами. Однако при этом становятся доступными возможности ANSI C++ по разделению операторов приведения типа, что значительно облегчает просмотр больших программ.
Оператор static_cast также полезен при применении различных форматов данных или при необходимости вывода данных в формате, отличном от того, который определен для данного типа по умолчанию. Например, нужно вывести целое число i в формате с плавающей точкой:
int i = 25; cout << static_cast<double>(i);
Заметим, что для вывода на экран адреса строки надо использовать оператор reinterpret_cast, так как имя строки имеет тип указателя, а не простейший тип:
char str[] = "Hello"; cout << hex << reinterpret_cast<int>(str);Предыдущая Оглавление Следующая