Учебное пособие для студентов Авторы А. Н. Вальвачев, К. А. Сурков, Д. А. Сурков, Ю. М. Четырько Содержание Содержание 1
Скачать 2.61 Mb.
|
AutoPtr()3.1. Виртуальные методы В С++ виртуальные методы определяются при помощи ключевого слова virtual: class TTextReader: virtual public TObject { ... }; При перекрытии виртуального метода ключевое слово virtual можно записать, а можно и опустить. Синтаксис перекрытия виртуальных методов не предусматривает такие проблемы, как версионность и рефакторинг кода (упрощение программного кода с сохранением функциональности). Если метод виртуальный следует всегда писать ключевое слово virtual. 3.2. Абстрактные классы В С++ абстрактный класс объявляется следующим образом: class TTextReader { protected: virtual void NextLine() = 0; ... }; Такой метод называется абстрактным и класс, содержащий данный метод, тоже называется абстрактным. Виртуальные методы следует объявлять в секции protected. 3.3. Подстановочные функции В С существует способ оптимизировать вызов функций с помощью макросов (#define). В С++ использование этого ключевого слово должно быть минимизировано, так как макросы обрабатываются препроцессором и в большинстве компиляторов символические имена макросов не видны в отладчике. Кроме того, макросы сильно затрудняют отладку программы, так как отладить макрос невозможно. Вместо макросов в С++ используются подстановочные функции. Они определяются с помощью ключевого слова inline: inline int Min(int x, int y) { if (x < y) return x; else return y; }; Тело подстановочной функции в большинстве случаев подставляется в код программы вместо ее вызова. Она должна быть целиком определена в h-файле. Типичной ошибкой программиста является размещение этой функции в cpp-файле и вынос ее прототипа в h-файл. Если записана директива inline, это еще не означает, что компилятор подставляет тело функции в место ее вызова — он сам решает, что будет более удобным в данном случае. 3.4. Операторы преобразования типа Существует четыре оператора преобразования типа в С++: reinterpret_cast<тип>(переменная) static_cast<тип>(переменная) const_cast<тип>(переменная) dynamic_cast<тип>(переменная) Первый оператор (reinterpret_cast) позволяет отключить контроль типов данных на уровне компилятора, с помощью него любой указатель может быть интерпретирован, как любой другой указатель, а также любая память или переменная может быть интерпретирована иначе. В программах этот оператор преобразования типа использовать не следует, так как он нарушает переносимость программ. Его наличие свидетельствует о том, что программа является кросс-платформенной. Пример использования: int i; char *p; p = reinterpret_cast Второй оператор (static_cast) используется вместо преобразования тип(переменная), (тип)переменная и (тип)(переменная) при работе с классами, структурами и указателями на них. Оператор static_cast был задуман по причине того, что в С++ выражение тип(переменная) может оказаться вызовом конструктора. Если в программе требуется преобразовать тип, а не вызвать конструктор типа, используется данный оператор. Кроме того, оператор (тип)переменная или (тип)(переменная) может в некоторых случаях оказаться преобразованием reinterpret_cast<тип>(переменная), а при разработке кросс-платформенных программ оператор reinterpret_cast всегда содержит потенциальную опасность неправильной работы программы на другой платформе. Поэтому вместо операторов тип(переменная), (тип)переменная и (тип)(переменная) следует использовать операторы reinterpret_cast и static_cast, которые убирают не явность из преобразования. Так как оператор static_cast является громоздким, то для простых типов данных допустимо использование форм: (тип)переменная и (тип)(переменная). Форма тип(переменная) не должна использоваться для преобразования типа. Третий оператор (const_cast) используется для приведения не константных указателей к константным и наоборот: void f2(char *s); void f1(const char *s) { ... f1(const char *s); ... f2(const_cast ... }; При объявлении переменных и параметров функций в описании типа может быть указано ключевое слово const. Объявление f(const char *s); означает, что символы, адресуемые указателем s, изменять нельзя. Объявление f(char const *s) означает, что указатель s изменять нельзя. Так же можно сделать объявление: f(const char const *s), которое будет означать, что ни указатель, ни переменную изменять нельзя. Если в программе объект объявлен с помощью модификатора const, то у него можно вызывать лишь те методы, которые объявлены с этим же модификатором: class TTextReader { public: int ItemCount() const; ... }; Наличие константных объектов порождает проблему — огромная избыточность программного кода. Заранее программист не знает, будет ли пользователь (другой программист) его класса создавать константные объекты. Вследствие того, что это не исключено, программист начинает записывать слово const в объявление всех методов, в которых его можно записать. Многие методы являются виртуальными или вызывают виртуальные методы. Случается так, что в производных классах виртуальные методы, вызванные константными методами, модифицируют поля объектов (это требуется по условию задачи). Это приводит к логической проблеме, которая решается либо за счет применения оператора const_cast к указателю this в производных классах, либо за счет объявления полей в производных классах с модификатором mutable (записывается при описании полей класса в том случае, если они должны модифицироваться константными методами). Пример: mutable int m_RefCount; Так же решить проблему можно при помощи перегрузки метода класса без модификатора const: class TTextReader { public: int ItemCount() const; int ItemCount(); int ItemCount() const volatile; int ItemCount() volatile; ... }; Варианты объявления: volatile TTextReader r; const volatile TTextReader r; const TTextReader r; TTextReader r; Ключевое слово volatile запрещает кэшировать значение переменной. Если в программе происходит считывание значения переменной, объявленной с этим ключевым словом, то значение считывается из памяти, а не из регистров, а запись всегда производится в память, в которой размещается данная переменная. Если ключевое слово volatile не указано, то оптимизатор C++ имеет право выполнять регистровые операции (оптимизации) при чтении и записи переменных, а так же размещать их в регистрах. Четвертый оператор (dynamic_cast) соответствует оператору as в Delphi. Для работы этого оператора нужно в опциях компилятора включить опцию RTTI. Если это выполнено, то оператор dynamic_cast работает, как static_cast. Оператор dynamic_cast работает по-разному в зависимости от того, применяется он к ссылке на объект (&) или указателю на объект (*). Если оператор применяется к ссылке на объект, то преобразование не может быть выполнено и возникает исключительная ситуация. Если он применяется к указателю на объект и преобразование не может быть выполнено, оператор возвращает NULL. 3.5. Размещающий оператор new Обычно оператор new размещает объекты в динамической памяти (heap). TDelimitedReader *R = new TDelimitedReader(); В данном случае оператор new имеет следующий вид: void *operator new(size_t size); Существует вид оператора new, который позволяет расположить объект по заданному адресу: TDelimitedReader *R = new (Buffer)TDelimitedReader(); В этом же случае оператор new имеет следующий вид: void *operator new(size_t size, void *p) { return p; } Внимание! Комбинировать различные способы выделения и освобождения памяти не рекомендуется. 3.6. Ссылки Ссылка является альтернативным именем объекта и объявляется следующим образом: int i; int &r = i; Использование ссылки r эквивалентно использованию переменной i. Основное применение ссылок — передача параметров в функцию и возврат значения. В случае, когда ссылка используется в качестве параметра функции, она объявляется неинициализированной: void f(int &i); Во всех остальных случаях ссылка должна инициализироваться при объявлении, как показано ранее. Если ссылка является полем класса, она должна инициализироваться в конструкторе класса в списке инициализации до тела конструктора. При использовании в качестве параметров функций ссылки соответствуют var-параметрам в языке Delphi: procedure P(var I: Integer) begin ... end; Константные ссылки соответствуют const-параметрам в языке Delphi: procedure P(const I: Integer) begin ... end; При передаче ссылочного параметра в стек заносится адрес переменной, а не ее копия. Ссылку следует рассматривать, как псевдоним переменной, которой она инициализирована. Ссылки отличаются от указателей тем, что позволяют компилятору лучше оптимизировать программный код. Для возврата значений из процедур (через параметры) предпочтение следует отдавать указателям, а не ссылкам. Ссылки следует использовать лишь в тех случаях, когда известно, что возвращаемый объект должен создаваться не в динамической памяти, а на стеке, то есть ссылки применяют при возврате value-type-объектов. При работе со ссылками существует типовая ошибка — возврат через ссылку переменной, созданной на стеке. Пример ошибочной записи: void f(int *p) { int i; p = &i; } Следующая запись тоже будет ошибочной: void f(int &r) { int i; r = i; } Следующий пример тоже ошибочен, так как нельзя возвращать адрес объекта, созданного на стеке: std::string& GetName(Object* Obj) { const char* str = Obj->GetName(); return std::string(str); } 3.7. Обработка исключительных ситуаций В С++ отсутствует аналог блока try…finally…end. На платформе Windows благодаря структурной обработке ОС существуют следующий блок: __try { ... } __finally { ... } Но следует отметить, что для переносимых программ он не подходит. В С++ существует аналог блока try…except…end: try { ... } catch(std::ios_base::failure) { ... } catch(std::exception) { ... } catch(...) { ... } Распознавание исключительных ситуаций происходит последовательно блоками catch, поэтому их последовательность должна быть от частного к общему. Последний блок catch в примере выше ловит любую исключительную ситуацию. Создание исключительных ситуаций выполняется с помощью оператора throw (аналог raise в Delphi): throw std::exception("Ошибка"); Внутри блока catch оператор throw возобновляет исключительную ситуацию, как и raise в Delphi. При создании исключительной ситуации при помощи оператора throw объект, описывающий исключительную ситуацию, может быть создан в динамической памяти: throw new std::exception("Ошибка"); Если применяется такой способ создания исключительной ситуации, ее уничтожение должно происходить следующим образом: try { ... throw new std::exception("Ошибка"); } catch(std::exception *e) { delete e; } catch(...) { ... } Если же записать так: try { ... throw new std::exception("Ошибка"); } catch(...) { ... } то возникнет утечка ресурсов из-за того, что объект std::exception, созданный в динамической памяти, не будет освобожден. В С++ отсутствует общий базовый класс для исключительных ситуаций, поэтому на верхнем уровне работы программы нужно отлавливать все возможные базовые классы исключительных ситуаций. Это является препятствием на пути построения расширяемых систем. Program Program 3.8. Защита ресурсов от утечки Поскольку в С+ отсутствует блок try…finally, его приходится эмулировать. Object *p = new Object(); try { ... } catch(...) { delete p; throw; } delete p; Данный код эквивалентен следующему: Object *p = new Object(); __try { ... } __finally { delete p; } за исключение того, что второй пример не является переносимым. Согласно стандарту С++ в деструкторах и операторах delete не должно быть исключительных ситуаций, если же исключительная ситуация произошла, то поведение программы не определено. Если исключительная ситуация происходит в конструкторе объекта, объект считается не созданным и деструктор для этого объекта не вызывается, но память, выделенная для объекта, освобождается. Если внутри объекта агрегированы другие объекты, то вызываются деструкторы лишь для тех объектов, которые были полностью созданы к моменту возникновения исключительной ситуации. Если объект создается в динамической памяти и освобождается в той же самой процедуре, то для защиты от утечки ресурсов можно применять оболочечные объекты — wrapper (содержит указатель на динамический объект, который уничтожается в деструкторе оболочечного объекта). Оболочечный элемент создается на стеке, поэтому его деструктор вызывается автоматически, гарантируя тем самым уничтожение агрегированного динамического объекта. Такие оболочечные объекты в библиотеках программирования называются AutoPtr, SafePtr и т.д. class AutoPtr { public: AutoPtr(int *arr); |