WWW.DISS.SELUK.RU

БЕСПЛАТНАЯ ЭЛЕКТРОННАЯ БИБЛИОТЕКА
(Авторефераты, диссертации, методички, учебные программы, монографии)

 

Pages:     | 1 || 3 | 4 |

«Т, А. Павловская C/C++ Программирование на языке высокого уровня Допущено Министерством образования Российской Федерации в качестве учебника для студентов высших учебных заведений, обучающихся по направлению Информатика ...»

-- [ Страница 2 ] --

^ Для так называемого сбалансированного дерева, в котором количество узлов справа и сле­ ва отличается не более чем на единицу, высота дерева равна двоичному логарифму коли­ чества узлов. Линейный список можно представить как вырожденное бинарное дерево, в котором каждый узел имеет не более одной ссылки. Для списка среднее время поиска равно половине длины списка.

Глава 3. Технология создания программ на левое или правое поддерево в зависимости от значения ключа в каждом узле.

Такой поиск гораздо эффективнее поиска по списку, поскольку время поиска определяется высотой дерева, а она пропорциональна двоичному логарифму ко­ личества узлов.

Дерево является рекурсивной структурой данных, поскольку каждое поддерево также является деревом. Действия с такими структурами изящнее всего описы­ ваются с помощью рекурсивных алгоритмов. Например, функцию обхода всех уз­ лов дерева в общем виде можно описать так:

function wayarouncl ( дерево ){ way^around ( левое поддерево ) посещение корня way_around ( правое поддерево ) Можно обходить дерево и в другом порядке, например, сначала корень, потом поддеревья, но приведенная функция позволяет получить на выходе отсортиро­ ванную последовательность ключей, поскольку сначала посещаются вершины с меньшими ключами, расположенные в левом поддереве. Результат обхода дере­ ва, изображенного на рис. 3.3:

1. 6. 8. 10. 20. 21. 25. Если в функции обхода первое обращение идет к правому поддереву, результат обхода будет другим:

30. 25. 21. 20. 10. 8. 6. Таким образом, деревья поиска можно применять для сортировки значений. При обходе дерева узлы не удаляются.

Для бинарных деревьев определены операции:

• включения узла в дерево;

• поиска по дереву;

• обхода дерева;

• удаления узла.

Для каждого рекурсивного алгоритма можно создать его нерекурсивный эквива­ лент. В приведенной ниже программе реализована нерекурсивная функция поис­ ка по дереву с включе7шем и рекурсивная функция обхода дерева. Первая функ­ ция осуществляет поиск элемента с заданным ключом. Если элемент найден, она возвращает указатель на него, а если нет — включает элемент в соответствующее место дерева и возвращает указатель на него. Для включения элемента необходи­ мо помнить пройденный по дереву путь на один шаг назад и знать, выполняется ли включение нового элемента в левое или правое поддерево его предка^.

Программа формирует дерево из массива целых чисел и выводит его на экран.

#1nclude struct Node{ Node '^left;

Node * searchJnsert(Node *root. 1nt d);

void pr1nt_tree(Node '^root. 1nt 1);

1nt b[] = {10. 25. 20. 6. 21. 8. 1. 30}:

// Поиск с включением Node * searchJnsertCNode *root. int d){ Node *pv = root, *prev;

boo! found = false:

while (pv & !found){ if (found) return pv;

// Создание нового узла:

Node *pnew = new Node;

pnew->right = 0:

// Присоединение к левому поддереву предка:

// Присоединение к правому поддереву предка:

void print_tree(Node *p. int level){ print_tree(p->left. level +1); // вывод левого поддерева print_tree(p->right. level +1); // вывод правого поддерева Текущий указатель для поиска по дереву обозначен pv, указатель на предка pv обозначен prev, переменная pnew используется для выделения памяти под вклю­ чаемый в дерево узел. Рекурсии удалось избежать, сохранив всего одну перемен­ ную (prev) и повторив при включении операторы, определяющие, к какому под­ дереву присоединяется новый узел.

Результат работы программы для дерева, изображенного на рис. 3.3:

Рассмотрим подробнее функцию обхода дерева. Вторым параметром в нее переда­ ется целая переменная, определяющая, на каком уровне находится узел. Корень находится на уровне 0. Дерево печатается по горизонтали так, что корень нахо­ дится слева (посмотрите на результат работы программы, наклонив голову вле­ во, и сравните с рис. 3.3). Перед значением узла для имитации структуры дерева выводится количество пробелов, пропорциональное уровню узла. Если заком­ ментировать цикл печати пробелов, отсортированный по возрастанию массив бу­ дет выведен в столбик. Заметьте, что функция обхода дерева длиной всего в не­ сколько строк может напечатать дерево любого размера — ограничением является только размер стека.

Удаление узла из дерева представляет собой не такую простую задачу, поскольку удаляемый узел может быть корневым, содержать две, одну или ни одной ссылки на поддеревья. Для узлов, содержащих меньше двух ссылок, удаление тривиаль­ но. Чтобы сохранить упорядоченность дерева при удалении узла с двумя ссылка­ ми, его заменяют на узел с самым близким к нему ключом. Это может быть са­ мый левый узел его правого поддерева или самый правый узел левого поддерева (например, чтобы удалить из дерева на рис. 3.3 узел с ключом 25, его нужно заме­ нить на 21 или 30, узел 10 заменяется на 20 или 8, и т. д.). Реализация функции удаления из дерева оставлена читателю для самостоятельной работы.

Реализация динамических структур с помощью массивов Операции выделения и освобождения памяти — дорогое удовольствие, поэтому если максимальный размер данных можно определить до начала использования и в процессе работы он не изменяется (например, при сортировке содержимого файла), более эффективным может оказаться однократное выделение непрерыв­ ной области памяти. Связи элементов при этом реализуются не через указатели, а через вспомогательные переме1Н1ые или массивы, в которых хранятся номера элементов.



Проще всего реализовать таким образом стек. Кроме массива элементов, соответ­ ствующих типу дагпхых стека, достаточно иметь одну неременную целого типа для хранения ихщекса элемента массива, являющегося вершиной стека. При помещении в стек индекс увеличивается на единицу, а при выборке — уменьшается.

Для реализации очереди требуются две переменных целого типа — для хранения индекса элементов массива, являющихся началом и концом очереди.

Для реализации линейного списка требуется вспомогательный массив целых чи­ сел и еще одна переменная, например:

10 25 20 6 21 8 1 30 - массив данных 1 2 3 4 5 6 7 - 1 - вспомогательный массив 0 - индекс первого элемента в списке 1-й элемент вспомогательного массива содержит для каждого i-ro элемента мас­ сива данных индекс следующего за ним элемента. Отрицательное число исполь­ зуется как признак конца списка. Тот же массив после сортировки:

10 25 20 6 21 8 1 30 - массив данных 2 7 4 5 1 0 3 - 1 - вспомогательный массив 6 - индекс первого элемента в списке Для создания бинарного дерева можно использовать два вспомогательных мас­ сива (индексы вершин его правого и левого поддерева). Отрицательное число используется как признак пустой ссылки. Например, дерево, приведенное на рис. 3.3, можно представить следующим образом:

10 25 20 6 21 8 1 30 - массив данных 1 7 4 5 -1 -1 -1 -1 - правая ссылка Память под такие структуры можно выделить либо на этапе компиляции, если размер можно задать константой, либо во время выполнения программы, напри­ мер:

struct Nodej Data d: // тип данных Data должен быть определен ранее Node sp1sokl[lOOO]; // на этапе компиляции Node *pspisok2 = new Node[m]: // на этапе выполнения

ВНИМАНИЕ

При работе с подобными структурами необходимо контролировать возможный выход ин­ дексов за границу массива.

Приведенный выше способ реализации позволяет использовать преимущества динамических структур (например, сортировать структуры из громоздких эле­ ментов данных без их физического перемещения в памяти), и при этом не расхо­ довать время на выделение и освобождение памяти для каждого элемента дан­ ных.

Упражнения к части I Циклические вычислительные процессы Вариант Вычислить и вывести на экран в виде таблицы значения функции F на интервале от Хнач. до Хкон. с шагом dX.

где а, Ь, с — действительные числа.

Функция F должна принимать действительное значение, если выражение (Ац ИЛИ Вц) И (Ац ИЛИ Сц) не равно нулю, и целое значение в противном случае. Через Ац, Вц и Сц обозна­ чены целые части значений а, Ь, с, операции И и ИЛИ — поразрядные. Значения а, Ь, с, Хнач., Хкон., dX ввести с клавиатуры.

Вариант Вычислить и вывести на экран в виде таблицы значения функции F на интервале от Хнач. до Хкон. с шагом dX.

сгде а, Ь, с — действительные числа.

Функция F должна принимать действительное значение, если выражение (Ац И Вц) ИЛИ (Вц И Сц) не равно нулю, и целое значение в противном случае. Через Ац, Вц и Сц обозна­ чены целые части значений а, Ь, с, операции И и ИЛИ — поразрядные. Значения а, Ь, с, Хнач., Хкон., dX ввести с клавиатуры.

Упражнения к части I Вариант Вычислить и вывести на экран в виде таблицы значения функции F на интервале от Хнач. до Хкон. с шагом dX.

где а,Ь, с — действительные числа.

Функция F должна принимать действительное значение, если выражение Ац И (Вц ИЛИ Сц) не равно нулю, и целое значение в противном случае. Через Ац, Вц и Сц обозна­ чены целые части значений а, Ь, с, операции И и ИЛИ — поразрядные. Значения а, by с, Хнач., Хкон., dX ввести с клавиатуры.

Вариант Вычислить и вывести на экран в виде таблицы значения функции F на интервале от Хнач. до Хкон. с шагом dX.

где а, Ь, с — действительные числа.

Функция F должна принимать действительное значение, если выражение Ац ИЛИ Вц ИЛИ Сц не равно нулю, и целое значение в противном случае. Через Ац, Вц и Сц обозна­ чены целые части значений а, Ь, с, операция ИЛИ — поразрядная. Значения а, Ь, с, Хнач., Хкон., dX ввести с клавиатуры.

Вариант Вычислить и вывести на экран в виде таблицы значения функции F на интервале от Хнач. до Хкон. с шагом dX, Зх + где а,Ь, с — действительные числа.

Функция F должна принимать действительное значение, если выражение (Ац ИЛИ Вц) И Сц не равно нулю, и целое значение в противном случае. Через Ац, Вц и Сц обозна­ чены целые части значений а, Ь, с, операции И и ИЛИ — поразрядные. Значения при обращении через указатель, например:

int п = Vasid.get_ammo();

stado[5].draw;

cout « beavis->get_health();

Обратиться таким образом можно только к элементам со спецификаторам риЫ ic.

Получить или изменить значения элементов со спецификатором private можно только через обращение к соответствующим методам.

Можно создать константный объект, значения полей которого изменять запре­ щается. К нему должны применяться только константные методы:

class monstr{ int get_health() const {return health;} const monstr Dead(Q,0); // Константный объект cout « Dead.get_health();

Константный метод:

• объявляется с ключевым словом const после списка параметров;

а не может изменять значения полей класса;

• может вызывать только константные методы;

• может вызываться для любых (не только константных) объектов.

Рекомендуется описывать как константные те методы, которые предназначены для получения значений полей.

Указатель this Каждый объект содержит свой экземпляр полей класса. Методы класса находят­ ся в памяти в единственном экземпляре и используются всеми объектами совме­ стно, поэтому необходимо обеспечить работу методов с полями именно того объ­ екта, для которого они были вызваны. Это обеспечивается передачей в функцию скрытого параметра this, в котором хранится константный указатель на вызвав­ ший функцию объект. Указатель this неявно используется внутри метода для ссылок на элементы объекта. В явном виде этот указатель применяется в основ­ ном для возвращения из метода указателя (return this;) или ссылки (return *this;) на вызвавший объект.

Для иллюстрации использования указателя this добавим в приведенный выше класс monstr новый метод, возвращающий ссылку на наиболее здорового (поле health) из двух монстров, один из которых вызывает метод, а другой передается ему в качестве параметра (метод нужно поместить в секцию public описания класса):

monstr & the_best(monstr &М){ if( health > М.health) return nhis:

.. monstr Vas1a(50). Super(200):

// Новый объект Best инициализируется значениями полей Super:

monstr Best = Vasia.the_best(Super):

Указатель this можно также применять для идентификации поля класса в том случае, когда его имя совпадает с именем формального параметра метода. Другой способ идентификации поля использует операцию доступа к области видимости:

void curednt health. 1nt ammo){ this -> health += health; / / Использование this Конструкторы Конструктор предназначен для инициализации объекта и вызывается автомати­ чески при его создании. Ниже перечислены основные свойства конструкторов.

• Конструктор 7ie возвращает значение, даже типа void. Нельзя получить указа­ тель на конструктор.

• Класс может иметь несколько конструкторов с разными параметрами для раз­ ных видов инициализации (при этом используется механизм перегрузки).

• Конструктор, вызываемый без параметров, называется конструктором по умолчанию.

• Параметры конструктора могут иметь любой тип, кроме этого же класса.

Можно задавать значения параметров по умолчанию. Их может содержать только один из конструкторов.

• Если программист не указал ни одного конструктора, компилятор создает его автоматически. Такой конструктор вызывает конструкторы по умолчанию для полей класса и конструкторы по умолчанию базовых классов (см. раздел «Простое наследование», с. 201). В случае, когда класс содержит константы или ссылки, при попытке создания объекта класса будет выдана ошибка, по­ скольку их необходимо инициализировать конкретными значениями, а кон­ структор по умолчанию этого делать не умеет.

• Конструкторы не наследуются.

• Конструкторы нельзя описывать с модификаторами const. virtual и static.

• Конструкторы глобальных объектов вызываются до вызова функции main.

Локальные объекты создаются, как только становится активной область их действия. Конструктор запускается и при создании временного объекта (на­ пример, при передаче объекта из функции).

• Конструктор вызывается, если в программе встретилась какая-либо из син­ таксических конструкций:

имя_класса имя_объекта [(список параметров)];

/ / Список параметров не должен быть пустым имя_класса (список параметров);

// Создается объект без имени (список может быть пустым) имя^класса имя_объекта = выражение;

// Создается объект без имени и копируется Примеры:

monstr Super(200. 300). Vas1a(50). Z:

monstr X = monstr(lOOO):

monstr Y = 500;

В первом операторе создаются три объекта. Значения не указанных параметров устанавливаются по умолчанию.

Во втором операторе создается безымянный объект со значением параметра health = 1000 (значение второго параметра устанавливается по умолчанию). Вы­ деляется память под объект X, в которую копируется безымянный объект.

В последнем операторе создается безымянный объект со значением параметра health = 500 (значение второго параметра устанавливается по умолчанию). Выде­ ляется память под объект Y, в которую копируется безымянный объект. Такая форма создания объекта возможна в том случае, если для инициализации объек­ та допускается задать один параметр.

В качестве примера класса с несколькими конструкторами усовершенствуем описанный ранее класс monstr, добавив в него поля, задающие цвет (skin) и имя (name):

enum color {red, green, blue}; // Возможные значения цвета class monstr{ monstr(int he = 100. int a =10);

monstr(color sk);

monstr(char * nam);

int get_health(){return health;} int get_ammo(){return ammo;} health = he; ammo = am; skin = red; name = 0;

//---- monstr::monstr(color sk)( switch (sk){ case green health = 100 ammo = 20 skin = green; name = 0; break case blue health = 100 ammo = 40 skin = blue; name = 0; break name = new char [strlen(nam) + 1];

// К длине строки добавляется 1 для хранения нуль-символа strcpyCname. nam):

health = 100: ammo = 1 0 : sk1n = red:

//---monstr * m = new monstr ("Ork"):

monstr Green (green):

Первый из приведенных выше конструкторов является конструктором по умол­ чанию, поскольку его можно вызвать без параметров. Объекты класса monstr те­ перь можно инициализировать различными способами, требуемый конструктор будет вызван в зависимости от списка значений в скобках. При задании несколь­ ких конструкторов следует соблюдать те же правила, что и при написании пере­ груженных функций — у компилятора должна быть возможность распознать нужный вариант.

ПРИМЕЧАНИЕ

Перегружать можно не только конструкторы, но и другие методы класса.

Существует еще один способ инициализации полей в конструкторе (кроме ис­ пользованного в приведенной выше программе присваивания полям значений формальных параметров) — с помощью списка инициализаторов, расположен­ ных после двоеточия между заголовком и телом конструктора:

monstr::monstr(1nt he. i n t am):

health (he), a m (am), skin (red), name (0){} Поля перечисляются через запятую. Для каждого поля в скобках указывается инициализирующее значение, которое может быть выражением. Без этого спосо­ ба не обойтись при инициализации полей-констант, полей-ссылок и полей-объек­ тов, В последнем случае будет вызван конструктор, соответствующий указан­ ным в скобках параметрам.

ПРИМЕЧАНИЕ

Конструктор не может возвратить значение, чтобы сообщить об ошибке во время инициа­ лизации. Для этого можно использовать механизм обработки исключительных ситуаций (см. раздел «Исключения в конструкторах и деструкторах», с. 228).

Конструктор копирования Конструктор копирования — это специальный вид конструктора, получающий в качестве единственного параметра указатель на объект этого же класса:

T::T(const Т&) {... / * Тело конструктора V } где Т ~ имя класса.

Этот конструктор вызывается в тех случаях, когда новый объект создается путем копирования существующего:

• при описании нового объекта с инициализацией другим объектом;

• при передаче объекта в функцию по значению;

• при возврате объекта из функции^ Если программист не указал ни одного конструктора копирования, компилятор создает его автоматически. Такой конструктор выполняет поэлементное копиро­ вание полей. Если класс содержит указатели или ссылки, это, скорее всего, будет неправильным, поскольку и копия, и оригинал будут указывать на одну и ту же область памяти.

Запишем конструктор копирования для класса monstr. Поскольку в нем есть поле name, содержащее указатель на строку символов, конструктор копирования дол­ жен выделять память под новую строку и копировать в нее исходную:

monstr::mcnstr(const monstr &М){ i f (M.name){ name = new char [strlen(M.name) + 1 ] :

strcpyCname. M.name):} health = M.health; ammo = M.ammo; skin = M.skin;

monstr Vasia (blue);

monstr Super = Vasia; // Работает конструктор копирования monstr *m = new monstr ("Ore");

monstr Green = *m; // Работает конструктор копирования

ПРИМЕЧАНИЕ

Любой конструктор класса, принимающий один параметр какого-либо другого типа, назы­ вается конструктором преобразования, поскольку он осуществляет преобразование из типа параметра в тип этого класса.

Правила написания конструкторов классов, входящих в иерархию, описаны в разделе «Простое наследование», с. 201.

Статические элементы класса С помощью модификатора static можно описать статические поля и методы класса. Их можно рассматривать как глобальные переменные или функции, дос­ тупные только в пределах области класса.

' А также при обработке исключений.

Статические поля Статические поля применяются для хранения данных, общих для всех объектов класса, например, количества объектов или ссылки на разделяемый всеми объек­ тами ресурс. Эти поля существуют для всех объектов класса в единственном эк­ земпляре, то есть не дублируются.

Ниже перечислены особенности статических полей.

• Память под статическое поле выделяется один раз при его инициализации независимо от числа созданных объектов (и даже при их отсутствии) и ини­ циализируется с помощью операции доступа к области действия, а не опера­ ции выбора (определение должно быть записано вне функций):

class А{ // i n t A::count = 10: Пример инициализации произвольным значением а Статические поля доступны как через имя класса, так и через имя объекта:

cout « A::count « a->count « b.count:

// Будет выведено одно и то же • На статические поля распространяется действие спецификаторов доступа, поэтому статические поля, описанные как private, нельзя изменить с помо­ щью операции доступа к области действия, как описано выше. Это можно сделать только с помощью статических методов (см. далее).

• Память, занимаемая статическим полем, не учитывается при определении размера объекта с помощью операции sizeof.

Статические методы Статические методы предназначены для обращения к статическим полям класса.

Они могут обращаться непосредственно только к статическим полям и вызывать только другие статические методы класса, потому что им не передается скрытый указатель this. Обращение к статическим методам производится так же, как к статическим полям — либо через имя класса, либо, если хотя бы один объект класса уже создан, через имя объекта.

class А{ static int count: // Поле count - скрытое static void inc_count(){ count++: } A::1nt count: // Определение в глобальной области void f(){ // a.count++ - нельзя, поле count скрытое // Изменение поля с помощью статического метода:

a.inc_count(): // или А: :inccount():

Статические методы не могут быть константными (const) и виртуальными (virtual).

Дружественные функции и классы Иногда желательно иметь непосредственный доступ извне к скрытым полям класса, то есть расБ1ирить интерфейс класса. Для этого служат дружественные функции и дружественцые классы.

Дружественная функция Дружественные функции применяются для доступа к скрытым нолям класса и представляют собой альтернативу методам. Метод, как правило, используется для реализации свойств объекта, а в виде дружественных функций оформляются действия, не представляющие свойства класса, но концептуально входящие в его интерфейс и нуждающиеся в доступе к его скрытым полям, например, переопре­ деленные операции вывода объектов (см. с. 284).

Ниже перечислены правила описания и особенности дружественных функций.

• Дружественная функция объявляется внутри класса, к элементам которого ей нужен доступ, с ключевым словом friend. В качестве параметра ей должен передаваться объект или ссылка на объект класса, поскольку указатель this ей не передается.

• Дружественная функция может быть обычной функцией или методом друго­ го ранее определенного класса. На нее не распространяется действие специ­ фикаторов доступа, место размещения ее объявления в классе безразлично.

• Одна функция может быть дружественной сразу нескольким классами.

В качестве примера ниже приведено описание двух функций, дружественных классу пюnstг. Функция kill является методом класса hero, а функция steal a m не принадлежит ни одному классу. Обеим функциям в качестве параметра пере­ дается ссылка на объект класса monstr.

class monstr: / / Предварительное объявление класса class hero{ void kilKmonstr &):

class monstr{ friend int steal_ammo(monstr &);

friend void hero::kill(monstr &);

//' Класс hero должен быть определен ранее int steal_ammo(monstr &M){return --M.ammo;} void hero::kill(monstr &M){M.health = 0: Ma m = 0;} Использования дружественных функций нужно по возможности избегать, по­ скольку они нарушают принцип инкапсуляции и, таким образом, затрудняют от­ ладку и модификацию программы.

Дружественный класс Если все методы какого-либо класса должны иметь доступ к скрытым полям другого, весь класс объявляется дружественным с помощью ключевого слова friend. В приведенном ниже примере класс mistress объявляется дружественным классу hero:

class hero{ friend class mistress:

class mistress!

Функции f 1 и f2 являются дружественными по отношению к классу hero (хотя и описаны без ключевого слова friend) и имеют доступ ко всем его полям.

Объявление friend не является спецификатором доступа и не наследуется.

ПРИМЕЧАНИЕ

Обратите внимание на то, что класс сам определяет, какие функции и классы являются дружественными, а какие нет.

Деструкторы Деструктор — это особый вид метода, применяющийся для освобождения памя­ ти, занимаемой объектом. Деструктор вызывается автоматически, когда объект выходит из области видимости:

• для локальных объектов — при выходе из блока, в котором они объявлены;

• для глобальных — как часть процедуры выхода из main:

• для объектов, заданных через указатели^ деструктор вызывается неявно при использовании операции delete.

ВНИМАНИЕ

При выходе из области действия указателя на объект автоматический вызов деструктора объекта не производится.

Имя деструктора начинается с тильды (~), непосредственно за которой следует имя класса. Деструктор:

• не имеет аргументов и возвращаемого значения;

• не может быть объявлен как const или static;

• не наследуется;

• может быть виртуальным (см. раздел «Виртуальные методы», с. 205).

Если деструктор явным образом не определен, компилятор автоматически созда­ ет пустой деструктор.

Описывать в классе деструктор явным образом требуется в случае, когда объект содержит указатели на память, выделяемую динамически ~ иначе при уничто­ жении объекта память, на которую ссылались его поля-указатели, не будет поме­ чена как свободная. Указатель на деструктор определить нельзя.

Деструктор для рассматриваемого примера (с. 183) должен выглядеть так:

monstr::~monstr() {delete [ ] name;} Деструктор можно вызвать явным образом путем указания полностью уточнен­ ного имени, например:

monstr %;...

m -> -monstrO;

Это может понадобиться для объектов, которым с помощью перегруженной опе­ рации new выделялся конкретный адрес памяти. Без необходимости явно вызы­ вать деструктор объекта не рекомендуется.

Перегрузка операций C++ позволяет переопределить действие большинства операций так, чтобы при использовании с объектами конкретного класса они выполняли заданные функ­ ции. Эта дает возможность использовать собственные типы дарпхых точно так же, как стандартные. Обозначения собственных операций вводить нельзя. Можно перегружать любые операции, существующие в C++, за исключением:

Перегрузка операций осуществляется с помощью методов специального вида {функций-операций) и подчиняется следующим правилам:

• при перегрузке операций сохраняются количество аргументов, приоритеты операций и правила ассоциации (справа налево или слева направо), исполь­ зуемые в стандартных типах данных;

• для стандартных типов данных переопределять операции нельзя;

• функции-операции не могут иметь аргументов по умолчанию;

• функции-операции наследуются (за исключением =*);

• функции-операции не могут определяться как static.

Функцию-операцию можно определить тремя способами: она должна быть либо методом класса, либо дружественной функцией класса, либо обычной функцией.

В двух последних случаях функция должна принимать хотя бы один аргумент, имеющий тип класса, указателя или ссылки на класс^.

Функция-операция содержит ключевое слово operator, за которым следует знак переопределяемой операции:

тип operator операция ( список параметров) { тело функции } Перегрузка унарных операций Унарная функция-операция, определяемая внутри класса, должна быть пред­ ставлена с помощью нестатического метода без параметров, при этом операндом является вызвавший ее объект, например:

class monstrf monstr & operator ++() {++health: return *this:} monstr Vasia;

cout « (++Vasia).get_health();

Если функция определяется вне класса, она должна иметь один параметр типа класса:

class monstr{ friend monstr & operator ++( monstr &M);

monstr& operator ++(monstr &M) {++M.health; return M;} Если не описывать функцию внутри класса как дружественную, нужно учиты­ вать доступность изменяемых полей. В данном случае поле heal th недоступно из­ вне, так как описано со спецификатором private, поэтому для его изменения тре­ буется использование соответствующего метода. Введем в описание класса monstr метод change_health, позволяющий изменить значение поля health void changehealth(int he){ health = he;} Тогда можно перегрузить операцию инкремента с помощью обычной функции, описанной вне класса:

monstr& operator ++(monstr &М){ i n t h = M.gethealth(); h++;

M.change_health(h);

* Особый случай: функция-операция, первый параметр которой имеет стандартный тип, не может определяться как метод класса.

Операции постфиксного инкремента и декремента должны иметь первый пара­ метр типа int. Он используется только для того, чтобы отличить их от префикс­ ной формы:

class monstrj monstr operator ++(int){ monstr MCnhis); health++;

monstr Vasia:

cout « (Vas1a++).get_health();

Перегрузка бинарных операций Бинарная функция-операция, определяемая внутри класса, должна быть пред­ ставлена с помощью нестатического метода с параметрами, при этом вызвавший ее объект считается первым операндом:

class monstr{ bool operator >(const monstr &M){ 1f( health > M.health) return true;

Если функция определяется вне класса, она должна иметь два параметра типа класса:

bool operator >(const monstr &M1. const monstr &M2){ 1f( Ml.get_health() > M2.get_health()) return true:

return false:

Перегрузка операции присваивания Операция присваивания определена в любом классе по умолчанию как поэле­ ментное копирование. Эта операция вызывается каждый раз, когда одному суще­ ствующему объекту присваивается значение другого. Если класс содержит поля, память под которые выделяется динамически, необходимо определить собствен­ ную операцию присваивания. Чтобы сохранить семантику присваивания, опера­ ция-функция должна возвращать ссылку на объект, д;;1я которого она вызвана, и принимать в качестве параметра единственный аргумент — ссылку на присваи­ ваемый объект.

const monstr& operator = (const monstr &M){ / / Проверка на самоприсваивание:

i f (name) delete [ ] name;

name = new char [strlen(M.name) + 1];

strcpyCname. M.name);} health = M.health; a m = M.ammo; skin = M.skin;

return *this;

Возврат из функции указателя на объект делает возможной цепочку операций присваивания:

monstr А(10). В. С;

Операцию присваивания можно определять только как метод класса. Она не на­ следуется.

Перегрузка операций new и delete Чтобы обеспечить альтернативные варианты управления памятью, можно опре­ делять собственные варианты операций new и new[] для выделения динамической памяти под объект и массив объектов соответственно, а также операции delete и delete [] для ее освобождения.

Эти функции-операции должны соответствовать следующим правилам:

• им не требуется передавать параметр типа класса;

• первым параметром функциям new и new[] должен передаваться размер объек­ та типа s1ze_t (это тип, возвращаемый операцией sizeof, он определяется в заголовочном файле ); при вызове он передается в функции неяв­ ным образом;

• они должны определяться с типом возвращаемого значения void*, даже если return возвращает указатель на другие типы (чаще всего на класс);

• операция delete должна иметь тип возврата void и первый аргумент типа void*;

• операции выделения и освобождения памяти являются статическими элемен­ тами класса.

Поведение перегруженных операций должно соответствовать действиям, выпол­ няемым ими по умолчанию. Для операции new это означает, что она должна воз­ вращать правильное значение, корректно обрабатывать запрос на выделение памяти нулевого размера и порождать исключение при невозможности выполне­ ния запроса (об исключениях рассказывается в разделе «Обработка исключи­ тельных ситуаций» на с. 222). Для операции delete следует соблюдать условие, что удаление нулевого указателя должно быть безопасным, поэтому внутри опе­ рации необходима проверка указателя на нуль и отсутствие каких-либо действий в случае равенства.

Стандартные операции выделения и освобождения памяти могут использоваться в области действия класса наряду с перегруженными (с помощью операции до­ ступа к области видимости :: для объектов этого класса и непосредственно — для любых других).

Перегрузка операции выделения памяти применяется для экономии памяти, по­ вышения быстродействия программы или для размещения данных в некоторой конкретной области. Например, пусть описывается loiacc, содержащий указатель на некоторый объект:

class Obj {...}:

class pObjj При выделении памяти под объект типа pObj с помощью стандартной операции new pObj *р = new pObj:

фактическое количество байтов будет превышать sizeof(pObj), поскольку new обычно записывает в начало выделяемой области ее размер (для того чтобы пра­ вильно отрабатывала операция delete):

Для небольших объектов эти накладные расходы могут оказаться весьма значи­ тельными. Для экономии памяти можно написать собственную операцию new класса pObj, которая будет выделять большой блок памяти, а затем размещать в нем указатели на Obj. Для этого в объект pObj вводится статическое поле headOf Free, в котором хранится указатель на первую свободную ячейку блока для размещения очередного объекта.

Неиспользуемые ячейки связываются в список. Чтобы не занимать память под поле связи, используется объединение (union), с помощью которого одна и та же ячейка используется либо для размещения указателя на объект, либо для связи со следующей свободной ячейкой:

class pObj{ static void * operator new(size_t size):

pObj *next: / / Указатель на следующую свободную ячейку static const int BLOCKSIZE:// Размер блока // Заголовок списка свободных ячеек:

static pObj *headOfFree:

void * pObj::operator new(size_t size){ // Перенаправить запросы неверного количества памяти // стандартной операции new:

if (size != sizeof(pObj)) return ::operator new(size);

pObj *p = headOfFree; // Указатель на первую свободную ячейку // Переместить указатель списка свободных ячеек:

if (р) headOfFree = р -> next:

// Если свободной памяти нет. выделяем очередной блок:

pObj *newblock = staticcast^ (::operator new(BLOCKSIZE * sizeof(pObj))):

// Bee ячейки свободны, кроме первой (она будет // занята), связываем их:

newblock[BLOCKSIZE - l].next = 0:

// Устанавливаем начало списка свободных ячеек:

headOfFree = &newblock[l]:

return p; / / Возвращаем указатель на выделенную память Перегруженная операция new наследуется, поэтому она вызывается для произ­ водных объектов. Если их размер не соответствует размеру базового (а так, ско­ рее всего, и есть), это может вызвать проблемы. Чтобы их избежать, в начале опе­ рации проверяется соответствие размеров. Если размер объекта не равен тому, для которого перегружена операция new, запрос на выделение памяти передается стандартной операции new.

В программе, использующей класс pObj, должна присутствовать инициализация его статических полей (статические поля рассматривались на с. 186):

pObj *pObj::headOfFree: / / Устанавливается в О по умолчанию const i n t pObj::BLOCK_SIZE = 1024:

Как видно из этого примера, помимо экономии памяти достигается еще и высо­ кое быстродействие, ведь в большинстве случаев выделение памяти сводится к нескольким простым операторам.

Естественно, что если операция new перегружена, то же самое должно быть вы­ полнено и для операции delete (например, в нашем случае стандартная операция delete не найдет в начале объекта верной информации о его размерах, что приве­ дет к неопределенному поведению программы).

В рассмотренном примере операция delete должна добавлять освобожденную ячейку памяти к списку свободных ячеек:

void pObj::operator delete(void * ObjToDie. size^t size){ i f (ObjToDie *== 0) return:

i f (size != sizeof(pObj)){ ^ Здесь использовано явное преобразование типа с помощью операции staticcast. О нем рассказывается в разделе «Операция static_cast» на с. 237.

::operator delete(ObjToDie): return:

pObj *p = stat1c_cast(0bjToDie);

p->next = headOfFree;

В операции delete выполнена проверка соответствия размеров объектов, анало­ гичная приведенной в операции new.

Перегрузка операции приведения типа Можно определить функции-операции, которые будут осуществлять преобразо­ вание объекта класса к другому хипу. Формат:

operator имянового^типа О:

Тип возвращаемого значения и параметры указывать не требуется. Можно опре­ делять виртуальные функции преобразования типа.

Пример:

.monstr::operator 1nt(){return health;} monstr Vasia; cout « intCVasia):

Перегрузка операции вызова функции Класс, в котором определена операция вызова функции, называется функцио­ нальным. От такого класса не требуется наличия других полей и методов:

class if_greater{ Использование такого класса имеет весьма специфический синтаксис. Рассмот­ рим пример:

if_greater х:

cout « 1fgreater()(5. 1) « endl; // Результат - Поскольку в классе if_greater определена операция вызова функции с двумя па­ раметрами, выражение х(1, 5) является допустимым (то же самое можно запи­ сать в виде X.operator О (1. 5)). Как видно из примера, объект функционального класса используется так, как если бы он был функцией.

Во втором операторе вывода выражение 1f_greater() используется для вызова конструктора по умолчанию класса if_greater. Результатом выполнения этого выражения является объект класса 1f_greater. Далее, как и в предыдущем слу­ чае, для этого объекта вызывается функция с двумя аргументами, записанными в круглых скобках.

Операцию ( ) можно определять только как метод класса. Можно определить пе­ регруженные операции вызова функции с различным количеством аргументов.

Функциональные объекты широко применяются в стандартной библиотеке C++.

Перегрузка операции индексирования Операция индексирования [ ] обычно перегружается, когда тип класса представ­ ляет множество значений, для которого индексирование имеет смысл. Операция индексирования должна возвращать ссылку на элемент, содержащийся в множе­ стве. Покажем это на примере класса Vect, предназначенного для хранения мас­ сива целых чисел и безопасной работы с ним:

#1nclucle #1nclude class Vect{ public:

explicit Vect(int n = 10):

VectCconst int a[]. int n): //инициализация массивом -VectO { delete [] p: } int& operator [] (int i):

void PrintO:

private:

Vect::Vect(int n) : size(n){ Vect::Vect(const int a[]. int n) : size(n){ p = new intCsize]:

for (int i = 0: i < size: i++) p[i] = a[i]:

// Перегрузка операции индексирования:

int& Vect::operator [] (int i){ cout « "Завершение программы" « endl:

return p[i]:

void Vect::Print(){ int ma1n(){ int агг[10] = {1. 2. 3. 4. 5. 6. 7. 8. 9. 10};

Vect а(агг, 10);

Результат работы программы:

Неверный индекс (1 = 12) Завершение программы Перегруженная операция индексирования получает целый аргумент и проверяет, лежит ли его значение в пределах диапазона массива. Если да, то возвращается ад­ рес элемента, что соответствует семантике стандартной операции индексирования.

В данном примере конструктор с параметром по умолчанию объявлен как explicit для того, чтобы он не являлся конструктором преобразования типа (см.

с. 185), вызываемым неявно. Ключевое слово explicit указывает на то, что этот конструктор будет вызываться только явным образом.

Операцию [] можно определять только как метод класса.

Указатели на элементы классов К элементам классов можно обращаться с помощью указателей. Для этого опре­ делены операции.* и ->*. Указатели на поля и методы класса определяются по-разному.

Формат указателя на метод класса:

возвр^тип (имя^класса::*имя_указателя)(параметры);

Например, описание указателя на методы класса monstr int gethealth() {return health;} int get^ammoO {return ammo:} (a также на другие методы этого класса с такой же сигнатурой) будет иметь вид:

int (monstr:: *pget)():

Такой указатель можно задавать в качестве параметра функции. Это дает воз­ можность передавать в функцию имя метода:

void fun(int (monstr:: *pget)()){ (*this.*pget)(): // Вызов функции через операцию.* (this->*pget)(): // Вызов функции через операцию ->* Можно настроить указатель на конкретный метод с помощью операции взятия адреса:

// Присваивание значения указателю:

pget « & monstr::get_health;

monstr Vasia. *p;

p = new monstr;

// Вызов функции через операцию. :

int Vasin^health - (Vasia.*pget)();

// Вызов функции через операцию ->* :

int pheaUh = (p->*pget)();

Ниже перечислены правила использования указателей на методы классов.

• Указателю на метод можно присваивать только адреса методов, имеющих со­ ответствующий заголовок.

• Нельзя определить указатель на статический метод класса.

• Нельзя преобразовать указатель па метод в указатель на обычную функцию, не являющуюся элементом класса.

Как и указатели на обычные функции, указатели на методы используются в том случае, когда возникает необходимость вызвать метод, имя которого неизвестно.

Однако в отличие указателя на переменную или обычную функцию, указатель на метод не ссылается на определенный адрес памяти. Он больше похож на индекс в массиве, поскольку задает смещение. Конкретный адрес в памяти получается путем сочетания указателя на метод с указателем на определенный объект.

ПРИМЕЧАНИЕ

Методы, вызываемые через указатели, могут быть виртуальными. При этом вызывается метод, соответствующий типу объекта, к которому применялся указатель.

Формат указателя на поле класса:

тип_данных(иня_класса::*имя_указателя):

В определение указателя можно включить его инициализацию в форме:

&имя_класса: :имяполя;// Поле должно быть public Если бы поле health было объявлено как public, определение указателя на него имело бы вид:

int (monstr::*phealth) » &nranstr::health:

cout « Vasia.*phealth: / / Обращение через операцию.* cout « p->*phealth: / / Обращение через операцию ->* Обратите внимание на то, что указатели на поля классов не являются обычными указателями — ведь при присваивании им значений они не ссылаются на кон­ кретный адрес памяти, поскольку память выделяется не под классы, а под объек­ ты классов.

Рекомендации по составу класса Как правило, класс как тип, определенный пользователем, должен содержать скрытые (private) поля и следующие функции:

а конструкторы, определяющие, как инициализируются объекты класса;

• набор методов, реализующих свойства класса (при этом методы, возвращаю­ щие значения скрытых полей класса, описываются с модификатором const, указывающим, что они не должны изменять значения полей);

• набор операций, позволяющих копировать, присваивать, сравнивать объекты и производить с ними другие действия, требующиеся по сути класса;

• класс исключений, используемый для сообщений об ошибках с помощью гене­ рации исключительных ситуаций (об этом будет рассказано на с. 228).

Если существуют функции, которые работают с классом или несколькими клас­ сами через интерфейс (то есть доступ к скрытым полям им не требуется), можно описать их вне классов, чтобы не перегружать интерфейсы, а для обеспечения логической связи поместить их в общее с этими классами пространство имен, на­ пример:

namespace Staff{ void interact(hero, monstr):

ГЛАВА Наследование Механизм наследования классов позволяет строить иерархии, в которых произ­ водные классы получают элементы родительских, или базовых, классов и могут дополнять их или изменять их свойства. При большом количестве никак не свя­ занных классов управлять ими становится невозможным. Наследование позво­ ляет справиться с этой проблемой путем упорядочивания и ранжирования клас­ сов, то есть объединения общих для нескольких классов свойств в одном классе и использования его в качестве базового.

Классы, находящиеся ближе к началу иерархии, объединяют в себе наиболее об­ щие черты для всех нижележащих классов. По мере продвижения вниз по иерар­ хии классы приобретают все больше конкретных черт. Множественное наследо­ вание позволяет одному классу обладать свойствами двух и более родительских классов.

Ключи доступа При описании класса в его заголовке перечисляются все классы, являющиеся для него базовыми. Возможность обращения к элементам этих классов регулиру­ ется с помощью ключей доступа private, protected и public:

class имя : [private | protected | public] базовый_класс { тело класса };

Если базовых классов несколько, они перечисляются через запятую. Ключ досту­ па может стоять перед каждым классом, например:

class D: А. protected В. public С {... }:

По умолчанию для классов используется ключ доступа private, а для структур — public.

Глава 5. Наследование До сих пор мы рассматривали только применяемые к элементам класса спецификаторы доступа private и public. Для любого элемента класса может также ис­ пользоваться спецификатор protected, который для одиночных классов, не вхо­ дящих в иерархию, равносилен private. Разница между ними проявляется при наследовании, что можно видеть из приведенной таблицы:

Ключ доступа Как видно из таблицы, private элементы базового класса в производном классе недоступны вне зависимости от ключа. Обращение к ним может осуществляться только через методы базового класса.

Элементы protected при наследовании с ключом private становятся в производ­ ном классе private, в остальных случаях права доступа к ним не изменяются.

Доступ к элементам public при наследовании становится соответствующим клю­ чу доступа.

Если базовый класс наследуется с ключом private, можно выборочно сделать некоторые его элементы доступными в производном классе, объявив их в сек­ ции public производного класса с помощью операции доступа к области види­ мости:

class Base{ public: void f();

class Derived : private Base{ public: Base: :vo1d f O :

Простое наследование Простым называется наследование, при котором производный класс имеет одно­ го родителя. Для различных методов класса существуют разные правила наслеЧасть li. Объектно-ориентированное программирование дования — например, конструкторы и операция присваивания в производном классе не наследуются, а деструкторы наследуются. Рассмотрим наследование классов и проблемы, возникающие при этом, на примере.

Создадим производный от класса monstr класс daemon, добавив полезную в неко­ торых случаях способность думать. Базовый класс приведен с некоторыми мето­ дами, добавленными в предыдущих разделах:

enum color {red. green, blue};

class monstr{ monstrCcolor sk);

monstrCchar * nam);

monstr(monstr &M);

-monstrO {delete [ ] name;} monstr& operator ++(){ monstr operator ++(int){ monstr M(*this); health++: return M;

bool operator >(monstr &M){ 1f( health > M.health) return true;

const monstr& operator = (monstr &M){ health = M.health; ammo = M.ammo; skin ^ M.skin;

int get_health() const {return health;} int getammo() const {return ammo;} void change_health(int he){ health « he;} void draw(int x. int y, int scale, int position):

monstr::monstr(int he. int am):

health (he), ammo (am), skin (red), name (0){} monstr::monstr(monstr &M){ name = new char [strlen(M.name) + 1];

strcpy(name. M.name);} health - M.health; ammo - M.ammo; skin = M.skin;

monstr::monstr(color sk){ switch (sk){ monstr::monstr(char * nam){ name = new char [strlen(nam) + 1];

strcpy(name, nam);

health = 100; ammo = 10: skin « red;

void monstr::draw(int x, int y. int scale, int position) { /*.. Отрисовка monstr */ } class daemon : public monstr{ daemon(int br « 10){brain = br;};

daemon(color sk) : monstr (sk) {brain = 10;} daemon(char * nam) : monstr (nam) {brain = 10:} daemon(daemon &M) : monstr (M) {brain = M.brain;} const daemon& operator = (daemon &M){ void drawCint х. int у. int scale, int position):

void daemon: :think(){ /*.. */ } void daemon::draw(int x. int y. int scale, int position) { /*.. Отрисовка daemon */ } В классе daemon введено поле brain и метод think, определены собственные конст­ рукторы и операция присваивания, а также переопределен метод отрисовки draw.

Все поля класса monstr, операции (кроме присваивания) и методы get^health, get^ammo и change_health наследуются в классе daemon, а деструктор формируется по умолчанию.

Рассмотрим правила наследования различных методов.

Конструкторы не наследуются, поэтому производный класс должен иметь собст­ венные конструкторы. Порядок вызова конструкторов определяется приведен­ ными ниже правилами.

• Если в конструкторе производного класса явный вызов конструктора базового класса отсутствует, автоматически вызывается конструктор базового клас­ са по умолчанию (то есть тот, который можно вызвать без параметров). Это использовано в первом из конструкторов класса daemon.

а Для иерархии, состоящей из нескольких уровней, конструкторы базовых классов вызываются начиная с самого верхнего уровня. После этого выпол­ няются конструкторы тех элементов класса, которые являются объектами, в порядке их объявления в классе, а затем исполняется конструктор класса.

• В случае нескольких базовых классов их конструкторы вызываются в поряд­ ке объявления.

ВНИМАНИЕ

Если конструктор базового класса требует указания параметров, он должен быть явным образом вызван в конструкторе производного класса в списке инициализации (это проде­ монстрировано в трех последних конструкторах).

Не наследуется и операция присваивания, поэтому ее также требуется явно определить в классе daemon. Обратите внимание на запись функции-операции:

в ее теле применен явный вызов функции-операции присваивания из базового класса. Чтобы лучше представить себе синтаксис вызова, ключевое слово operator вместе со знаком операции можно интерпретировать как имя функ­ ции-операции.

Вызов функций базового класса предпочтительнее копирования фрагментов кода из функций базового класса в функции производного. Кроме сокращения объема кода, этим достигается упрощение модификации программы: изменения требуется вносить только в одну точку программы, что сокращает количество возможных ошибок.

Ниже перечислены правила наследования деструкторов, • Деструкторы не наследуются, и если программист не описал в производном классе деструктор, он формируется по умолчанию и вызывает деструкторы всех базовых классов.

• В отличие от конструкторов, при написании деструктора производного клас­ са в нем не требуется явно вызывать деструкторы базовых классов, посколь­ ку это будет сделано автоматически.

• Для иерархии классов, состоящей из нескольких уровней, деструкторы вызы­ ваются в порядке, строго обратном вызову конструкторов: сначала вызыва­ ется деструктор класса, затем — деструкторы элементов класса, а потом де­ структор базового класса.

Поля, унаследованные из класса monstr, недоступны функциям производного класса, поскольку они определены в базовом классе как private. Если функциям, определенным в daemon, требуется работать с этими полями, можно либо описать их в базовом классе как protected, либо обращаться к ним с помощью функций из monstr, либо явно переопределить их в daemon так, как было показано в предыду­ щем разделе (с. 201).

Рассматривая наследование методов, обратите внимание на то, что в классе daemon описан метод draw, переопределяющий метод с тем же именем в классе monstr (поскольку отрисовка различных персонажей, естественно, выполняется по-разному). Таким образом, производный класс может не только дополнять, но и корректировать поведение базового класса^. Доступ к переопределенному ме­ тоду базового класса для производного класса выполняется через имя, уточнен­ ное с помощью операции доступа к области видимости (::).

Виртуальные методы Работа с объектами чаще всего производится через указатели. Указателю на ба­ зовый класс можно присвоить значение адреса объекта любого производного класса, например:

/ / Описывается указатель на базовый класс:

monstr *р:

/ / Указатель ссылается на объект производного класса:

р = new daemon:

Вызов методов объекта происходит в соответствии с типом указателя, а не фак­ тическим типом объекта, на который он ссылается, поэтому при выполнении оператора, например, p->draw(l. 1. 1. 1):

будет вызван метод класса monstr, а не класса daemon, поскольку ссылки на методы разрешаются во время компоновки программы. Этот процесс,называется ранним ^ Переопределять в производном классе рекомендуется только виртуальные методы (см.

след. раздел).

связыванием. Чтобы вызвать метод класса daemon, можно использовать явное пре­ образование типа указателя:

(daemon * p)->draw(l. 1. 1. 1);

Это не всегда возможно, поскольку в разное время указатель может ссылаться на объекты разных классов иерархии, и во время компиляции программы конкрет­ ный класс может быть неизвестен. В качестве примера можно привести функ­ цию, параметром которой является указатель на объект базового класса. На его место во время выполнения программы может быть передан указатель на любой производный класс. Другой пример — связный список указателей па различные объекты иерархии, с которым требуется работать единообразно.

В C++ реализован механизм позднего связывания, когда разрешение ссылок на метод происходит на этапе выполнения программы в зависимости от конкретно­ го типа объекта, вызвавшего метод. Этот механизм реализован с помощью вирту­ альных методов и рассмотрен в следующем разделе.

Для определения виртуального метода используется спецификатор virtual, на­ пример:

virtual void draw(int х. int у. int scale, int position);

Рассмотрим правила описания и использования виртуальных методов.

• Если в базовом классе метод определен как виртуальный, метод, определенный в производном классе с тем же именем и набором параметров, автоматически становится виртуальным, а с отличающимся набором параметров — обычным.

• Виртуальные методы наследуются, то есть переопределять их в производном классе требуется только при необходимости задать отличающиеся действия.

Права доступа при переопределении изменить нельзя.

• Если виртуальный метод переопределен в производном классе, объекты этого класса могут получить доступ к методу базового класса с помощью операции доступа к области видимости.

• Виртуальный метод не может объявляться с модификатором static, но может быть объявлен как дружественный.

• Если в классе вводится описание виртуального метода, он должен быть опре­ делен хотя бы как чисто виртуальный.

Чисто виртуальный метод содержит признак » О вместо тела, например:

virtual void f(int) = 0;

Чисто виртуальный метод должен переопределяться в производном классе (воз­ можно, опять как чисто виртуальный).

Если определить метод draw в классеraonstrкак виртуальный, решение о том, ме­ тод какого класса вызвать, будет приршматься в зависимости от типа объекта, на который ссылается указатель:

r->draw(l. 1. 1. 1); // Вызывается метод monstr::draw p->draw(l. 1. 1. 1); // Вызывается метод daemon::draw р-> monstr: rdrawd, 1. 1. 1 ) ; / / Обход механизма виртуальных методов Если объект класса daemon будет вызывать метод draw не непосредственно, а кос­ венно (то есть из другого метода, определенного в классе monstr), будет вызван метод draw класса daemon.

Итак, виртуальным называется методу ссылка на который разрешается на этапе выполнения программы (перевод красивого английского слова virtual — в данном значении всего-навсего «фактический», то есть ссылка разрешается по факту вы­ зова).

Механизм позднего связывания Для каждого класса (не объекта!), содержащего хотя бы один виртуальный ме­ тод, компилятор создает таблиг^у виртуальных методов (vtbl), в которой для ка­ ждого виртуального метода записан его адрес в памяти. Адреса методов содер­ жатся в таблице в порядке их описания в классах. Адрес любого виртуального метода имеет в vtbl одно и то же смещение для каждого класса в пределах иерар­ хии.

Каждый объект содержит скрытое дополнительное поле ссылки на vtb1, называе­ мое vptr. Оно заполняется конструктором при создании объекта (для этого ком­ пилятор добавляет в начало тела конструктора соответствующие инструкции).

На этапе компиляции ссылки на виртуальные методы заменяются на обращения к vtbl через vptr объекта, а на этапе выполнения в момент обращения к методу его адрес выбирается из таблицы. Таким образом, вызов виртуального метода, в отличие от обычных методов и функций, выполняется через дополнительный этап получения адреса метода из таблицы. Это несколько замедляет выполнение программы.

Рекомендуется делать виртуальными деструкторы для того, чтобы гарантиро­ вать правильное освобождение памяти из-под динамического объекта, поскольку в этом слз^ае в любой момент времени будет выбран деструктор, соответствую­ щий фактическому типу объекта. Деструктор передает операции delete размер объекта, имеющий тип size_t. Если удаляемый объект является производным и в нем не определен виртуальный деструктор, передаваемый размер объекта мо­ жет оказаться неправильным.

Четкого правила, по которому метод следует делать виртуальным, не существует.

Можно только дать рекомендацию объявлять виртуальными методы, для кото­ рых есть вероятность, что они будут переопределены в производных классах.

Методы, которые во всей иерархии останутся неизменными или те, которыми производные классы пользоваться не будут, делать виртуальными нет смысла.

С другой стороны, при проектировании иерархии не всегда можно предсказать, каким образом будут расширяться базовые классы (особенно при проектирова­ нии библиотек классов), а объявление метода виртуальным обеспечивает гиб­ кость и возможность расширения.

Для пояснения последнего тезиса представим себе, что вызов метода draw осуще­ ствляется из метода перемещения объекта. Если текст метода перемещения не зависит от типа перемещаемого объекта (поскольку принцип перемещения всех объектов одинаков, а для отрисовки вызывается конкретный метод), переопреде­ лять этот метод в производных классах нет необходимости, и он может быть опи­ сан как невиртуальный. Если метод draw виртуальный, метод перемещения смо­ жет без перекомпиляции работать с объектами любых производных классов — даже тех, о которых при его написании ничего известно не было.

Виртуальный механизм работает только при использовании указателей или ссы­ лок на объекты. Объект, определенный через указатель или ссылку и содержа­ щий виртуальные методы, называется полиморфным. В данном случае полимор­ физм состоит в том, что с помощью одного и того же обращения к методу выполняются различные действия в зависимости от типа, на который ссылается указатель в каждый момент времени.

Абстрактные классы Класс, содержащий хотя бы один чисто виртуальный метод, называется абст­ рактным. Абстрактные классы предназначены для представления общих понятий, которые предполагается конкретизировать в производных классах. Абстрактный класс может использоваться только в качестве базового для других классов — объ­ екты абстрактного класса создавать нельзя, поскольку прямой или косвенный вы­ зов чисто виртуального метода приводит к ошибке при выполнении.

При определении абстрактного класса необходимо иметь в виду следующее:

• абстрактный класс нельзя использовать при явном приведении типов, для описания типа параметра и типа возвращаемого функцией значения;

• допускается объявлять указатели и ссылки на абстрактный класс, если при инициализации не требуется создавать временный объект;

• если класс, производный от абстрактного, не определяет все чисто виртуаль­ ные функции, он также является абстрактным.

Таким образом, можно создать функцию, параметром которой является указа­ тель на абстрактный класс. На место этого параметра при выполнении програм­ мы может передаваться указатель на объект любого производного класса. Это по­ зволяет создавать полиморфные функции^ работающие с объектом любого типа в пределах одной иерархии.

Множественное наследование Множественное наследование означает, что класс имеет несколько базовых клас­ сов. Если в базовых классах есть одноименные элементы, при этом может про­ изойти конфликт идентификаторов, который устраняется с помощью операции доступа к области видимости:

class monstr{ public: int gethedlth():

class hero{ public: int get_health();

class ostrich: public monstr. public hero{ int main(){ cout « A.monstr::get_health():

cout « A.hero: :gethealth():

Как видно из примера, для вызова метода gethealth требуется явно указывать класс, в котором он описан. Использование обычной для вызова метода класса конструкции A.get_health() приведет к ошибке, поскольку компилятор не в состоянии разобраться, к методу какого из базовых классов требуется обра­ титься.

Если у базовых классов есть общий предок, это приведет к тому, что производ­ ный от этих базовых класс унаследует два экземпляра полей предка, что чаще всего является нежелательным. Чтобы избежать такой ситуации, требуется при наследовании общего предка определить его как виртуальный класс:

class monstr{ class daemon: virtual public monstr{ class lady: virtual public monstr{ class baby: public daemon, public lady{ Класс baby содержит только один экземпляр полей класса monstr. Если базовый класс наследуется и как виртуальный, и обычным образом, в производном классе будут присутствовать отдельные экземпляры для каждого невиртуального вхож­ дения и еще один экземпляр для виртуального.

Множественное наследование применяется для того, чтобы обеспечить произ­ водный класс свойствами двух или более базовых. Чаще всего один из этих классов является основным, а другие обеспечивают некоторые дополнительные свойства, поэтому они называются классами подмешивания. По возможности классы подмешивания должны быть виртуальными и создаваться с помощью конструкторов без параметров, что позволяет избежать многих проблем, возни­ кающих при ромбовидном наследовании (когда у базовых классов есть общий предок).

Отличия структур и объединений от классов Структуры (struct) и объединения (union) представляют собой частные случаи классов.

Структуры отличаются от классов тем, что доступ к элементам, а также базовый класс при наследовании по умолчанию считаются public. Структуры предпочти­ тельнее использовать для классов, все элементы которых доступны.

Отличия объединений от классов перечислены ниже:

• доступ в объединениях по умолчанию риЫ ic, кроме того, в них вообще нельзя явным образом использовать спецификаторы доступа;

• объединение не может участвовать в иерархии классов;

• элементами объединения не могут быть объекты, содержащие конструкторы и деструкторы;

• объединение может иметь конструктор и другие методы, только не статиче­ ские;

• в анонимном объединении нельзя описывать методы.

ГЛАВА Шаблоны классов В первой части этой книги были рассмотрены шаблоны функций (с. 85), с помо­ щью которых можно отделить алгоритм от конкретных типов данных, с которыми он работает, передавая тип в качестве параметра. Шаблоны классов предоставляют аналогичную возможность, позволяя создавать параметризованные классы.

Параметризованный класс создает семейство родственных классов, которые можно применять к любому типу данных, передаваемому в качестве параметра.

Наиболее широкое применение шаблоны находят при создании контейнерных классов. Контейнерным называется класс, который предназначен для хранения каким-либо образом организованных данных и работы с ними. Стандартная биб­ лиотека C++ содержит множество контейнерных классов для организации структур данных различного вида (они описаны в разделе «Контейнерные клас­ сы» на с. 295).

Преимущество использования шаблонов состоит в том, что как только алгоритм работы с данными определен и отлажен, он может применяться к любым типам данных без переписывания кода.

Создание шаблонов классов Рассмотрим процесс создания шаблона класса на примере. В разделе «Линейные списки» (с. 115) был описан двусвязный список и приведены алгоритмы работы с ним. Поскольку списки часто применяются для организации данных, удобно описать список в виде класса, а так как может потребоваться хранить данные раз­ личных типов, этот класс должен быть параметризованным.

Сначала рассмотрим непараметризованную версию класса «список».

Список состоит из узлов, связанных между собой с помощью указателей. Каж­ дый узел хранит целое число, являющееся ключом списка. Опишем вспомога­ тельный класс для представления одного узла списка:

class Nocle{ Поскольку этот класс будет описан внутри класса, представляющего список, поля для простоты доступа из внешнего класса сделаны доступными (public).

Это позволяет обойтись без функций доступа и изменения полей. Назовем класс списка List:

class L1st{ Node *pbeg. *pend: // Указатели на начало и конец списка List(){pbeg = 0; pend =0;} //Конструктор // Вставка узла d после узла с ключом key:

void print_back(): // Печать списка в обратном направлении Рассмотрим реализацию методов класса. Метод add выделяет память под новый объект типа Node и присоединяет его к списку, обновляя указатели на его начало и конец:

void List::add(int d){ Node *pv = new Node(d): // Выделение памяти под новый узел if (pbeg == 0)pbeg = pend = pv: // Первый узел списка // Связывание нового узла с предыдущим:

При желании получить отсортированный список этот метод можно заменить на метод, аналогичный функции формирования отсортированного списка add_sort, приведенной в разделе «Линейные списки» на с. 119.

Метод f i nd выполняет поиск узла с заданным ключом и возвращает указатель на него в случае успешного поиска и О в случае отсутствия такого узла в списке:

Метод insert вставляет в список узел после узла с ключом key и возвращает ука­ затель на вставленный узел. Если такого узла в списке нет, вставка не выполня­ ется и возвращается значение 0:

if(Node *pkey = find(key)){ / / Поиск узла с ключом key / / Выделение памяти под новый узел и его инициализация:

/ / Установление связи нового узла с последующим:

/ / Установление связи нового узла с предыдущим:

/ / Установление связи предыдущего узла с новым:

/ / Установление связи последующего узла с новым:

i f ( ркеу != pend) (pv->next)->prev = pv;

/ / Обновление указателя на конец списка.

/ / если узел вставляется в конец:

Метод remove удаляет узел с заданным ключом из списка и возвращает значение true в случае у-спешного удаления и false, если узел с таким ключом в списке не найден:

bool List::remove(int key){ if(Node *pkey = find(key)){ else i f (pkey == pend){ / / Удаление из конца списка (pkey->prev)->next « pkey->next;

(pkey->next)->prev = pkey->prev;} return false;

Методы печати списка в прямом и обратном направлении поэлементно просмат­ ривают список, переходя по соответствующим ссылкам:

void List::print_back(){ Деструктор списка освобождает память из-под всех его элементов:

List::4ist(){ Ниже прив(еден пример программы, использующей класс List. Программа ана­ логична приведенной на с. 116: она формирует список из 5 чисел, выводит его на экран, добавляет число в список, удаляет число из списка и снова выводит его на экран:

i n t main(){ При определении экземпляров шаблона Block с параметрами int и 100 будет за­ действован специализированный вариант.

Достоинства и недостатки шаблонов Шаблоны представляют собой мощное и эффективное средство обращения с раз­ личными типами данных, которое можно назвать параметрическим полиморфиз­ мом, а также обеспечивают безопасное использование типов, в отличие от макро­ сов препроцессора. Однако следует иметь в виду, что программа, использующая шаблоны, содержит полный код для каждого порожденного типа, что может уве­ личить размер исполняемого файла. Кроме того, с некоторыми типами данных шаблоны могут работать менее эффективно, чем с другими. В этом случае имеет смысл использовать специализацию шаблона.

Стандартная библиотека C++ предоставляет большой набор шаблонов для раз­ личных способов организации хранения и обработки данных (см. раздел «Кон­ тейнерные классы», с. 295).

ГЛАВА Обработка исключительных ситуаций Исключительная ситуация, или исключение — это возникновение непредвиден­ ного или аварийного события, которое может порождаться некорректным ис­ пользованием аппаратуры. Например, это деление на ноль или обращение по не­ существующему адресу памяти. Обычно эти события приводят к завершению программы с системным сообщением об ошибке. C++ дает программисту воз­ можность восстанавливать программу и продолжать ее выполнение.

Исключения C++ не поддерживают обработку асинхронных событий, таких, как ошибки оборудования или обработку прерываний, например, нажатие клавиш Ctrl+C. Механизм исключений предназначен только для событий, которые про­ исходят в результате работы самой программы и указываются явным образом.

Исключения возникают тогда, когда некоторая часть программы не смогла сде­ лать то, что от нее требовалось. При этом другая часть программы может попы­ таться сделать что-нибудь иное.

Исключения позволяют логически разделить вычислительный процесс на две час­ ти — обнаружение аварийной ситуации и ее обработка. Это важно не только для лучшей структуризации программы. Главной причиной является то, что функ­ ция, обнаружившая ошибку, может не знать, что предпринимать для ее исправ­ ления, а использующий эту функцию код может знать, что делать, но не уметь определить место возникновения. Это особенно актуально при использовании библиотечных функций и программ, состоящих из многих модулей.

Другое достоинство исключений состоит в том, что для передачи информации об ошибке в вызывающую функцию не требуется применять возвращаемое значе­ ние, параметры или глобальные переменные, поэтому интерфейс функций не раздувается. Это особенно важно, например, для конструкторов, которые по син­ таксису не могут возвращать значение.

ПРИМЕЧАНИЕ

В принципе, ничто не мешает рассматривать в качестве исключений не только ошибки, но и нормальные ситуации, возникающие при обработке данных, но это не имеет преиму­ ществ перед другими решениями и не улучшает структуру и читаемость программы.

Общий механизм обработки исключений Место, в котором может произойти ошибка, должно входить в контролируемый блок — составной оператор, перед которым записано ключевое слово try.

Рассмотрим, каким образом реализуется обработка исключительных ситуаций.

• Обработка исключения начинается с появления ошибки. Функция, в которой она возникла, генерирует исключение. Для этого используется ключевое сло­ во throw с параметром, определяющим вид исключения. Параметр может быть константой, переменной или объектом и используется для передачи инфор­ мации об исключении его обработчику.

• Отыскивается соответствующий обработчик исключения и ему передается управление.

• Если обработчик исключения не найден, вызывается стандартная функция terminate, которая вызывает функцию abort, аварийно завершающую текущий процесс. Можно установить собственную функцию завершения процесса.

В первой части книги (см. раздел «Функции», с. 73) говорилось о том, что при вызове каждой функции в стеке создается область памяти для хранения локаль­ ных переменных и адреса возврата в вызывающую функцию. Термин стек вызо­ вов обозначает последовательность вызванных, но еще не завершившихся функ­ ций. Раскручиванием стека называется процесс освобождения памяти из-под локальных переменных и возврата управления вызывающей функции. Когда функция завершается, происходит естественное раскручивание стека. Тот же са­ мый механизм используется и при обработке исключений. Поэтому после того, как исключение было зафиксировано, исполнение не может быть продолжено с точки генерации исключения. Подробнее этот механизм рассматривается в сле­ дующем разделе.

Синтаксис исключений Ключевое слово try служит для обозначения контролируемого блока — кода, в котором может генерироваться исключение. Блок заключается в фигурные скобки:

Все функции, прямо или косвенно вызываемые из try-блока, также считаются ему принадлежащими.

Генерация (порождение) исключения происходит по ключевому слову throw, кото­ рое употребляется либо с параметром, либо без него:

throw [ выражение ];

Тип выражения, стоящего после throw, определяет тип порождаемого исключения.

При генерации исключения выполнение текущего блока прекращается, и происхо­ дит поиск соответствующего обработчика и передача ему управления. Как прави­ ло, исключение генерируется не непосредственно в try-блоке, а в функциях, прямо или косвенно в него вложенных.

Не всегда исключение, возникшее во внутреннем блоке, может быть сразу пра­ вильно обработано. В этом случае используются вложенные контролируемые блоки, и исключение передается на более высокий уровень с помощью ключево­ го слова throw без параметров.

Обработчики исключений начинаются с ключевого слова catch, за которым в скобках следует тип обрабатываемого исключения. Они должны располагаться непосредственно за try-блоком. Можно записать один или несколько обработчи­ ков в соответствии с типами обрабатываемых исключений. Синтаксис обработ­ чиков напоминает определение функции с одним параметром — типом исключе­ ния. Существует три формы записи:

catch(TMn имя){.. /* тело обработчика */ } catch(THn){.. /* тело обработчика */ } catch(...){. /* тело обработчика */ } Первая форма применяется, когда имя параметра используется в теле обработчи­ ка для выполнения каких-либо действий — например, вывода информации об ис­ ключении. Вторая форма не предполагает использования информации об исклю­ чении, играет роль только его тип. Многоточие вместо параметра обозначает, что обработчик перехватывает все исключения. Так как обработчики просматрива­ ются в том порядке, в котором они записаны, обработчик третьего типа следует помещать после всех остальных. Пример:

catchdnt 1){.. // Обработка исключений типа int catch(const char *){.. // Обработка исключений типа const char* catch(Overflow){.. // Обработка исключений класса Overflow catch(...){.. // Обработка всех необслуженных исключений После обработки исключения управление передается первому оператору, находя­ щемуся непосредственно за обработчиками исключений. Туда же, минуя код всех обработчиков, передается управление, если исключение в try-блоке не было сгенерировано.

Перехват исключений Когда с помощью throw генерируется исключение, функции исполнительной библиотеки C++ выполняют следующие действия:

1) создают копию параметра throw в виде статического объекта, который сущест­ вует до тех пор, пока исключение не будет обработано;

2) в поисках подходящего обработчика раскручивают стек, вызывая деструкто­ ры локальных объектов, выходящих из области действия;

3) передают объект и управление обработчику, имеющему параметр, совмести­ мый по типу с этим объектом.

При раскручивании стека все обработчики на каждом уровне просматриваются последовательно, от внутреннего блока к внешнему, пока не будет найден подхо­ дящий обработчик.

Обработчик считается найденным, если тип объекта, указанного после throw:

• тот же, что и указанный в параметре catch (параметр может быть записан в форме Т, const Т. Т или const Т&. где Т— тип исключения);

• является производным от указанного в параметре catch (если наследование производилось с ключом доступа public);

• является указателем, который может быть преобразован по стандартным пра­ вилам преобразования указателей к типу указателя в параметре catch.

Из вышеизложенного следует, что обработчики производных классов следует размещать до обработчиков базовых, поскольку в противном случае им никогда не будет передано управление. Обработчик указателя типа void автоматически скрывает указатель любого другого типа, поэтому его также следует размещать после обработчиков указателей конкретного типа.

Рассмотрим пример.

#include class Hello{ // Класс, информирующий о своем создании и уничтожении public:

Hello(){cout « "Hello!" « endl;} ~Hello(){cout « "Bye!" « endl;} void fl(){ ifstream ifs("\\INVALID\\FILE\\NAME"); // Открываем файл cout « "Генерируем исключение" « endl;

throw "Ошибка при открытии файла";} void f2(){ flO; // Вызываем функцию. генерирую1цую исключение int mainOi cout « "Входим в try-блок" « endl:

cout « "Выходим из try-блока" « епсП;

cout « "Вызван обработчик int. исключение - " « 1 « endl:

catchCconst char * p){ cout « "Вызван обработчик const char*, исключение - " catch(...){ cout « "Вызван обработчик всех исключений" « endl:

return 0: / / Все обошлось благополучно Результаты выполнения профаммы:

Входим в try-блок Hello!

Генерируем исключение Вызван обработчик const char *. исключение - Ошибка при открытии файла Обратите внимание, что после порождения исключения был вызван деструктор локального объекта, хотя управление из функции f 1 было передано обработчику, находящемуся в функции main. Сообщение «Выходим из try-блока» не было вы­ ведено. Для работы с файлом в программе использовались потоки (о них расска­ зывается в главе 10 «Потоковые классы» на с. 265).

Таким образом, механизм исключений позволяет корректно уничтожать объекты при возникновении ошибочных ситуаций. Поэтому выделение и освобождение ре­ сурсов полезно оформлять в виде классов^ конструктор которых выделяет ресурс, а деструктор освобождает. В качестве примера можно привести класс для работы с файлом. Конструктор класса открывает файл, а деструктор — закрывает. В этом случае есть гарантия, что при возникновении ошибки файл будет корректно за­ крыт, и информация не будет утеряна.

Как уже упоминалось, исключение может быть как стандартного, так и опреде­ ленного пользователем типа. При этом нет необходимости определять этот тип глобально — достаточно, чтобы он был известен в точке порождения исключения и в точке его обработки. Класс для представления исключения можно описать внутри класса, при работе с которым оно может возникать. Конструктор копиро­ вания этого класса должен быть объявлен как public, поскольку иначе будет не­ возможно создать копию объекта при генерации исключения (конструктор копи­ рования, создаваемый по умолчанию, имеет спецификатор public).

Глава 7. Обработка исключительных ситуаций Список исключений функции В заголовке функции можно задать список исключений, которые она может прямо или косвенно порождать. Поскольку заголовок является интерфейсом функции, указание в нем списка исключений дает пользователям функции необходимую информацию для ее использования, а также гарантию, что при возникновении непредвиденного исключения эта ситуация будет обнаружена.

Алгоритм обработки исключения представлен на рис. 7.1.

Типы исключений перечисляются в скобках через запятую после ключевого сло­ ва throw, расположенного за списком параметров функции, например:

void f l O throw (int. const char*){ /* Тело функции */ } void f2() throw (Oops*){ /* Тело функции */ } Функция fl должна генерировать исключения только типов int и const char*.

Функция f2 должна генерировать только исключения типа указателя на класс Oops или производных от него классов.

Если ключевое слово throw не указано, функция может генерировать любое ис­ ключение. Пустой список означает, что функция не должна порождать исключе­ ний:

void fO throw (){ // Тело функции, не порождающей исключений Исключения не входят в прототип функции. При переопределении в производ­ ном классе виртуальной функции можно задавать список исключений, такой же или более ограниченный, чем в соответствующей функции базового класса.

Указание списка исключений ни к чему не обязывает — функция может прямо или косвенно породить исключение, которое она обещала не использовать. Эта ситуация обнаруживается во время исполнения программы и приводит к вызову стандартной функции unexpected, которая по умолчанию просто вызывает функ­ цию terminate. С помощью функции set^unexpected можно установить собствен­ ную функцию, которая будет вызываться вместо terminate и определять действие программы при возникновении непредвиденной исключительной ситуации.

Функция terminate по умолчанию вызывает функцию abort, которая завершает выполнение программы. С помощью функции set_terminate можно установить собственную функцию, которая будет вызываться вместо abort и определять спо­ соб завершения программы. Функции set_unexpected и set^terminate описаны в заголовочном файле.

Исключения в конструкторах и деструкторах Язык C++ не позволяет возвращать значение из конструктора и деструктора.

Механизм исключений дает возможность сообщить об ошибке, возникшей в кон­ структоре или деструкторе объекта. Для иллюстрации создадим класс Vector, в котором ограничивается количество запрашиваемой памяти:

class Vector{ publicclass Size{}: // Класс исключения enum {max = 32000}: // Максимальная длина вектора При использовании класса Vector можно предусмотреть перехват исключений типа Size:

try{ Vector *р = new Vector(i):

catchCVector::S1ze){... / / Обработка ошибки размера вектора В обработчике может использоваться стандартный набор основных способов вы­ дачи сообщений об ошибке и восстановления. Внутри класса, определяющего ис­ ключение, может храниться информация об исключении, которая передается об­ работчику. Смысл этой техники заключается в том, чтобы обеспечить передачу информации об ошибке из точки ее обнаружения в место, где для обработки ошибки имеется достаточно возможностей.

Если в конструкторе объекта генерируется исключение, автоматически вызыва­ ются деструкторы для полностью созданных в этом блоке к текущему моменту объектов, а также для полей данных текущего объекта, являющихся объектами, и для его базовых классов. Например, если исключение возникло при создании массива объектов, деструкторы будут вызваны только для успешно созданных элементов.

Если объект создается в динамической памяти с помощью операции new и в кон­ структоре возникнет исключение, память из-под объекта корректно освобожда­ ется.

Иерархии исключений Использование собственных классов исключений предпочтительнее применения стандартных типов данных. С помощью классов можно более гибко организовать передачу информации об исключении, легче дифференцировать обработку исклю­ чений, а кроме того, появляется возможность использовать иерархии классов.

Поскольку механизм управления исключениями позволяет создать обработчик для базового класса, родственные исключения часто можно представить в виде иерархии. Производя исключения от общего базового класса, можно в обработ­ чике перехватывать ссылку или указатель на базовый класс, используя полимор­ физм. Например, в математической библиотеке можно организовать классы сле­ дующим образом:

class Matherr{}:

class Overflow: public Matherr{}: / / Переполнение class Underflow: public Matherr{}: / / Исчезновение порядка class ZeroDivide: public Matherr{}: / / Деление на ноль Для представления ошибок ввода/вывода могут использоваться следующие классы:

class IOerr{}:

class Readerr: public IOerr{}: / / Ошибка чтения class Writerr: public IOerr{}: / / Ошибка записи class Seekerr: public IOerr{}: / / Ошибка поиска В зависимости от обстоятельств можно использовать либо обработчик исключе­ ний базового класса, который будет перехватывать и производные исключения, либо собственные обработчики производных классов.

Существует ряд стандартных исключений, которые генерируются операциями или функциями C++ (см. главу 16 «Другие средства стандартной библиотеки», с. 378). Все они являются производными от библиотечного класса exception, опи­ санного в заголовочном файле. Например, операция new при неудач­ ном выделении памяти генерирует исключение типа Ьас1_аПос.

Программист может определить собственные исключения, производные от стан­ дартных.

ГЛАВА Преобразования типов При выполнении программы производятся явные и неявные преобразования ве­ личин из одного типа в другой. Неявные преобразования выполняются в соот­ ветствии с правилами, приведенными на с. 38 и в приложении 3. Для выполне­ ния явных преобразований типа в C++ существует целая группа операций — const_cast. clynamiccast, re1nterprGt_cast и stat1c_cast, а также операция приве­ дения типа, унаследованная из языка С, которая уже использовалась в этой кни­ ге (с. 56). Для начала рассмотрим ее подробнее.

Операция приведения типов в стиле С Операция может записываться в двух формах:

тип (выражение) (тип) выражение Результатом операции является значение заданного типа, например:

float b = 6.8;

Величина a преобразуется к типу double, a переменная b — к типу 1nt с отсечени­ ем дробной части, в обоих случаях внутренняя форма представления результата операции преобразования иная, чем форма исходного значения.

Необходимость в преобразовании типа возникает, например, в случае, когда функция возвращает указатель на тип void, который требуется присвоить пере­ менной конкретного типа для последующего выполнения с ней каких-либо дей­ ствий:

Явное преобразование типа является источником возможных ошибок, поскольку вся ответственность за его результат возлагается на программиста. Поэтому в C++ введены операции, позволяющие выполнять частичный контроль выполЧасть II. Объектно-ориентированное программирование няемых преобразований или сделать намерения программиста более явными для понимания. Рассмотренное выше преобразование в стиле С оставлено в C++ только для нисходящей совместимости, и использовать его не рекомендуется.

В зависимости от вида требуемого преобразования необходимо использовать со­ ответствующую ему операцию приведения типа.

ПРИМЕЧАНИЕ

В ранних версиях компиляторов C++ операции приведения типа const_cast, dynam1c_cast, reinterpret^cast и static^cast пе поддерживаются.

Операция const_cast Операция служит для удаления модификатора const. Как правило, она использу­ ется при передаче в функцию константного указателя на место формального па­ раметра, не имеющего модификатора const. Формат операции:

const_cast (выражение) Обозначе1П1ый тип должен быть таким же, как и тип выражения, за исключением модификатора const. Обычно это указатель. Операция формирует результат ука­ занного типа.

Необходимость введения этой операции обусловлена тем, что программист, реа­ лизующий функцию, не обязан описывать не изменяемые в ней формальные па­ раметры как const, хотя это и рекомендуется. Правила C++ запрещают передачу константного указателя на место обычного. Операция constcast введена для того, чтобы обойти это ограничение. Естественно, функция не должна пытаться изменить значение, на которое ссылается передаваемый указатель, иначе резуль­ тат выполнения программы не определен.

Пример:

void printdnt *р){ // Функция не изменяет значение *р const int *р:

/* pr1nt(p); Ошибка, поскольку р объявлен как указатель на константу */ Операция const_cast используется в том случае, когда программист уверен, что в теле функции значение, на которое ссылается указатель, не изменяется. Естест­ венно, если есть возможность добавить к описанию формального параметра мо­ дификатор const, это предпочтительнее использования преобразования типа при вызове функции.

Операция dynamic_cast Операция применяется для преобразования указателей родственных классов ие­ рархии, в основном — указателя базового класса в указатель на производный класс, при этом во время выполнения программы производится проверка допус­ тимости преобразования.

Формат операции:

dynamic_cast (выражение) Выражение должно быть указателем или ссылкой на класс, тип — базовым или про­ изводным для этого класса. После проверки допустимости преобразования в слу­ чае успешного выполнения операция формирует результат заданного типа, в противном случае для указателя результат равен нyлю^ а для ссылки порожда­ ется исключение bad^cast. Если заданный тип и тип выражения не относятся к одной иерархии, преобразование не допускается.

Преобразование из базового класса в производный называют понижающим (downcast), так как графически в иерархии наследования принято изображать производные классы ниже базовых. Приведение из производного класса в базо­ вый называют повышающим (upcast), а приведение между производными класса­ ми одного базового или, наоборот, между базовыми классами одного производ­ ного — перекрестным (crosscast).

Повышающее преобразование Выполнение с помощью операции clynam1c_cast повышающего преобразования равносильно простому присваиванию:

class В{ / *... * / };

class С: public В{ /*.. */ }:

В* b = clynam1ccast(c): / / Эквивалентно В* b = с;

Понижающее преобразование Чаще всего операция dynam1c_cast применяется при понижающем преобразова­ нии — когда компилятор не имеет возможности проверить правильность приве­ дения.

Производные классы могут содержать функции, которых нет в базовых классах.

Для их вызова через указатель базового класса нужно иметь уверенность, что этот указатель в действительности ссылается на объект производного класса. Та­ кая проверка производится в момент выполнения приведения типа с использова­ нием RTTI (run-time type information) — «информации о типе во время выполне­ ния программы». Для того чтобы проверка допустимости могла быть выполнена, аргумент операции dynamic_cast должен быть полиморфного типа, то есть иметь хотя бы один виртуальный метод (см. с. 205).

Если выражение равно нулю, результат также равен нулю.

ПРИМЕЧАНИЕ

Для использования RTTI необходимо подключить к программе заголовочный файл. Кроме того, необходимо, чтобы был установлен соответствующий режим ком­ пилятора.

Для полиморфного объекта реализация операции clynam1c_cast весьма эффектив­ на, поскольку ссылка на информацию о типе объекта заносится в таблицу вирту­ альных методов, и доступ к ней осуществляется легко.

С точки зрения логики требование, чтобы объект был полиморфным, также оп­ равдано: ведь если класс не имеет виртуальных методов, его нельзя безопасным образом использовать, не зная точный тип указателя. А если тип известен, ис­ пользовать операцию dynam1c_cast нет необходимости.

Результат примепения операции dynamic^cast к указателю всегда требуется про­ верять явным образом. В приведенном ниже примере описан полиморфный базо­ вый класс В и производный от него класс С, в котором определена функция f2.

Для того чтобы вызывать ее из функции demo только в случае, когда последней передается указатель на объект производного класса, используется операция dynamic^cast с проверкой результата преобразования:

#1nclude #include class B{ class C: public B{ public: void f2(){cout « "f2":}:

void demo(6* p){ C* с = dynamic_cast(p):

else cout « "Передан не класс С":

int main(){ demo(b): // Выдается сообщение "Передан не класс С" demo(c): // Выдается сообщение "f2" (правильно) При использовании в этом примере вместо dynamic_cast приведения типов в сти­ ле С, например:

проконтролировать допустимость операции невозможно, PI если указатель р на самом деле не ссылается на объект класса С, это приведет к ошибке.

Другим недостатком приведения в стиле С является невозможность преобра­ зования в производный виртуального базового класса, это запрещено синтаксиГлава 8. Преобразование типов чески. С помощью операции dynamic^cast такое преобразование возможно при условии, что класс является полиморфным и преобразование недвусмыслерню.

Рассмотрим пример, в котором выполняется понижающее преобразование вирту­ ального базового класса:

#1nclude #include class A{ public: virtual ~A(){}:} c]ass B: public virtual A{}:

class C: public virtual A{}:

class D: public B. public C{};

void demo(A *a){ D* d = dynamic_cast(a):

int main(){ D *d = new D; demo(d);



Pages:     | 1 || 3 | 4 |


Похожие работы:

«Программа по Искусство 8-9 классы Пояснительная записка Цели и задачи, решаемые при реализации рабочей программы: Цель программы — развитие опыта эмоционально-ценностного отношения к искусству как социокультурной форме освоения мира, воздействующей на человека и общество. Задачи реализации данного курса: —формирование целостного представления о роли искусства в культурноисторическом процессе развития человечества; —воспитание художественного вкуса; —приобретение культурно-познавательной,...»

«МУНИЦИПАЛЬНОЕ ОБЩЕОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ СРЕДНЯЯ ОБЩЕОБРАЗОВАТЕЛЬНАЯ ШКОЛА №37 РАССМОТРЕНО Педагогическим советом Протокол № __20_г. УТВЕРЖДАЮ Директор МОУ СОШ № 37 И.В. Голова __20_г. Основная образовательная программа начального общего образования МОУ СОШ №37 (1-4 классы) 2011 — 2015гг. СОДЕРЖАНИЕ • Целевой раздел • Пояснительная записка • Планируемые результаты освоения обучающимися основной образовательной программы начального общего образования • Система оценки достижения планируемых...»

«Государственное автономное образовательное учреждение высшего профессионального образования Тюменской области ТЮМЕНСКАЯ ГОСУДАРСТВЕННАЯ АКАДЕМИЯ МИРОВОЙ ЭКОНОМИКИ, УПРАВЛЕНИЯ И ПРАВА 2.5. Реализация образовательных программ СМК – РОП - РУП - 2.5.20 ПРОСТРАНСТВЕННАЯ ЭКОНОМИКА СОГЛАСОВАНО УТВЕРЖДЕНО Проректор по учебной работе Решением Учёного совета _ Т.А. Кольцова (протокол № 9 от 23.03.2011 г.) _ 2011 г. А. Г. ПОЛЯКОВА ПРОСТРАНСТВЕННАЯ ЭКОНОМИКА Рабочая учебная программа Направление подготовки...»

«УДК 334.012.23 Костюкевич Руслан Николаевич к.э.н., доц. кафедра Менеджмента Национальный университет водного хозяйства и природопользования Украина, г. Ровно МЕТОДИЧЕСКИЕ ПОДХОДЫ К РАЗРАБОТКЕ МЕХАНИЗМОВ ФИНАНСИРОВАНИЯ ПРИРОДООХРАННЫХ ПРОГРАММ METHODOLOGICAL APPROACHES TO THE DEVELOPMENT OF MECHANISMS FOR FINANCING ENVIRONMENTAL PROGRAMS Государственное регулирование в области охраны окружающей среды в Украине базируется на использовании программно-целевого подхода, который эффективно...»

«http://b2blogger.com/pressroom/562.pdf Softline открыла представительство в Омске 12 Октябрь, 2006 - Softline | ИТ: софт Новое региональное представительство Softline планирует вести активную деятельность на всей территории Омской области. Наши основные ориентиры – средний и крупный бизнес. Сейчас идет активная работа с партнерами, среди которых – консалтинговые компании, крупные поставщики компьютерной техники, системные интеграторы и т. д. В ближайших планах представительства – проведение...»

«Информация о рабочих программах в начальной школе по учебным предметам в 2013-2014 учебном году в МКОУ Школа-интернат № 1 Наименование, автор(ы), год издания Наименование УМК (наименование, автор) учебной программы, на основе которой рабочей программы составлена рабочая программа Обучение грамоте; чтение и развитие речи Рабочая программа по Программа Министерства образования РФ Букварь. Воронкова В.В., Коломыткина И.В. Учебник для 1 обучению грамоте для 1 класса для специальных (коррекционных)...»

«Проект 17августа 2011 г. БИО-2020 Программа развития биотехнологий в Российской Федерации на период до 2020 г. 1 СОДЕРЖАНИЕ Оглавление Содержание Введение Глава 1. Мировые тренды в развитии биотехнологий и позиции России Биофармацевтика Биомедицина Промышленные биотехнологии Биоэнергетика Сельскохозяйственные биотехнологии Биотехнологии для лесного сектора Морская биотехнология Биологические коллекции Глава 2. Цель и Задачи программы Показатели решения задач Программы Финансовое обеспечение...»

«Белорусский государственный университет УТВЕРЖДАЮ Проректор по учебной работе В.Л.Клюня (подпись) _20г. (дата утверждения) Регистрационный № УД-_/баз ПРОГРАММА ГОСУДАРСТВЕННОГО ЭКЗАМЕНА по специальности 1-25 01 01 Экономическая теория (первое высшее образование) 20_ г. Программа разработана кафедрой теоретической и институциональной экономики Зав. кафедрой _ П.С.Лемещенко Рассмотрена и рекомендована к утверждению на заседании кафедры теоретической и институциональной экономики экономического...»

«А.Д. Гетманова УЧЕБНИК ЛОГИКИ Со сборником задач 8е издание, переработанное УДК 373.167.1:16 ББК 87.4 Г44 Гетманова А.Д. Г44 Учебник логики. Со сборником задач : учебник / А.Д. Гетманова. — 8е изд., перераб. — М. : КНОРУС, 2011. — 368 с. ISBN 9785406011973 Учебник ориентирован на программу преподавания логики в вузах, но может также использоваться в качестве учебника в специализированных гимназиях, лицеях и колледжах. В целях развития творческо о мышле...»

«Программа восстановления жилого фонда Возрождение Нью-Йорка (NY Rising) Условия программы для съемного жилья 8/13/2014 9:22 AM Условия программы 1 Введение Ремонт Возмещение расходов Поднятие строения Добровольные мероприятия по повышению устойчивости Доплата для семей с низким и умеренным доходом (Lower Moderate Income Allowance, LMI) Максимальная сумма выплат по программе Вклад владельца недвижимости Базовые критерии правомочности участия в программе Требования, касающиеся страхования от...»

«УТВЕРЖДАЮ Министр связи и массовых коммуникаций Российской Федерации И.О.Щголев 24 февраля 2010 г. Отчет о деятельности Федерального агентства по печати и массовым коммуникациям за 2009 год В 2009 году Федеральное агентство по печати и массовым коммуникациям, руководствуясь Посланием Президента Российской Федерации Федеральному Собранию Российской Федерации, решениями Правительства Российской Федерации, Коллегии Министерства связи и массовых коммуникаций Российской Федерации, в соответствии с...»

«ФИНАНСОВАЯ АКАДЕМИЯ 297BФГОУ ПРИ ПРАВИТЕЛЬСТВЕ РОССИЙСКОЙ ФЕДЕРАЦИИ (ФИНАКАДЕМИЯ) Кафедра бухгалтерского учета С.Н. Гришкина, Ю.В. Щербинина БУХГАЛТЕРСКИЙ УЧЕТ Сборник задач Рекомендовано УМО по образованию в области финансов, учета и мировой экономики в качестве учебного пособия для студентов, обучающихся по экономическим специальностям МОСКВА 2010 1 УДК 06.047 ББК 65.052.2 Б94 Рецензенты: к.э.н., доц. И.Ф. Ветрова (Финакадемия) к.э.н., доц. Г.Н. Карпова (Всероссийская государственная...»

«Учебно-методических комплекс дисциплины Процессы получения наночастиц и наноматериалов. Нанотехнологии Разработали: Идентификационный номер: Контрольный экземпляр находится Лист 1 из 8 Горошко Д.Л., Ваванова С.В. УМКД 210602.65 на кафедре физики низкоразмерных струкСД.Ф.3. -2012 тур ШЕН ДВФУ МИНИСТЕРСТВО ОБРАЗОВАНИЯ И НАУКИ РОССИЙСКОЙ ФЕДЕРАЦИИ Федеральное государственное автономное образовательное учреждение высшего профессионального образования Дальневосточный федеральный университет (ДВФУ)...»

«Приложение 7Б: Рабочая программа дисциплины по выбору Методология и логика педагогических исследований ФЕДЕРАЛЬНОЕ ГОСУДАРСТВЕННОЕ БЮДЖЕТНОЕ ОБРАЗОВАТЕЛЬНОЕ УЧРЕЖДЕНИЕ ВЫСШЕГО ПРОФЕССИОНАЛЬНОГО ОБРАЗОВАНИЯ ПЯТИГОРСКИЙ ГОСУДАРСТВЕННЫЙ ЛИНГВИСТИЧЕСКИЙ УНИВЕРСИТЕТ Утверждаю Проректор по научной работе и развитию интеллектуального потенциала университета профессор З.А. Заврумов _2012 г. Аспирантура по специальности 13.00.01 Общая педагогика, история педагогики и образования отрасль науки: 13.00.00...»

«Православие и современность. Электронная библиотека. Священник Артемий Владимиров Учебник жизни Книга для чтения в семье и школе Рекомендована отделом дополнительного образования Министерства образовании России для использования в системе школьного образования По благословению Святейшего Патриарха Московского и всея Руси Алексия II © Издательство Православного Братства Святителя Филарета Митрополита Московского, Москва 1998. © Библиотека Веб-Центра Омега. Содержание Предисловие I. Господи,...»

«Рабочая программа Ф ТПУ 7.1-21/01 учебной дисциплины ФЕДЕРАЛЬНОЕ АГЕНТСТВО ПО ОБРАЗОВАНИЮ Государственное образовательное учреждение высшего профессионального образования ТОМСКИЙ ПОЛИТЕХНИЧЕСКИЙ УНИВЕРСИТЕТ УТВЕРЖДАЮ Декан ГФ ТПУ В.Г. Рубанов _2007 г. КУЛЬТУРОЛОГИЯ Рабочая программа для студентов ТПУ Обеспечивающая кафедра Культурологии и социальной коммуникации Курс Семестр 1, Учебный план набора 2007 г. Распределение учебного времени Лекции 36 час. Самостоятельная работа 72 час. Общая...»

«Государственное образовательное учреждение высшего профессионального образования Московской области Международный университет природы, общества и человека Дубна (университет Дубна) УТВЕРЖДАЮ проректор по учебной работе С.В. Моржухина __2010 г. ПРОГРАММА ДИСЦИПЛИНЫ МЕТРОЛОГИЯ, СТАНДАРТИЗАЦИЯ И СЕРТИФИКАЦИЯ (наименование дисциплины) по направлению 230105 Программное обеспечение вычислительной техники и автоматизированных систем Форма обучения: очная Уровень подготовки: бакалавр (выбрать нужное)...»

«Москва, 28-29 марта 2013 года четверг – пятница Место проведения: Москва, ГК Измайлово, Измайловское шоссе., 71 Исполнительная дирекция: ГК Открытый Диалог, 127549, г. Москва, Бибиревская ул., д. 8, стр. 1 Тел.: (495) 287-88-77; факс: (499) 922-12-02; e-mail: orgс[email protected] www.infotarif.ru ПРОГРАММА Регистрация участников семинара, приветственный кофе 8:30-10:00 Секция 1, часть 1: Качественные изменения государственной политики ценообразования 10:00-11: и регулирования тарифов на...»

«Программа вступительных испытаний по специальности 05.13.12 Системы автоматизации проектирования Содержание программы Раздел 1. Основные понятия и задачи автоматизированного проектирования Понятие инженерного проектирования. Принципы системного подхода. Основные понятия системотехники. Иерархическая структура проектных спецификаций и иерархические уровни проектирования. Стадии проектирования. Содержание технических заданий на проектирование. Классификация параметров, используемых при...»

«Министерство образования Республики Беларусь Учебно-методическое объединение высших учебных заведений Республики Беларусь по педагогическому образованию ТЕОРИЯ И МЕТОДИКА ОБУЧЕНИЯ ФИЗИКЕ Типовая учебная программа для высших учебных заведений по специальностям: 1-02 05 02 Физика; 1 -02 05 04 Физика. Дополнительная специальность СОСТАВИТЕЛИ: И.М. Елисеева, заведующая кафедрой методики преподавания физики учреждения образования Белорусский государственный педагогический университет имени Максима...»






 
2014 www.av.disus.ru - «Бесплатная электронная библиотека - Авторефераты, Диссертации, Монографии, Программы»

Материалы этого сайта размещены для ознакомления, все права принадлежат их авторам.
Если Вы не согласны с тем, что Ваш материал размещён на этом сайте, пожалуйста, напишите нам, мы в течении 1-2 рабочих дней удалим его.