
- •1. Понятие модуля. Принципы модульного программирования. Понятие объекта как динамического модуля.
- •2. Понятие класса. Понятие метода. Представление метода в виде обычной процедуры. Понятие конструктора и деструктора.
- •3. Понятие свойства. Методы получения и установки значений свойств. Свойства-массивы (в некоторых языках программирования). Индексаторы (в некоторых языках программирования).
- •Информация о типе времени выполнения программы
- •5. Классы в программных модулях. Атрибуты доступа к элементам объектов. Термин «инкапсуляция».
- •Термин «инкапсуляция»
- •Virtual – в базовом классе
- •7. Понятие ссылки на метод объекта (или делегата – в зависимости от языка программирования). Понятие события. Применение ссылок на методы для расширения объектов.
- •8. Понятие метакласса (в некоторых языках программирования). Методы, применяемые к классам. Виртуальные конструкторы (в некоторых языках).
- •Понятие метакласса (в некоторых языках программирования)
- •Виртуальные конструкторы (в некоторых языках)
- •3) Finally – вызвать free.
- •Если глобально-уникальный идентификатор назначается интерфейсу, то он записывается после ключевого слова interface и заключается в квадратные скобки, например:
- •Microsoft Visual Studio
- •Import Network;
- •Var Stat: SocketStat;
- •Var SocketStatCollection: … ;
- •Конструкторы и деструкторы
- •Стандартные конструкторы
- •Создание объектов по значению (на стеке) и по ссылке (в динамической памяти)
- •Операторы new и delete
- •Размещающий оператор new
- •Порядок конструирования и разрушения объектов
- •Вложенные определения классов
- •«Друзья» класса
- •Статические члены класса
- •19. Перегрузка бинарных операторов. Перегрузка унарных операторов. Перегрузка операторов преобразования типа.
- •Индексаторы
- •Механизм вызова событий
- •Создание пользовательских обобщенных коллекций
- •Создание обобщенных интерфейсов
- •Несколько слов о вложенных делегатах
- •25. Понятие итератора в языке c#. Оператор foreach. Оператор yield.
- •И напоследок... Блок finally
- •26. Понятие атрибутов в языке c#. Создание пользовательских атрибутов. Анализ атрибутов во время выполнения программы. Понятие рефлексии (reflection) в языке c#. Сериализация объектов.
- •Что такое метаданные и зачем они нужны?
- •1. Метаданные в .Net обязательны и универсальны.
- •2. Метаданные в .Net общедоступны.
- •3. Метаданные в .Net исчерпывающи.
- •4. Метаданные в .Net расширяемы.
- •5. Метаданные в .Net конструируемы программно.
- •Получение экземпляра класса Type
- •Динамическая загрузка сборок
- •Динамическая загрузка типов
- •Исследование типа
- •Характеристики типа как целого
- •Члены класса
- •Исследование объекта
- •Динамическое создание объекта и вызов методов
- •Создание объекта по его типу
- •Декларативное программирование
- •Новые механизмы абстракции?
- •Динамическое создание типов
- •Роль графов объектов
- •Formatter сериализации
- •XmlSerializer
- •Интерфейсы iFormatter и iRemotingFormatter
- •Точность типов среди форматеров
- •28*. Ооп в языке программирования Smalltalk. Достоинства и недостатки этого языка в сравнениии с языком программирования c#.
Вложенные определения классов
В С++ внутри класса можно определить другой класс:
class TTextReader
{
public:
class TItems
{
...
};
...
};
Эта запись по смыслу соответствует следующей записи:
class TTextReader::TItems
{
...
};
Class TTextReader
{
friend class TTextReader::TItems;
};
Таким образом, определения классов и других типов данных внутри класса означает использование имени внешнего класса как пространства имен.
«Друзья» класса
Для того, чтобы объекты некоторого класса могли получить доступ в private и protected полям другого класса, используется оператор friend, который разрешает доступ ко всем записям и методам класса для того класса, который указан в операторе. Данный оператор используется внутри класса, с его помощью нельзя разрешать доступ к членам класса извне, иначе это нарушит принцип сокрытия данных.
Друзья не могут быть виртуальными функциями, поскольку они не являются членами класса, а виртуальными функциями могут быть только члены.
Поскольку дружественная функция фактически не является членом класса, то она не наследуется. Однако вам может понадобиться, чтобы друг производного класса использовал друга базового класса. Способ осуществления этого предполагает приведение типа ссылки или указателя производного клаcca к эквиваленту базового класса и последующее применение нового указателя или ссылки для обращения к другу базового класса:
Статические члены класса
Поля и методы класса могут быть объявлены при помощи слова static:
class TTextReader
{
public:
...
static char*ClassName();
...
private:
static int m_ObjectCount;
...
};
По смыслу данный код эквивалентен следующему:
class TTextReader
{
friend char* TTextReader::ClassName();
...
};
сlass TTextReader::ClassName()
{
...
};
int TTextReader::m_ObjectCount;
Если поле объявлено с ключевым словом static, то это – обычная глобальная переменная, для которой имя класса используется как пространство имен. Если метод объявляется с этим словом, то это – обычная глобальная функция, которая является другом класса. Такая функция не имеет псевдо-параметра this.
15. Множественное наследование. Проблема повторяющихся базовых классов. Замена множественного наследования наследованием от интерфейсов в других языках объектно-ориентированного программирования. Типовой пример применения множественного наследования — «наблюдатель» (observer).
Множественное наследование
У одного класса может быть несколько базовых классов:
class TDelimitedReader : public TTextReader, public TStringList
{
...
};
Объект класса TDelimitedReader содержит все поля и методы базовых классов TTextReader и StringList. При этом в классе TDelimitedReader можно переопределять виртуальные методы каждого базового класса.
Множественное наследование имеет ряд проблем:
отсутствие эффективной реализации (неэффективность скрыта от программиста);
неоднозначность, возникающая из-за того, что в базовых классах могут быть одноименные поля, а также методы с одинаковой сигнатурой;
повторяющийся базовый класс в иерархии классов.
Неоднозначность при множественном наследовании:
class TTextReader
{
virtual void NextLine();
...
};
class TStringList
{
public:
virtual void NextLine();
...
};
class TDelimitedReader: public TTextReader, public TStringList
{
...
};
TDelimitedReader *Reader;
Reader->NextLine(); // Ошибка. Неоднозначность.
Неоднозначность возникает потому, что в классе TDelimitedReader существуют две таблицы виртуальных методов и неизвестно, к какой из них надо обращаться за методом NextLine().
Поэтому последний оператор должен быть скорректирован на следующий:
Reader->TTextReader::NextLine(); или Reader->TStringList::NextLine();
В С++ для классов поддерживается столько таблиц виртуальных методов, сколько у него базовых классов. При перекрытии общего виртуального метода, существующего в нескольких базовых классах, происходит замещение адреса во всех таблицах виртуальных методов. Перегрузка функций по типам аргументов не приводит к разрешению неоднозначности. Если функция NextLine() была объявлена с различной сигнатурой в различных классах, то неоднозначность тоже остается. В некоторых случаях наличие в базовых классах функций с одинаковыми именами (но различным количеством параметров или различными типами параметров) является преднамеренным решением. Чтобы в производном классе открыть нужную функцию нужного базового класса, применяется оператор using:
class TTextReader
{
public:
virtual void NextLine();
...
};
class TStringList
{
public:
virtual void NextLine(int);
...
};
class TDelimitedReader : public TTextReader, public TStringList
{
public:
using TStringList::NextLine;
virtual void NextLine(int);
...
};
Проблема повторяющихся базовых классов
Классы TStringList и TTextReader в нашем примере могут иметь одинаковый базовый класс, например TObject. В этом случае объект класса TDelimitedReader имеет две копии полей класса TObject. Из-за дублирования полей возникает неоднозначность при обращении к полю класса TObject из метода класса TDelimitedReader. Проблема решается с помощью уточненного имени:
TTextReader::m_Field;
TStringList::m_Field;
Однако главная проблема состоит в том, что одна сущность дублируется внутри базового класса. На практике это обычно не требуется.
Такой результат достигается при применении виртуальных базовых классов:
class TDelimitedReader: public TTextReader, public TStringList
{
...
};
class TTextReader: public TObject
{
...
};
class TStringList: virtual public TObject
{
...
};
Обычное наследование соответствует агрегации всех полей базового класса. Виртуальное наследование соответствует агрегации ссылки на поля базового класса.
Если же при объявлении класса TTextReader мы запишем следующее:
class TTextReader: virtual public TObject
{
...
};
то структура полей будет такой:
Таким образом, множественное наследование таит следующую проблему: заранее неизвестно от каких классов программист захочет унаследовать свой класс. Однако при создании класса использовать виртуальное наследование неэффективно, если наследуются поля, так как доступ к полям всегда будет осуществляться через дополнительный указатель.
Замена множественного наследования наследованием от интерфейсов в других языках объектно-ориентированного программирования
Интерфейсы являются заменой множественному наследованию.
Вывод: одинарное наследование в стиле Java, C++, Delphi допустимо только от классов, множественное – от интерфейсов. Иначе можно осуществлять множественное наследование лишь от классов, в которых отсутствуют поля.
Типовой пример применения множественного наследования — «наблюдатель» (observer)
16. Виртуальные методы в языке C++. Недостатки синтаксиса определения и перекрытия виртуальных методов в языке C++. Понятие константного метода. Проблемы, порождаемые наличием константных методов. Операторы приведения типа в языке C++: const_cast, reinterpret_cast, static_cast, dynam-ic_cast.
Виртуальные методы в языке C++
В С++ виртуальные методы определяются при помощи ключевого слова virtualю.
При перекрытии виртуального метода ключевое слово virtual можно записать, а можно и опустить. Синтаксис перекрытия виртуальных методов не предусматривает такие проблемы, как версионность и рефакторинг кода (упрощение программного кода с сохранением функциональности). Если метод виртуальный следует всегда писать ключевое слово virtual.
Недостатки синтаксиса определения и перекрытия виртуальных методов в языке C++
Таблица виртуальных функций хранит адреса виртуальных функций, объявленных для объектов данного класса. Например, объект базового класса содержит указатель на таблицу адресов всех виртуальных функций для этого класса. Объект производного класса содержит указатель на отдельную таблицу адресов. Если производный класс дает новое определение виртуальной функции, то в таблице виртуальных функций сохраняется адрес новой функции. Если же производный класс не переопределяет виртуальную функцию, таблица виртуальных функций хранит адрес исходной версии функции. Если производный класс определяет новую функцию и объявляет ее виртуальной, ее адрес добавляется в таблицу виртуальных функций.
При вызове виртуальной функции программа просматривает адрес таблицы виртуальных функций, хранящийся в объекте, и переходит к соответствующей таблице адресов функций. Например, если вызывается третья виртуальная функция в объявлении класса, программа запускает функцию, адрес которой хранится в третьем элементе массива.
Кратко можно сказать, что использование виртуальных функций влечет за собой следующие умеренные затраты памяти и скорости:
• Каждый объект имеет свой размер, который увеличивается на значение, необходимое для хранения адреса.
• Для каждого класса компилятор создает таблицу (массив) адресов виртуальных функций.
• При каждом вызове функции выполняется дополнительный шаг для нахождения адреса в таблице.
Хотя невиртуальные функции более эффективны, чем виртуальные, они не предусматривают динамического связывания.
• Если в базовом классе начать объявление метода класса с ключевого слова virtual, то функция становится виртуальной для базового класса и для всех классов, производных от данного, включая вложенные производные классы.
• Если виртуальный метод вызывает через ссылку или указатель на объект, то программа использует метод, определенный для типа объекта, а не для типа указателя или ссылки. Этот процесс называется динамическим, или поздним, связыванием.
• Если вы определяете класс, который будет использоваться в качестве базового для наследования, то вы должны объявить виртуальными те методы класса, которые могут быть переопределены в производных классах.
Конструкторы не могут быть виртуальными. Создание объекта производного класса актиизирует конструктор производного, а не базового класса. Конструктор производного класса затем использует конструктор базового класса, однако, эта последовательность явно вытекает из механизма наследования. Таким образом, производный класс не наследует конструкторы базового класса, поэтому нет смысла делать их виртуальными.
Деструкторы должны быть виртуальными, за исключением тех классов, которые не будут использоваться в качестве базовых. Например, предположим, что Employee – это базовый класс, а Singer – производный класс, добавляющий член char * , который указывает на память, выделенную операцией new. Затем, когда объект Singer завершает свою работу, необходимо вызвать деструктор Singer () для того, чтобы освободить эту память.
Employee *ре = new Singer;
delete ре;
Если применяется стандартное статическое связывание, то операция delete активизирует деструктор Employee() . При этом освобождается память, на которую ссылаются компоненты Employee объекта Singer, но не память, на которую указывают новые члены класса. При этом если деструкторы виртуальные, то тот же самый код активизирует деструктор Singer(), освобождающий память, на которую указывает компонент Singer, а затем вызывает деструктор Employee() для освобождения памяти, на которую указывает компонент Employee.
Даже если базовый класс не требует явного деструктора, вы должны предусмотреть виртуальный деструктор, даже если он ничего не будет делать:
virtual BaseClass() { }
Друзья не могут быть виртуальными функциями, поскольку они не являются членами класса, а виртуальными функциями могут быть только члены. Если это вызывает проблему при разработке, то вы можете обойти ее за счет использования виртуальных методов внутри дружественных функций.
Если в производном классе не происходит переопределение функции (виртуальной или нет), то класс будет использовать версию функции базового класса. Если класс является частью длинной цепочки порождений, то будет использоваться самая последняя версия функции. Исключение составляет случай, когда базовая версия скрыта, как описано ниже.
Переопределение скрывает методы
Предположим, что вы создали нечто наподобие:
class Dwelling
{
public :
virtual void showperks (int а) const;
} ;
class Hovel : public Dwelling
public:
virtual void showperks() const;
} ;
Это вызывает проблему. Вы можете получить предупреждение компилятора вроде следующего:
Hovel::showperks (void) hides Dwelling :: showperks (int)
Hovel trurnp ;
trurnp .showperks() ; //true
trurnp .showperks(S) ; //false
Новое определение создает функцию showperks() , которая не принимает аргументов. Вместо того чтобы получить результат в виде двух перегруженных версий функции, это переопределение скрывает версию базового класса, в которой был аргумент int. Другими словами, переопределение унаследованных методов не является разновидностью перегрузки. Если вы переопределяете функцию в производнам классе, то при этом происходит не просто перегрузка объявления базового класса с той же самой сигнатурой функции. Вместо этого скрываются все методы базового класса с тем же именем вне зависимости от сигнатур аргументов.
Этот факт приводит к нескольким практическим правилам. Во-первых, если вы переопределяете унаследованный метод, необходимо убедиться в точном совпадении с исходным прототипом. Одно сравнительно новое исключение из этого правила состоит в том, что возвращаемый тип (указатель или ссылка на базовый класс) может быть заменен указателем или ссылкой на производный класс. Это свойство назыется изменчивостью возвращаемого типа, поскольку возвращаемый тип допускается изменять параллельно с типом класса.
Во-вторых, если объявление класса перегружается, вам необходимо переопределить все версии функции базового класса в производном классе. Если вы переопределяете только одну версию, то две остальных становятся скрытыми и не могут использоваться объектами производиого класса. Обратите внимание, что если не нужны никакие изменения, то переопределение может просто вызывать версию базового класса.
Понятие константного метода
Константные объекты – это те, которые запрещено менять. Исполняющая система может размещать константные объекты в страницах физической памяти, защищенных от записи. В многозадачной системе, если запущено несколько одинаковых процессов, то страницы физической памяти, защищенные от записи, существуют в единственном экземпляре и разделяются всеми процессами, т.е. являются для них общими. Таким образом достигается существенная экономия памяти. Современный C и, конечно, C++ позволяет средствами языка разделить константные и не константные объекты.
Адрес константного объекта можно помещать лишь в указатель на константный объект. Проиллюстрируем на примере, как в C и C++ описывается указатель на константу:
const int *p;
Описан указатель на константный объект типа int. Отметим, что сам указатель не является константой, ему можно присвоить значение.
Если мы хотим определить константный указатель на неконстантный бъект, надо использовать следующую запись:
int x;
int const * p = &x;
Такой указатель надо инициализировать сразу при описании, так как ему нельзя присваивать никаких значений (он константный!). Впрочем, такую запись в C++ почти не употребляют, поскольку такого же эффекта можно достичь с помощью ссылки. Наоборот, указатели на константные объекты и ссылки на константные объекты встречаются на каждом шагу, ведь они гарантируют, что объект, на который ссылается указатель, не будет испорчен даже в результате неправильных действий. Указатель на константный объект позволяет прочесть объект, но не дает возможности изменить его.
Переменная типа "указатель на константный объект" или "ссылка на константный объект" вовсе не обязательно должна ссылаться только на константные объекты! Напротив, чаще всего такие указатели хранят адреса самых обычных объектов. Такой указатель лишь не дает возможности изменить объект, на который он ссылается. Например, мы передаем константный указатель на объект как аргумент некоторой функции. Константность указателя означает, что функция сможет "прочитать" объект, но не сможет его "испортить".
Константные методы классов – это те методы, которые гарантированно не меняют объекта, к которому они применяются. К константным объектам можно применять лишь константные методы. Константный метод задается с помощью модификатора "const", который записывается после круглых скобок, содержащих параметры метода. Например, для класса R2Vector метод "length", вычисляющий длину вектора, является константным. Описание его прототипа в h-файле выглядит так:
class R2Vector
{
. . .
double length() const;
. . .
};
Как правило, все методы, которые выдают какие-либо характеристики объекта, должны быть описаны как константные. В противном случае их нельзя будет применить для константных объектов или даже для нормальных объектов, доступ к которым осуществляется через константный указатель или ссылку!
Бывают, однако, и менее очевидные ошибки. Не забывайте о следующей возможности C++: может быть несколько одинаковых методов одного и того же класса, но с разными формальными параметрами – как иногда говорят, с разной сигнатурой. Это знают все без исключения программисты на C++, однако не все помнят, что модификатор "const" также включается в сигнатуру метода! Это означает, что у класса могут быть два метода, которые отличаются только модификатором "const"; например, такая запись вовсе не является ошибкой:
class A {
. . .
int f();
int f() const;
. . .
};
Первый (неконстантный) метод "f" применяется к обычным объектам, второй – к константным. Рассмотрим следующий фрагмент кода:
A x;
const A y;
. . .
int res1 = x.f();
int res2 = y.f();
К объекту "x" применяется первый, неконстантный метод "f", к объекту "y" – второй, константный метод "f".
class Matrix {
const int n;
const int m;
double* elements;
public:
. . .
double& at(int i, int j) {
return elements[i*m + j];
}
const double& at(int i, int j) const {
return elements[i*m + j];
}
double* operator[](int i) {
return elements + i*m;
}
const double* operator[](int i) const {
return elements + i*m;
}
. . .
};
Подведем итог:
метод обязательно должен быть описан как константный, если он не меняет объекта, к которому применяется;
для доступа к внутреннему элементу объекта класса должно быть два метода с одинаковым названием, обычный и константный. Обычный метод возвращает ссылку или указатель на элемент объекта, константный метод – константную ссылку или константный указатель.
Проблемы, порождаемые наличием константных методов
При объявлении переменных и параметров функций в описании типа может быть указано ключевое слово 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:
classTTextReader
{
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++ имеет право выполнять
регистровые операции (оптимизации) при чтении и записи переменных, а также размещать их в регистрах.
Операторы приведения типа в языке C++: const_cast, reinterpret_cast, static_cast, dynamic_cast
Static_cast – безопасное преобразование, не содержит за собой инструкций процессора.
Существует четыре оператора преобразования типа в С++:
reinterpret_cast<тип>(переменная)
static_cast<тип>(переменная)
const_cast<тип>(переменная)
dynamic_cast<тип>(переменная)
reinterpret_cast позволяет отключить контроль типов данных на уровне компилятора, с помощью него любой указатель может быть интерпретирован, как любой другой указатель, а также любая память или переменная может быть интерпретирована иначе. В программах этот оператор преобразования типа использовать не следует, так как он нарушает переносимость программ. Его наличие свидетельствует о том, что программа не является кросс-платформенной. Обычно такое приведение типа применяется для низкоуровневого, зависящего от реализации программирования.
int i;
char* p;
p = reinterpret_cast<char *>(&i);
static_cast используется вместо преобразования тип(переменная), (тип)переменная и (тип)(переменная) при работе с классами, структурами и указателями на них. Он задуман по причине того, что в С++ выражение тип(переменная) может оказаться вызовом конструктора. Если в программе требуется преобразовать тип, а не вызвать конструктор типа, используется данный оператор. Кроме того, оператор (тип)переменная или (тип)(переменная) может в некоторых случаях оказаться преобразованием reinterpret_cast<тип>(переменная), а при разработке кросс-платформенных программ оператор reinterpret_cast всегда содержит потенциальную опасность неправильной работы программы на другой платформе. Поэтому вместо операторов тип(переменная), (тип)переменная и (тип)(переменная) следует использовать операторы reinterpret_cast и static_cast, которые убирают не явность из преобразования. Так как оператор static_cast является громоздким, то для простых типов данных допустимо использование форм: (тип)переменная и(тип)(переменная). Форма тип(переменная) не должна использоваться для преобразования типа. Static_cast допустима только в том случае , если _тип_ может быть неявно преобразовано в тип, который имеет выражение, или наоборот. В любом другом случае приведение типа вызывает ошибку.
Сonst_cast используется для приведения не константных указателей к константным и наоборот:
void f2(char* s);
void f1(const char* s)
{
...
f1(const char*s);
...
f2(const_cast<char>(s))
...
};
Операция const_cast предназначена исключительно для приведения типа, если значение имеет тип const или volatile.
const cast < имя_типа > (выражение)
Результатом такого приведения типа будет ошибка, если любые другие аспекты типов не совпадают. То есть имя_типа и выражение должны быть одного типа, за исключением того, что они могут отличаться только наличием или отсутствием const или volatile.
Операция const_cast не во всем хороша. Она может изменить указатель на величину, но эффект от изменения значения, заданного как const, не определен.
void change (const int * pt , int n) ;
int main ()
{
int pop l 38383;
const int рор2 = 2000;
change (& pop1, - 1) ;
change (& pop2 , - 1) ;
void change (const int * pt , int n)
{
int * ре;
if (n < 0)
{
ре = const_ cast<int *> (pt) ;
*ре = 100;
}
}
Так как рор2 объявлен как const, компилятор может защитить его от изменения , как показано в следующем выводе программы:
рор1 , рор2 : 38383, 2000
рор1 , рор2 : 100, 2000
В данном случае компилятор создает временную копию рор2 и присваивает ее адрес ре, но, как уже
говорилось, в стандарте С++ данная ситуация называется неопределенной.
dynamic_cast соответствует оператору as в Delphi. Для работы этого оператора нужно в опциях компилятора включить опцию RTTI. Если это выполнено, то оператор dynamic_cast работает, как static_cast. Оператор dynamic_cast работает по-разному в зависимости от того, применяется он к ссылке на объект (&) или указателю на объект (*). Если оператор применяется к ссылке на объект, то преобразование не может быть выполнено и возникает исключительная ситуация. Если он применяется к указателю на объект и преобразование не может быть выполнено, оператор возвращает NULL.
17. Ссылки в языке C++. Рекомендации по работе со ссылками. Типичные ошибки при работе со ссылками.
Ссылки в языке C++
Ссылка является альтернативным именем объекта и объявляется следующим образом:
int i;
int &r = i;
Использование ссылки r эквивалентно использованию переменной i. Основное применение ссылок – передача параметров в функцию и возврат значения. В случае, когда ссылка используется в качестве параметра функции, она объявляется неинициализированной:
void f(int &i);
Во всех остальных случаях ссылка должна инициализироваться при объявлении, как показано ранее. Если ссылка является полем класса, она должна инициализироваться в конструкторе класса в списке инициализации до тела конструктора. При использовании в качестве параметров функций ссылки соответствуют var-параметрам в языке Delphi:
procedure P(var i: Integer);
Константные ссылки соответствуют const-параметрам в языке Delphi:
procedure P(const i: Integer);
При передаче ссылочного параметра в стек заносится адрес переменной, а не ее копия.
Cсылка во многом подобна указателю в замаскированной записи, где операция разыменования * предполагается неявно. И, фактически, это в какой-то степени именно то, чем является ссылка. Однако между ссылками и указателями существуют различия помимо обозначений. Одно из таких различий состоит в том, что ссылку необходимо инициализировать в момент ее объявления. Нельзя сначала объявить ссылку, а затем присвоить ей значение.
Ссылка во многом аналогична указателю со спецификатором const. Ее следует инициализировать в момент создания, после чего ссылка остается привязанной к определенной переменной до конца программы. Таким образом, конструкция
int & rodents = rats;
по сути, является замаскированной записью выражения, подобного следующему:
int const * pr = &rats;
В данном случае ссылка rodents играет ту же роль, что и выражение *pr.
{
int rats = 101;
int & rodents = rats; // rodents - это ссылка
int bunnies = 50;
rodents = bunnies;
}
Программа генерирует следующий вывод:
rats = 101, rodents = 101
адрес rats = Ox0065f d44, адрес rodents = Ox0065fd44
bunnies = 50, rats = 50, rodents = 50
адрес bunnies = Ox 0065fd48, адрес rodents = O0065f d4
Рекомендации по работе со ссылками
Ссылку следует рассматривать, как псевдоним переменной, которой она инициализирована. Ссылки отличаются от указателей тем, что позволяют компилятору лучше оптимизировать программный код. Для возврата значений из процедур (через параметры) предпочтение следует отдавать указателям, а не ссылкам. Ссылки следует использовать лишь в тех случаях, когда известно, что возвращаемый объект должен создаваться не в динамической памяти, а на стеке, то есть ссылки применяют при возврате value-type-объектов.
Передача аргументов по ссылке и передача по значению выглядят идентичными.
Если требуется, чтобы функция использовала передаваемую ей информацию, но не изменяла ее, и при этом использовать ссылку, следует воспользоваться постоянной ссылкой.
const double &r
Ссылочные аргументы полезно применять для более крупных элементов данных, таких как структуры и классы.
cube (double & ref);
double z = cube (x + 3.0) ; // может привести к ошибке компиляции
В современном языке С++ это ошибка, и некоторые компиляторы выведут сообщение об этом, другие – просто отобразят предостережение, что при вызове используется временная переменная в качестве параметра. Язык С++ в первые годы своего становления допускал передачу выражений в качестве ссылочных переменных. В некоторых случаях это допускается и сейчас.
Происходит следующее: программа создает временную переменную, не имеющую имени, инициализируя ее значением выражения х + 3. Затем ref становится ссылкой на эту временную переменную.
Когда создаются временные переменные:
С++ может создавать временную переменную, если передаваемый аргумент не соответствует определению ссылочного аргумента. В настоящее время С++ допускает это только в случае, когда аргументом является ссылка со спецификатором const, но это тоже новое ограничение.
При условии, что ссылочный параметр имеет спецификатор const, компилятор генерирует временную переменную в двух случаях:
• Когда тип фактического аргумента выбран правильно, но сам параметр не является lvalue (L-значением).
• Когда тип фактического параметра выбран неправильно, но может быть преобразован в правильный тип.
Аргумент, являющийся значением lvalue (L-значением), представляет собой объект данных, на который можно ссылаться. Примерам может служить переменная, элемент массива, элемент структуры, ссылка и разыменаванный указатель – все они являются L-значениями. К L-значениям не относятся литеральные константы и выражения, состоящие из нескольких элементов.
Одним словом, если назначение функции со ссылочными аргументами состоит в том, чтобы модифицировать переменные, передаваемые в качестве аргументов, ситуация, которую создают временные переменные, препятствуют достижению этой цели.
В этом случае решение заключается в том, чтобы запретить создание временных переменных, и новый стандарт С++ реализует именно этот подход. (Однако некоторые компиляторы по умолчанию выводят предупреждение вместо сообщения об ошибке. Поэтому не игнорируйте сообщения об использовании временных переменных.)
Существуют три "железных" довода в пользу объявления ссылочных аргументов в качестве ссылок на константы:
• Применение спецификатора const предотвращает программные ошибки, которые приводят к непреднамеренному изменению данных.
• Применение спецификатора const позволяет функции выполнять обработку формальных аргументов как со спецификатором const, так и без него.
• Использование ссылки со спецификатором const позволяет функции генерировать и использовать временные переменные по мере необходимости.
Ссылку можно использовать в качестве возвращаемого значения. Обычно механизм возврата копирует возвращаемое значение во временную область памяти, к которой вызывающая программа затем осуществляет доступ. Однако возврат ссьшки означает, что вызывающая программа выполняет прямой доступ к возвращаемому значению без использования приготовленной заранее копии. В обычных случаях такая ссылка указывает на ссылку, которая вначале была передана функции, так что вызывающая функция в результате осуществляет прямой доступ к одной из собственных переменных.
Типичные ошибки при работе со ссылками
При работе со ссылками существует типовая ошибка — возврат через ссылку переменной, созданной на стеке. Пример ошибочной записи:
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);
}
Важнее всего избегать ситуации, когда возвращаемая ссылка указывает на область памяти, которая прекращает существование после завершения работы функции. Подобно этому следует избегать ситуаций, когда на такие временные переменные возвращаются указатели.
Проще всего избежать подобной ошибки путем реализации возврата ссылки, которая была передана функции в качестве аргумента. Такой параметр будет ссылаться на данные, используемые вызывающей функцией. Таким образом, возвращаемая ссылка будет ссылаться на те же данные.
Второй метод заключается в использовании операции new для создания новой области хранения. Но необходимо не забыть после вызвать delete:
delete &ref
18. Обработка исключительных ситуаций в языке C++. Защита от утечки ресурсов. Имитация оператора try-finally. Понятие автоматического указателя (auto_ptr). Использование автоматических указателей для защиты от утечки ресурсов.
Обработка исключительных ситуаций в языке C++
В С++ отсутствует аналог блока try…finally…end. На платформе Windows благодаря структурной обработке ОС существуют следующий блок:
__try
{
...
}
__finally
{
...
}
Но следует отметить, что для переносимых программ он не подходит. В С++ существует аналог блока try…except…end:
try
{
...
}
catch(std::ios_base::failure)
{
...
}
catch(std::exception)
{
...
}
catch(...)
{
...
}
Распознавание исключительных ситуаций происходит последовательно блоками catch, поэтому их последовательность должна быть от частного к общему. Последний блок catch в примере выше ловит любую исключительную ситуацию. Создание исключительных ситуаций выполняется с помощью оператора throw:
throw std::exception("Ошибка");
Внутри блока catch оператор throw возобновляет исключительную ситуацию, как и raise в Delphi. При создании исключительной ситуации при помощи оператора throw объект, описывающий исключительную ситуацию, может быть создан в динамической памяти:
throw new std::exception("Ошибка");
Если применяется такой способ создания исключительной ситуации, ее уничтожение должно происходить следующим образом:
try
{
...
throw new std::exception("Ошибка"); // по ссылке – throw std::exception(“Mistake”);
}
catch(std::exception & e)
{
}
catch(std::exception *e)
{
delete e;
}
catch(...)
{
...
}
При try на стек укладывается дополнительная информация. Throw идет по кадрам в ближайший кадр try – получает адрес, на который нужно сделать переход в случае исключения. Остальные кадры выталкиваются.
Delphi: в глобальную переменную потоков помещается исключение, если объект создается в динамической памяти.
Если объект создается на стеке и он throw-объект, то throw находит нужный кадр, очищает стек, копирует объект, делает jmp. По правилам С++ нужно вызывать размещающий оператор new и конструктор копирования.
В С# можно бросить исключение любого типа, а словить тип Object.
Цель расширяемого программирования с С++ достигается только при жестких ограничениях для программиста. Невозможно программировать без thirdpart-libraries.
Если технология обеспечивает расширяемость и параллельную обработку, то у нее есть будущее.
Защита от утечки ресурсов. Имитация оператора try-finally
Если же записать так:
try
{
...
throw new std::exception("Ошибка");
}
catch(...)
{
...
}
то возникнет утечка ресурсов из-за того, что объект std::exception, созданный в динамической памяти, не будет освобожден. В С++ отсутствует общий базовый класс для исключительных ситуаций, поэтому на верхнем уровне работы программы нужно отлавливать все возможные базовые классы исключительных ситуаций. Это является препятствием на пути построения расширяемых систем.
Поскольку в С++ отсутствует блок try…finally, его приходится эмулировать.
Object *p = new Object();
try
{
...
}
catch(...)
{
delete p;
throw;
}
delete p;
Данный код эквивалентен следующему:
Object *p = new Object();
__try
{
...
}
__finally
{
delete p;
}
за исключением того, что второй пример не является переносимым. Согласно стандарту С++ в деструкторах и операторах delete не должно быть исключительных ситуаций, если же исключительная ситуация произошла, то поведение программы не определено. Если исключительная ситуация происходит в конструкторе объекта, объект считается не созданным и деструктор для этого объекта не вызывается, но память, выделенная для объекта, освобождается. Если внутри объекта агрегированы другие объекты, то вызываются деструкторы лишь для тех объектов, которые были полностью созданы к моменту возникновения исключительной ситуации.
Понятие автоматического указателя (auto_ptr)
Класс auto_ptr – это шаблонный класс для контроля над распределением динамической памяти.
void remodel ( string & str)
{
string * ps = new string(str) ;
str = ps ;
return ;
}
При каждом вызове функции происходит выделение памяти в куче, однако память не освобождается. Это приводит к утечкам памяти. Решение проблемы очевидно – не забыть освободить память:
delete ps ;
перед оператором return. Однако, решение, которое содержит фразу "не забыть", не является идеальным. Иногда разработчик не желает помнить об этом. Или разработчик помнит, но случайно удалил или закомментировал строку. И даже в том случае, когда это решение используется, могут возникать проблемы.
void remodel( string & str)
{
string * ps = new string (str) ;
if ( weird_thing() )
throw exception() ;
str = *ps ;
delete ps ;
return;
}
При возникновении исключительной ситуации операция delete вообще не будет достигнута, и снова появится утечка памяти.
Такие ошибки исправимы, но желательно иметь лучшее решение. Программа должна выполнять некоторые дополнительные действия после удаления указателя. В базовых типах подобной функциональности нет. Для классов такая возможность предоставлена через деструкторы. Если бы ps был объектом, то деструктор бы освобождал память, на которую указывал объект, при удалении объекта. Это и есть основная идея использования auto_ptr.
Шаблон аutо_рtr определяет объект, которому присваивается адрес области памяти, полученный вызовом операции new. Когда объект auto_ptr удаляется, его деструктор использует delete для освобождения памяти. Таким образом, присвоив адрес, возвращаемый операцией new, объекту auto_ptr, не нужно беспокоиться о последующем высвобождении памяти. Это будет сделано автоматически, при удалении объекта.
Для создания объекта auto_ptr необходимо включить заголовочный файл memory, в котором содержится шаблон auto_ptr. После этого, используя обычный синтаксис шаблонов, можно создавать указатели нужного типа. Интеллектуальные указатели принадлежати типу std.
template <class Х>
class auto_ptr
{
public :
explicit auto_ptr (X* р =0) throw() ; // конструктор не должен генерировать исключения
...
} ;
Auto_ptr<double> pd(new double);
Auto_ptr<string> ps(new string);
Допустимо только явное преобразование их простого указателя в интеллектуальный.
Недопустимо:
String helloStr(“Hello”);
auto_ptr<string> pHello(&helloStr);
В С++11 шаблон auto_ptr объявлен устаревшим, в качестве альтернативы предлагаются shared_ptr и unique_ptr.
Auto_ptr:
auto_ptr<string> p1(new string(“OOP”));
auto_ptr<string> p2;
p2=p1
cout << p1 // p1 утрачивает право владения указателем и перестает ссылаться на строку. Может привести к Segmentation fault (core dump).
Shared_ptr:
shared_ptr<string> p1(new string(“OOP”));
shared_ptr<string> p2;
p2=p1
cout << p1 //операция завершится успешно, т.к. p1и p2 указывают на один и тот же объект. Используется стратегия подсчета ссылок. При p1=p2 значение счетчика ссылок увеличилось на 1. В конце программы сначала вызовется деструктур p2, который уменьшит количество ссылок на 1. delete выполнится, когда счетчик ссылок станет 0.
Unique_ptr:
unique_ptr<string> p1(new string(“OOP”));
unique_ptr<string> p2;
p2=p1 // будет сгенерирована ошибка во время компиляции
Но если программа пытается присвоить один объект unique_ptr другому, компилятор не препятствует этому, если исходный объект является временным значением, и запрещает это, если исходный объет существует некоторое время.
Auto_ptr и shared_ptr не могут использоваться с массивами (память, выделенная как new[]). Unique_ptr может использоваться как с объектами, память для которых выделена операцией new, так и с объектами, память для которых выделена операцией new[].
Использование автоматических указателей для защиты от утечки ресурсов
Если объект создается в динамической памяти и освобождается в той же самой процедуре, то для защиты от утечки ресурсов можно применять оболочечные объекты – wrapper (содержит указатель на динамический объект, который уничтожается в деструкторе оболочечного объекта). Оболочечный элемент создается на стеке, поэтому его деструктор вызывается автоматически, гарантируя тем самым уничтожение агрегированного динамического объекта. Такие оболочечные объекты в библиотеках программирования называются AutoPtr, SafePtr и т.д.
void SomeMethod()
{
std::auto_ptr<SomeClass> temp(new SomeClass);
temp->DoSomething();
}
class MyClass
{
private:
std::auto_ptr<SomeClass> a_;
std::auto_ptr<SomeOtherClass> b_;
public:
MyClass() : a_(new SomeClass), b_(new SomeOtherClass) {}
~MyClass() throw() {} // показывает, что деструктор не выбрасывает исключений
};
Этот код безопасен с точки зрения исключений, а также прост и элегантен.