- •1. Понятие класса. Методы класса. Управление доступом к компонентам.
- •2. Объявление и определение класса. Внешнее определение функций.
- •3. Создание, копирование и удаление объекта.
- •4. Статические компоненты класса. Инициализация статических компонентов класса.
- •5. Наследование. Типы наследования. Виртуальное наследование.
- •6. Виртуальные функции
- •7. Абстрактные классы и чистые виртуальные функции. Интерфейс
- •8. Дружественность. Дружественные классы и функции.
- •10. Шаблоны классов
- •9. Вложенные классы. Внутреннее и внешнее определение.
- •11.Создание экземпляров шаблона. Инстанцирование.
- •12.Шаблоны и наследование.
- •13.Терминология шаблонов.
- •14. Параметры и аргументы шаблона.
- •15. Шаблоны компонентных функций
- •16. Полная специализация шаблонов.
- •17. Частичная специализация шаблонов.
- •18. Перегрузка операций. Основные понятия.
- •19. Перегрузка унарных операций.
- •20. Перегрузка бинарных операций.
- •Int test() {
- •Int test() {
- •Int test() {
- •// Делаем что-то
- •Вопрос 23
- •24. Группировка и композиция исключений. Повторная генерация. Перехват всех исключений.
- •25. Автоматическое управление ресурсами. Методика raii.
- •Void f() { FileOpen("myfile.Txt", "rt"); //здесь выполняем нужную работу с файлом //... }
- •Void f (int a) throw (x2, x3)
- •27. Стандартная библиотека. Организация стандартной библиотеки
- •28. Тип вектора. Вложенные типы. Итераторы. Доступ к элементам
- •29.Тип Вектора. Конструкторы. Операции со стеком. Списочные операции. Размеры и емкость.
- •30. Стандартные контейнеры. Вопросы производительности операций.
- •31. Процесс разработки по. Цели и этапы проектирования.
- •32. Процесс разработки по. Выявление классов. Определение операций.
- •33. Процесс разработки по. Определение взаимосвязей. Определение интерфейсов.
- •Этап 3: выявление зависимостей
- •Этап 4: определение интерфейсов
- •34. Паттерны проектирования. Основные паттерны.
- •35. Тестирование по. Методы тестирования.
19. Перегрузка унарных операций.
Любая унарная операция @ может быть определена двумя способами: либо как компонентная функция без параметров, либо как глобальная (возможно дружественная) функция с одним параметром. В первом случае выражение @ Z означает вызов Z.operator @(), во втором - вызов operator @(Z).
Унарные операции, перегружаемые в рамках определенного класса, могут перегружаться только через нестатическую компонентную функцию без параметров. Вызываемый объект класса автоматически воспринимается как операнд.
Унарные операции, перегружаемые вне области класса (как глобальные функции), должны иметь один параметр типа класса. Передаваемый через этот параметр объект воспринимается как операнд.
Синтаксис:
а) в первом случае (описание в области класса):
тип_возвр_значения operator знак_операции
б) во втором случае (описание вне области класса):
тип_возвр_значения operator знак_операции (идентификатор_типа)
Пример.
class Person { public: void operator ++() { ++age; } protected: int age; ... }; int main(void) { Person John; ++John; ... } |
class Person { protected: int age; ... friend void operator ++(Person &); }; void operator ++(Person &ob) { ++ob.age; } int main (void) { Person John; ++John; ... } |
Унарные операции инкремента ++ и декремента -- существуют в двух формах: префиксной и постфиксной. В современной спецификации С++ определен способ, по которому компилятор может различить эти две формы. В соответствии с этим способом задаются две версии функции operator ++() и operator --(). Они определены следующим образом:
Префиксная форма:
operator ++();
operator --();
Постфиксная форма:
operator ++(int);
operator --(int);
Указание параметра int для постфиксной формы не специфицирует второй операнд, а используется только для отличия от префиксной формы.
Пример.
class Person { public: ... void operator ++() { ++age; } void operator ++(int) { age++; } protected: int age; ... }; int main(void) { Person John; John++; ++John; }
20. Перегрузка бинарных операций.
Любая бинарная операция @ может быть определена двумя способами: либо как компонентная функция с одним параметром, либо как глобальная (возможно дружественная) функция с двумя параметрами. В первом случае x @ y означает вызов x.operator @(y), во втором – вызов operator @(x, y).
Операции, перегружаемые внутри класса, могут перегружаться только нестатическими компонентными функциями с параметрами. Вызываемый объект класса автоматически воспринимается в качестве первого операнда.
Операции, перегружаемые вне области класса, должны иметь два операнда, один из которых должен иметь тип класса.
Пример.
class Person { ... }; class AdressBook { public: Person &operator [](int);// доступ к i protected: // содержит в качестве компонентных данных множество объектов типа // Person представляемых как динамический массив, список или дерево ... }; Person &AdressBook::operator [](int i) { ... } int main(void) { AdressBook persons; Person record; ... record = persons[3]; ... } class Person { ... }; class AdressBook { protected: // содержит в качестве компонентных данных множество объектов типа // Person представляемых как динамический массив, список или дерево ... friend Person &operator [](const AdressBook &, int); // доступ к i }; Person &AdressBook::operator [](const AdressBook &, int i) { ... } int main(void) { AdressBook persons; Person record; ... record = persons[3]; ... }
Перегрузка операции вызова функции.
Это операция '()'. Она является бинарной операцией. Первым операндом обычно является объект класса, вторым – список параметров.
Пример.
class Matrix // двумерный массив вещественных чисел { public: ... double operator ()(int, int); //доступ к элементам матрицы по индексам }; double Matrix::operator() (int i, int j) { ... } int main (void) { Matrix a; double k; ... k = a(5, 6); ... }
Перегрузка операции присваивания.
Операция отличается тремя особенностями:
операция не наследуется;
операция определена по умолчанию для каждого класса в качестве операции поразрядного копирования объекта, стоящего справа от знака операции, в объект, стоящий слева.
операция может перегружаться только в области определения класса. Это гарантирует, что первым операндом всегда будет леводопустимое выражение.
Если вас устраивает поразрядное копирование, нет смысла создавать собственную функцию operator =(). Однако бывают случаи, когда поразрядное копирование нежелательно. Например, использование предопределенной операции присваивания для классов, содержащих указатели в качестве компонентных данных, чаще всего приводит к ошибкам. Покажем это на примере.
Пользовательский класс - строка String:
class String { public: String(char *); ~String(); void show(); protected: char *p; // указатель на строку int len; // текущая длина строки }; String::String(char *ptr) { len = strlen(ptr); p = new chat[len + 1]; if (!p) { cout << "Ошибка выделения памяти\n"); exit(1); } strcpy(p, ptr); } String::~String() { delete [] p; } void String::show(void) { cout << *p& lt;< "\n"; } int main(void) { String s1("Это первая строка"), s2("А это вторая строка"); s1.show(); s2.show() s2 = s1; // Это ошибка s1.show(); s2.show(); return 0; }
В чем здесь ошибка? Когда объект s1 присваивается объекту s2, указатель p объекта s2 начинает указывать на ту же самую область памяти, что и указатель p объекта s1. Таким образом, когда эти объекты удаляются, память, на которую указывает указатель p объекта s1, освобождается дважды, а память, на которую до присваивания указывал указатель p объекта s2, не освобождается вообще.
Хотя в данном примере эта ошибка и не опасна, в реальных программах с динамическим распределением памяти она может вызвать крах программы.
В этом случае необходимо самим перегружать операцию присваивания. Покажем как это сделать для нашего класса String.
class String { public: ... String &operator =(String &); protected: char *p; // указатель на строку int len; // текущая длина строки }; String &String::operator =(String &ob); { if (this == &ob) return *this; if (len < ob.len) { // требуется выделить дополнительную память delete [] p; p = new char[ob.len + 1]; if (!p) { cout << "Ошибка выделения памяти\n"); exit(1); } } len = ob.len; strcpy(p, ob.p); return *this; }
В этом примере выясняется, не происходит ли самоприсваивание (типа ob = ob). Если имеет место самоприсваивание, то просто возвращается ссылка на объект.
Затем проверяется, достаточно ли памяти в объекте, стоящем слева от знака присваивания, для объекта, стоящего справа от знака присваивания. Если не достаточно, то память освобождается и выделяется новая, требуемого размера. Затем строка копируется в эту память.
Отметим две важные особенности функции operanor =(). Во-первых, в ней используется параметр-ссылка. Это необходимо для предотвращения создания копии объекта, передаваемого через параметр по значению. В случае создания копии, она удаляется вызовом деструктора при завершении работы функции. Но деструктор освобождает память, на которую указывает p. Однако эта память все еще необходима объекту, который является аргументом. Параметр-ссылка помогает решить эту проблему.
Во-вторых, функция operator =() возвращает не объект, а ссылку на него. Смысл этого тот же, что и при использовании параметра-ссылки. Функция возвращает временный объект, который удаляется после завершения ее работы. Это означает, что для временной переменной будет вызван деструктор, который освобождает память по адресу p. Но она необходима для присваивания значения объекту. Поэтому, чтобы избежать создания временного объекта, в качестве возвращаемого значения используется ссылка.
Другой путь решения проблем, описанных выше - это создание конструктора копирования. Но конструктор копирования может оказаться не столь эффективным решением, как ссылка в качестве параметра и ссылка в качестве возвращаемого значения функции. Это происходит потому, что использование ссылки исключает затраты ресурсов, связанных с копированием объектов в каждом из двух указанных случаев
21. Понятие смешанной арифметики
22 Концепции «умного» указателя и итератора. Необходимые операции.
умные указатели: это объекты, которые хранят указатели на динамически аллоцированные участки памяти произвольного типа. Причем они автоматически очищают память по выходу из области видимости.
Простейший smart pointer
// Класс нашего умного указателя
template <typename T>
class smart_pointer {
T *m_obj;
public:
// Отдаем ему во владение некий объект
smart_pointer(T *obj)
: m_obj(obj)
{ }
// По выходу из области видимости этот объект мы уничтожим
~smart_pointer() {
delete m_obj;
}
// Перегруженные операторы
// Селектор. Позволяет обращаться к данным типа T посредством "стрелочки"
T* operator->() { return m_obj; }
// С помощью такого оператора мы можем разыменовать указатель и получить ссылку на
// объект, который он хранит
T& operator* () { return *m_obj; }
}
int test {
// Отдаем myClass во владение умному указателю
smart_pointer<MyClass> pMyClass(new MyClass(/*params*/);
// Обращаемся к методу класса MyClass посредством селектора
pMyClass->something();
// Допустим, что для нашего класса есть функция вывода его в ostream
// Эта функция одним из параметров обычно получает ссылку на объект,
// который должен быть выведен на экран
std::cout << *pMyClass << std::endl;
// по выходу из скоупа объект MyClass будет удален
}
Понятно, что наш смарт пойнтер не лишен недостатков (например, как хранить в нем массив?), но он в полной мере реализует идиому RAII. Он ведет себя так же, как и обычный указатель (благодаря перегруженным операторам), причем нам не нужно заботиться об освобождении памяти: все будет сделано автоматически. По желанию к перегруженным операторам можно добавить const, гарантировав неизменность данных, на которые ссылается указатель. рассмотрим их конкретные реализации. Итак, наши смарт-пойнтеры:
boost::scoped_ptr
std::auto_ptr
std::tr1::shared_ptr (он же std::shared_ptr в C++11, либо boost::shared_ptr из boost)
boost::scoped_ptr
Он находится в библиотеке буст. Реализация простая и понятная, практически идентичная нашей, за несколькими исключениями, одно из них: этот пойнтер не может быть скопирован (то есть у него приватный конструктор копирования и оператор присваивания). Поясню на примере:
