close

Вход

Забыли?

вход по аккаунту

?

Bukunov Osnovy obektno-orient

код для вставкиСкачать
С. В. БУКУНОВ,
О. В. БУКУНОВА
С. В. БУКУНОВ, О. В. БУКУНОВА
ОСНОВЫ ОБЪЕКТНО-ОРИЕНТИРОВАННОГО ПРОГРАММИРОВАНИЯ
ОСНОВЫ ОБЪЕКТНО-ОРИЕНТИРОВАННОГО
ПРОГРАММИРОВАНИЯ
Министерство образования и науки
Российской Федерации
Санкт-Петербургский государственный
архитектурно-строительный университет
С. В. БУКУНОВ, О. В. БУКУНОВА
ОСНОВЫ ОБЪЕКТНО-ОРИЕНТИРОВАННОГО
ПРОГРАММИРОВАНИЯ
Учебное пособие
Санкт-Петербург
2017
1
УДК 681.3
ВВЕДЕНИЕ
Рецензенты: д-р физ.-мат. наук, профессор Б. Г. Вагер (СПбГАСУ);
д-р физ.-мат. наук, профессор Е. Л. Генихович
(Главная геофизическая обсерватория им. А. И. Воейкова)
Букунов, С. В.
Основы объектно-ориентированного программирования: учеб.
пособие / С. В. Букунов, О. В. Букунова; СПбГАСУ. – СПб., 2017. –
195 с.
ISBN 978-5-9227-0713-8
Содержит основные сведения об объектно-ориентированном подходе
к программированию на языке С++. Рассматриваются три важнейшие парадигмы объектно-ориентированного программирования: инкапсуляция,
наследование и полиморфизм. Особое внимание уделяется использованию
в программах таких сущностей, как классы и объекты.
Пособие состоит из девяти глав, содержащих необходимые теоретические сведения, упражнения и примеры программ с подробными комментариями, а также задания для самостоятельной работы.
Предназначено для студентов, обучающихся по специальностям «Прикладная математика» и «Прикладная математика и информатика».
Табл. 4. Ил. 191. Библиогр.: 3 назв.
Рекомендовано Учебно-методическим советом СПбГАСУ в качестве
учебного пособия.
ISBN 978-5-9227-0713-8
© С. В. Букунов, О. В. Букунова, 2017
© Санкт-Петербургский государственный
архитектурно-строительный университет, 2017
2
Настоящее пособие предназначено для студентов третьего
и четвертого курсов специальностей «Прикладная математика»
и «Прикладная математика и информатика», изучающих предмет
«Объектно-ориентированное программирование». В пособии излагается вторая часть курса, посвященная использованию объектноориентированного подхода при написании компьютерных программ.
Объектно-ориентированный подход принципиально отличается от более ранней методики процедурного программирования,
в основе которой лежал принцип разбиения программы на подпрограммы-процедуры и подпрограммы-функции. Программы, написанные в соответствии с принципами процедурного программирования, состоят из набора подпрограмм, причем для решения конкретной задачи программист явно указывает подробные инструкции.
Программы, созданные согласно принципам объектно-ориентированного программирования, пишутся совершенно иначе.
Основная работа состоит в продумывании и описании того, как
устроены классы, и лишь небольшая часть времени посвящается
решению конкретной задачи – написанию кода с использованием
созданных классов. Код с описанием класса может быть многократно использован без внесения каких-либо изменений. Именно
благодаря такому подходу объектно-ориентированное программирование приобрело огромную популярность.
Задачи настоящего пособия – познакомить студентов с основными парадигмами и сущностями объектно-ориентированной
концепции в программировании и помочь в выработке навыков
написания объектно-ориентированных программ. Изучение предлагаемого материала позволяет сформировать теоретическую и практическую базу для дальнейшего освоения принципов работы с различными библиотеками классов (библиотеки STL, MFC, Qt,
OpenGL и др.), которые широко используются в современном программировании.
Для написания всех программ использовалась интегрированная среда разработки Microsoft Visual Studio.
3
Глава 1. КЛАССЫ И ОБЪЕКТЫ. ЧАСТЬ I
1.1. Основные понятия
Основополагающей идеей объектно-ориентированного программирования является объединение данных и действий, производимых над ними, в единое целое, называемое объектом.
Класс является формой, определяющей, какие данные и функции будут включены в объект класса. Таким образом, класс
рассматривается как определяемый пользователем тип данных
и является описанием совокупности схожих между собой объектов.
При объявлении класса не создаются никакие объекты этого
класса (по аналогии с тем, как существование типа int не означает
существования переменных этого типа).
Объект является экземпляром некоторого класса и создается
как переменная типа класса, используемая для доступа к даннымчленам класса и для вызова методов или функций-членов класса.
При использовании объектно-ориентированного подхода
в случае, если необходимо считать какие-либо данные объекта,
нужно вызвать соответствующий метод, который выполнит считывание и возвратит требуемое значение. Прямой доступ к данным
невозможен. Данные сокрыты от внешнего воздействия, что защищает их от случайного изменения. Говорят, что «данные и методы
инкапсулированы». Термины «сокрытие» и «инкапсуляция данных» являются ключевыми в описании объектно-ориентированных
языков.
Если необходимо изменить данные объекта, эти действия также будут возложены на методы объекта. Никакие другие функции
не могут изменять данные класса.
Таким образом, в объектно-ориентированном программировании вместо разбиения задачи на функции производится разбиение
ее на объекты, а сама объектно-ориентированная программа на
языке С++ состоит из совокупности объектов, взаимодействующих
между собой посредством вызова методов друг друга.
В объектно-ориентированной программе в виде объектов могут представляться:
• физические объекты (автомобили – при моделировании
уличного движения; самолеты – при моделировании диспетчерской
4
системы; схемные элементы – при моделировании цепи электрического тока; ценные бумаги и т. д.);
• элементы интерфейса (окна; меню; графические объекты –
линии, прямоугольники, круги; мышь; клавиатура; дисковые
устройства; принтеры);
• структуры данных (массивы; стеки; связные списки; очереди);
• группы людей (сотрудники; студенты; покупатели; продавцы);
• базы данных (описи инвентаря; списки сотрудников; словари; географические координаты городов);
• пользовательские типы данных (время; даты; величины углов; точки на плоскости);
• математические конструкции (комплексные числа; обыкновенные дроби; полиномы);
• участники компьютерных игр (автомобили – в гонках; позиции – в шашках, шахматах; друзья и враги – в приключенческих
играх) и т. д.
Соответствие между программными и реальными объектами
устанавливается вследствие объединения данных и функций, которое является стержневой идеей объектно-ориентированного программирования (рис. 1.1).
Рис. 1.1
Следующий пример (рис. 1.2) содержит один простой класс
и два объекта этого класса. Несмотря на свою простоту, он демонстрирует синтаксис и основные черты классов С++.
5
Упражнение 1.1. Простой класс
Рис. 1.2
1.2. Определение класса. Управление доступом
Определение класса начинается с ключевого слова class, за
которым следует имя класса (в данном случае – smallobj). Подобно
структуре, тело класса заключено в фигурные скобки, после которых следует точка с запятой (;).
Тело класса содержит два ключевых слова – private и public,
которые описывают права доступа для переменных и методов класса и называются модификаторами доступа. Любая программа, использующая объект определенного класса, может иметь непосредственный доступ к членам объекта из раздела public. Доступ же
к членам объекта из раздела private программа может получить
только через открытые функции-члены из раздела public. Например, единственный способ изменить переменную somedata класса
smallobject – это воспользоваться функцией setdata(), которая является открытой функцией-членом класса. Таким образом, открытые функции-члены действуют в качестве посредников между программой и закрытыми членами объекта – они представляют интерфейс между объектом и программой. Такая изоляция данных от
прямого доступа со стороны программы называется сокрытием
данных (рис. 1.4).
Класс smallobj, определенный в программе, содержит одно
поле данных и два метода. Методы обеспечивают доступ к полю
данных класса. Первый метод присваивает полю значение, а второй
метод выводит это значение на экран.
Класс smallobj определяется в начале программы, а позже,
в функции main(), определяются два объекта этого класса – s1 и s2.
Каждый из этих объектов имеет свое значение и способен выводить
его на экран.
Результатом работы программы будет вывод на экран значений полей объектов s1 и s2 (рис. 1.3).
Рис. 1.4
Рис. 1.3
Для модификаторов доступа справедливы следующие утверждения:
• модификатор доступа относится ко всем перечисленным после него членам до следующего модификатора доступа;
6
7
• один и тот же модификатор доступа может указываться несколько раз;
• после модификатора доступа ставится двоеточие;
• если модификатор доступа не указан, то по умолчанию
предполагается private.
Замечание. Не следует путать сокрытие данных с техническими средствами, предназначенными, например, для защиты баз
данных. В последнем случае для обеспечения сохранности данных
можно, в частности, попросить пользователя ввести пароль, перед
тем как разрешить ему доступ к базе. Пароль обеспечивает защиту
базы данных от копирования и чтения, а также от несанкционированного или злоумышленного изменения ее содержимого.
Сокрытие данных означает их ограждение от тех частей программы, которые не нуждаются в использовании этих данных.
В более узком смысле это означает сокрытие данных одного класса
от другого класса. Сокрытие данных позволяет уберечь опытных
программистов от своих же собственных ошибок: программисты
могут сами создавать средства доступа к закрытым данным, что
значительно снижает вероятность случайного или некорректного
доступа.
1.3. Данные класса
Класс smallobj содержит всего одно поле данных somedata,
имеющее тип int. Данные, содержащиеся внутри класса, называются данными-членами класса, или полями класса. Число полей класса, как и у структуры, теоретически может быть любым. Поскольку
перед описанием поля somedata стоит ключевое слово private, это
поле доступно только внутри класса.
Рис. 1.5
Замечание. Как правило, скрывая данные класса, его методы
оставляют доступными. Это объясняется тем, что данные скрывают
во избежание нежелательного внешнего воздействия на них,
а функции, работающие с этими данными, должны обеспечивать
взаимодействие между ними и внешней по отношению к классу частью программы. Тем не менее не существует четкого правила, регламентирующего, какие данные следует определять как private,
а какие функции – как public. Вполне возможна ситуация, когда
необходимо скрыть функции и обеспечить свободный доступ
к данным класса.
1.5. Методы класса внутри определения класса
Методы класса – это функции, входящие в состав класса.
Класс smallobj содержит два метода: setdata() и showdata(). Тела
обоих методов состоят из одного оператора. Поскольку методы setdata() и showdata() описаны с ключевым словом public, они доступны за пределами класса smallobj. На рис. 1.5 показан синтаксис
определения класса.
Методы класса smallobj выполняют действия, типичные для
методов классов вообще: они считывают и присваивают значения
полям класса. Метод setdata() принимает аргумент и присваивает
полю somedata значение, равное значению аргумента. Метод
showdata() отображает на экране значение поля somedata.
Функции setdata() и showdata() определены внутри класса, то
есть код функции содержится непосредственно в определении
класса (здесь определение функции не означает, что код функции
помещается в память, это происходит лишь при создании объекта).
Методы класса, определенные подобным образом, по умолчанию
являются встраиваемыми (inline-функциями).
На практике можно только объявлять функции внутри класса,
а определение функции производить в другом месте программы.
8
9
1.4. Методы класса
Функция, определенная вне класса, по умолчанию уже не является
встраиваемой.
1.6. Определение объектов
После определения класса можно определить его объекты, что
и делает первый оператор функции main(). Он определяет два объекта класса smallobj – s1 и s2:
smallobj s1, s2;
Определение объекта похоже на определение переменной: оно
означает выделение памяти, необходимой для хранения объекта.
После этого все операции программа производит с объектами.
Замечание. При определении класса выделения памяти не
происходит. Определение класса лишь задает вид будущего объекта, подобно тому как определение структуры не выделяет память
под структурные переменные, а лишь описывает их организацию.
1.7. Вызов методов класса
Для получения доступа к методу класса необходимо использовать операцию точки (.) (операцию доступа к члену класса), связывающую метод с именем объекта. Синтаксически это напоминает
доступ к полям структуры, но скобки позади имени метода говорят
о том, что совершается вызов функции, а не используется значение
переменной.
В программе из упражнения 1.1 вызов метода setdata() осуществляет следующая пара операторов:
s1.setdata(1066);
s2.setdata(1776);
Первый оператор вызывает метод setdata() для объекта s1
и присваивает его полю somedata значение, равное 1066. Второй
оператор вызывает тот же метод setdata(), но для объекта s2, и присваивает его полю somedata значение, равное 1776. В результате
выполнения этих двух операторов создаются два объекта s1 и s2
класса smallobj с различными значениями поля somedata (рис. 1.6).
Аналогично два вызова функции showdata() отобразят на
экране значения полей соответствующих объектов.
10
Рис. 1.6
Замечания
1. Поскольку setdata() является методом класса smallobj, его
вызов должен быть связан с объектом этого класса. Поэтому,
например, оператор
setdata(1066);
сам по себе не имеет смысла, так как метод всегда производит
действие с конкретным объектом, а не с классом в целом. Попытка доступа к классу по смыслу схожа с попыткой сесть за руль чертежа автомобиля. Помимо бессмысленности такого действия, компилятор расценил бы его как ошибку. Таким образом, доступ к методам класса возможен только через конкретный объект этого
класса.
2. В некоторых объектно-ориентированных языках программирования вызовы методов объектов называют сообщениями. Так,
например, вызов
s1.showdata();
рассматривается как посылка объекту s1 сообщения с указанием
вывести на экран свои данные. Представление вызова метода в виде
сообщения подчеркивает независимость объектов как самостоятельных единиц, взаимодействие с которыми осуществляется путем
обращения к их методам.
11
1.8. Объекты программы и объекты реального мира
Объекты, использующиеся в программе, зачастую представляют собой реальные физические объекты. В таких ситуациях проявляется взаимодействие между программой и реальным миром.
В качестве примера создадим объект класса, основой которого
послужит структура, описывающая модели телефонов (рис. 1.7).
и два метода. Метод класса set_phone() присваивает значения всем
трем полям класса одновременно. Метод show_phone() выводит на
экран содержимое полей.
В программе создается два объекта класса phone с именами
phone1 и phone2. Метод set_phone() присваивает их полям соответствующие значения, а метод show_phone() выводит эти значения на экран.
Результат работы программы представлен на рис. 1.8.
Упражнение 1.2. Телефоны в качестве объектов
Рис. 1.8
1.9. Класс как тип данных
Следующий пример (рис. 1.9) демонстрирует применение
объектов в качестве переменных типа, определенных пользователем. Объекты представляют собой расстояния, выраженные в метрической системе мер.
Упражнение 1.3. Длины в метрической системе мер в качестве
объектов
Рис. 1.7
В данной программе используется класс phone, который содержит три поля данных (model_name, model_number и model_cost)
Рис. 1.9 (начало)
12
13
1.10. Конструкторы
При создании объекта необходимо корректно его проинициализировать. В предыдущем примере было реализовано два способа
инициализации полей объектов класса. Как правило, удобнее инициализировать поля объекта автоматически, в момент его создания,
а не явно вызывать в программе соответствующий метод. Такой
способ инициализации реализуется с помощью особого метода
класса, называемого конструктором. Конструктор – это метод
класса, выполняющийся автоматически в момент создания объекта.
Рис. 1.9 (окончание)
В этой программе класс Distance содержит два поля (meters
и centimeters) и три метода: set_dist(), предназначенный для задания полей объекта через передаваемые ему аргументы; get_dist(),
получающий эти же значения с клавиатуры, и show_dist(), отображающий на экране расстояние в метрах и сантиметрах. Таким образом, значения полей объекта класса Distance могут быть заданы
двумя способами.
В функции main() определяются две переменные типа Distance: dist1 и dist2. Значения полей переменной dist1 задаются
с помощью функции set_dist(), вызванной с аргументами 13 и 6.5,
а значения полей переменной dist2 вводятся пользователем.
Результат работы программы представлен на рис. 1.10.
1.10.1. Имя конструктора, виды конструкторов, списки
инициализации
Для конструктора справедливы следующие утверждения:
• имя конструктора совпадает с именем класса;
• конструктор не возвращает никакого значения;
• класс может иметь несколько конструкторов, отличающихся
списком параметров;
• конструкторы, как и функции, могут быть перегруженными.
В качестве примера рассмотрим класс, объектами которого
будут счетчики (рис. 1.11). Счетчик – это средство для хранения
количественной меры какой-либо изменяющейся величины. Счетчик может хранить, например, число обращений к файлу, число
нажатий пользователем клавиши Enter, количество клиентов банка
и т. п. Как правило, при наступлении соответствующего события
счетчик увеличивается на единицу (инкрементируется). Обращение
к счетчику обычно происходит для осведомления о текущем значении измеряемой им величины.
Упражнение 1.4. Использование конструктора на примере
счетчика в качестве объекта
Рис. 1.10
В данном примере в функции main() создаются два объекта c1
и c2 типа Counter. При создании каждого из них вызывается конструктор Counter(), присваивающий полю counter каждого объекта
нулевое значение. Таким образом, одновременно с созданием объектов происходит их инициализация. Затем значение счетчика c1
инкрементируется один раз, а значение счетчика c2 – два раза.
14
15
ны, по которым инициализация не проводится в теле конструктора,
достаточно сложны.
2. Инициализация полей с помощью списка инициализации
также происходит до начала исполнения тела конструктора, что
бывает важно в некоторых ситуациях. Так, например, список инициализации – это единственный способ задать начальные значения
констант и ссылок. В теле конструктора, как правило, производятся
более сложные действия, чем обычная инициализация.
Чтобы убедиться в том, что конструктор функционирует
именно так, как описано выше, изменим предыдущую программу
таким образом, чтобы конструктор выводил сообщение во время
выполнения (рис. 1.13).
Упражнение 1.5. Использование конструктора на примере
счетчика в качестве объекта
Рис. 1.11
Результат работы программы представлен на рис. 1.12.
Рис. 1.12
В данной программе конструктор Counter() инициализирует
только одно поле объекта. Если необходимо инициализировать сразу несколько полей, то их значения разделяются запятыми. В результате образуется список инициализации, например:
someClass(): field1(5), field2(23), field3(34)
{}
Замечания
1. Как видно из приведенных выше примеров, инициализация
полей объекта осуществляется не в теле конструктора, а между
прототипом метода и телом конструктора (в рассмотренных примерах тело конструктора – пустое), и предварена двоеточием. Причи16
Рис. 1.13
17
Результат работы программы представлен на рис. 1.14.
Рис. 1.14
Видно, что конструктор действительно выполняется дважды:
первый раз – при создании объекта c1, второй раз – при создании
объекта c2.
В данной программе список аргументов конструктора пуст.
Такой конструктор без аргументов называется стандартным конструктором, или конструктором по умолчанию.
В качестве примера использования конструктора с аргументами возьмем программу из упражнения 1.2 и изменим ее таким образом, чтобы вместо метода set_phone() использовался конструктор.
Чтобы инициализировать три поля класса, конструктору необходимо указать три имени и три соответствующих им значения в списке
инициализации (рис. 1.15).
Упражнение 1.6. Использование конструктора на примере телефона в качестве объекта
Данная программа схожа с программой из упражнения 1.2
с той лишь разницей, что функция set_phone() заменена конструктором. Эта замена позволила упростить функцию main(). Вместо
двух операторов, используемых для создания объекта и его инициализации, теперь используется один оператор, совмещающий оба
эти действия.
Для конструктора, как и для любой другой функции, можно
указать параметры по умолчанию. Например, конструктор
phone(char* name, int number, float cost = 13500.):
model_name(name), model_number(number), model_cost(cost)
{}
выполнит инициализацию поля model_cost создаваемого объекта
значением по умолчанию (в данном случае значением 13500.), если
объект создан следующим образом:
phone phone1(“HTC Desire”, 601)
18
Рис. 1.15
1.10.2. Перегрузка конструкторов
Поскольку конструктор является специальной функцией,
в С++ существует возможность перегрузки конструкторов, то есть
возможность использования конструкторов с одинаковыми именами и различными наборами аргументов. Возможности инициализации объектов в таком случае расширяются.
Рассмотрим перегрузку конструкторов (рис. 1.16) на примере
программы из упражнения 1.3.
Упражнение 1.7. Пример перегрузки конструкторов
Рис. 1.16 (начало)
19
поля объекта константными значениями (нулевыми), а конструктор с
аргументами – значениями, переданными ему в качестве аргументов.
Существует еще один способ инициализации объекта: использование значений полей уже существующего объекта. Для этого не
нужно самим создавать специальный конструктор. Такой конструктор предоставляется компилятором для каждого создаваемого класса и называется копирующим конструктором по умолчанию
(рис. 1.18). Копирующий конструктор имеет единственный аргумент, являющийся объектом того же класса, что и конструктор.
Упражнение 1.8. Копирующий конструктор по умолчанию
Рис. 1.16 (окончание)
Возможный
на рис. 1.17.
результат
работы
программы
представлен
Рис. 1.17
Рис. 1.18
1.10.3. Конструктор копирования по умолчанию
В предыдущем примере были реализованы два способа инициализации объекта. Конструктор без аргументов инициализировал
20
21
Результат работы программы представлен на рис. 1.19.
Упражнение 1.9. Использование деструктора при работе
с динамическими строками
Рис. 1.19
Объект dist1 был инициализирован значением 13 м 6,5 см при
помощи конструктора с двумя аргументами. Затем были созданы
два новых объекта dist2 и dist3, каждый из которых был инициализирован значением объекта dist1. В обоих случаях был вызван
копирующий конструктор по умолчанию, действие которого сводилось к копированию значений полей объекта dist1 в соответствующие поля объектов dist2 и dist3.
Замечание. Несмотря на то, что при инициализации объектов
dist2 и dist3 используются разные операторы, они выполняют одинаковые действия и равноправны в использовании.
1.11. Деструкторы
Деструктор – это специальный метод класса, обычно использующийся для освобождения памяти, выделенной конструктором
при создании объекта. Для деструкторов справедливы следующие
утверждения:
• деструктор имеет имя, совпадающее с именем конструктора
(а следовательно, и с именем класса) и предваряющееся знаком ~
(тильда);
• у класса может быть только один деструктор;
• деструкторы не имеют параметров;
• деструкторы нельзя перегружать;
• деструкторы вызываются автоматически при использовании
оператора delete по отношению к указателю класса или при выходе
программы из области действия объекта класса.
В упражнении 1.9 (рис. 1.20) приведен пример использования
деструктора при работе с динамическими строками.
Рис. 1.20
22
23
Результат работы программы представлен на рис. 1.21.
Рис. 1.21
В данной программе деструктор ~cstring() освобождает память, используемую для хранения динамической строки.
Замечания
1. Деструктор всегда вызывается перед тем, как освобождается память, выделенная под объект. Если объект был создан с помощью операции new, то при вызове операции delete вначале выполняется деструктор, а затем высвобождается память, занимаемая
этим объектом. То же самое происходит с автоматической переменной при выходе из функции, в которой она определена.
2. Если деструктор в определении класса не объявлен, то при
уничтожении объекта никаких действий не производится.
3. В особых случаях деструктор можно вызвать явно, например:
psz −> ~ cstring();
Но такие вызовы встречаются достаточно редко.
Таким образом, деструктором называется метод, вызываемый
при разрушении объекта данного класса.
Заключение
1. Если у класса есть конструктор, то он вызывается всегда,
когда создается объект класса.
2. Если у класса есть деструктор, то он вызывается всегда, когда объект класса уничтожается.
Задания для самостоятельной работы
Составьте объектно-ориентированную программу, имитирующую работу автоматического турникета при въезде на парковку.
Программа должна моделировать ситуацию, когда часть машин
оплачивает парковку, а часть проезжает бесплатно. В кассе ведется
24
учет числа проехавших за день машин и суммарной выручки от
оплаты парковки.
Для реализации программы создайте класс (например, с именем gate).
В состав полей данных класса включите три переменные:
• целочисленную переменную (например, с именем payCars)
типа unsigned int для учета числа машин, оплативших парковку;
• целочисленную переменную (например, с именем
nopayCars) типа unsigned int для учета числа машин, не оплативших парковку;
• целочисленную переменную (например, с именем totalCash) типа unsigned int для хранения суммарной выручки от оплаты парковки.
В состав методов класса включите следующие функции:
• конструктор класса, инициализирующий все поля нулевыми значениями;
• функцию (например, с именем payingCars()), инкрементирующую количество оплативших парковку машин и увеличивающую суммарную выручку на величину тарифа за парковку;
• функцию (например, с именем nopayingCars()), инкрементирующую количество не оплативших парковку машин и оставляющую суммарную выручку без изменения;
• функцию, выводящую на экран результаты работы турникета за день.
В функции main() продемонстрируйте работу класса. Для этого в ней:
• создайте объект класса gate;
• предложите пользователю:
- нажать одну клавишу для имитации заплатившего водителя;
- нажать другую клавишу для имитации недобросовестного
водителя;
- нажать клавишу ESC (код клавиши – 27).
Нажатие клавиши ESC должно приводить к выводу всей информации о работе парковки и завершению работы программы.
Для считывания введенных пользователем данных используйте функцию _getche().
Для передачи тарифа за парковку в метод payingCars() используйте глобальную переменную.
25
Глава 2. КЛАССЫ И ОБЪЕКТЫ. ЧАСТЬ II
2.1. Определение методов класса вне класса
Определение методов класса внутри самого класса не является
обязательным. Как и в случае обычных функций, внутри определения класса может содержаться только прототип функции-метода
класса. Вариант определения метода класса вне класса был приведен
в упражнении 1.9 на примере метода setstring(). В упражнении 2.1
(рис. 2.1) для реализации такого способа организации объектноориентированной программы использована версия программы из
упражнения 1.7, позволяющая складывать между собой различные
длины.
Упражнение 2.1. Пример определения метода класса вне класса
Рис. 2.1 (окончание)
Основной блок программы начинается с присвоения начальных значений полям объекта dist2 класса Distance, после чего
производится его сложение с объектом dist1, инициализируемым
пользователем. Объекты dist1 и dist3 создаются при помощи конструктора по умолчанию (конструктора без аргументов), а объект
dist2 – при помощи конструктора, принимающего два аргумента,
значения которых инициализируют поля объекта dist2. Значения
для инициализации объекта dist1 вводятся пользователем при помощи метода get_dist().
Возможный результат работы программы представлен на рис. 2.2.
Рис. 2.1 (начало)
Рис. 2.2
В данной программе внутри определения класса содержится
лишь прототип функции add_dist(). Такая форма записи означает,
26
27
что данная функция является методом класса, однако ее определение следует искать не внутри определения класса, а где-то в другом
месте листинга программы (в данной программе – после функции
main()).
Заголовок функции add_dist() содержит символ :: – знак операции глобального разрешения. Такая форма записи устанавливает
взаимосвязь функции и класса, к которому она относится. В данном
случае запись Distance::add_dist() означает, что функция
add_dist() является методом класса Distance (рис. 2.3).
Рис. 2.3
Замечание. Если создаваемый класс достаточно велик, то для
оптимизации общей структуры программы его можно размещать в
отдельном заголовочном файле.
2.2. Объекты в качестве аргументов функций
Рассмотрим на примере предыдущей программы механизм
передачи объектов в функции. Сложение объектов dist1 и dist2
и присвоение результатов объекту dist3 осуществляются с помощью вызова функции add_dist(). Величины, которые мы хотим
сложить, передаются методу add_dist() в качестве аргументов.
Синтаксис передачи объектов в функцию такой же, как и для переменных стандартных типов: на месте аргумента указывается имя
объекта.
Смысл вызова
dist3.add_dist(dist1, dist2);
можно сформулировать так: выполнить метод add_dist() для объекта dist3. Когда внутри функции происходит обращение к полям
centimeters и meters, это означает, что на самом деле обращение
происходит к полям dist3.centimeters и dist3.meters.
28
Таким образом, методу класса всегда предоставлен доступ
к полям объекта, для которого он был вызван: объект связан с методом операцией точки (.). Однако на самом деле методу класса доступны и другие объекты (dist1 и dist2), поскольку они выступают
в качестве его аргументов. Объект dist3 можно рассматривать как
псевдоаргумент функции add_dist(): формально он не является аргументом, но функция имеет доступ к его полям.
Замечания
1. Поскольку функция add_dist() является методом класса
Distance, она получает доступ к любым полям любого объекта
класса Distance, используя операцию точки (.), например:
dist1.centimeters, dist2.meters.
2. Функция add_dist() не возвращает значения. Типом возвращаемого значения является void. Результат автоматически присваивается объекту dist3.
Выводы
1. Каждый вызов метода класса обязательно связан с конкретным объектом этого класса.
2. Метод может напрямую обращаться по имени (в данном
примере – centimeters и meters) к любым открытым и закрытым
полям данного объекта.
3. Метод объекта имеет непрямой – через операцию точки
(например, dist1.centimeters и dist2.meters) – доступ к полям других объектов своего класса, которые выступают в качестве аргументов метода.
2.3. Объекты, возвращаемые функцией
Мы рассмотрели способ передачи объектов в функцию в качестве аргументов. Рассмотрим теперь способ, которым функция может возвращать объект в вызывающую программу (рис. 2.4).
Упражнение 2.2. Пример возвращения функцией значения типа
Distance
В предыдущей программе в качестве аргументов в функцию
add_dist() были переданы два объекта, а результат был сохранен
в объекте dist3, методом которого и являлась вызванная функция
add_dist(). В данной программе в качестве аргумента в функцию
29
add_dist() передается лишь один аргумент – объект dist2. Далее
объект dist2 складывается с объектом dist1, к которому относится
вызываемый метод add_dist(), а результат возвращается в функцию
main() и присваивается объекту dist3.
форма записи более естественна, поскольку использует операцию
присваивания самым обычным образом.
Замечание. Покажем, как с помощью перегрузки операций
можно добиться еще более простой формы записи этих же действий, а именно:
dist3 = dist1 + dist2;
Функция add_dist() в данной программе также отличается от
аналогичной функции в предыдущей программе. В данном случае
в функции add_dist() создается временный объект класса Distance,
хранящий значение вычисленной суммы до тех пор, пока оно не
будет возвращено вызывающей программе. Сумма вычисляется путем сложения двух объектов класса Distance: объекта dist1, по отношению к которому функция add_dist() является методом, и объекта dist2, переданного в функцию в качестве аргумента. Обращение к его полям из функции выглядит как d2.centimeters
и d2.meters. Результат сложения хранится в объекте temp, обращение к полям которого выглядит как temp.centimeters
и temp.meters. Значение объекта temp возвращается в вызывающую программу с помощью оператора return. Вызывающая программа main() присваивает возвращенное функцией значение объекту dist3.
2.4. Перегрузка методов класса
Поскольку методы класса – это специальные функции, то их
можно перегружать, так же как и обычные функции. Перегрузка
методов означает, что в текущей области действия заданы несколько функций с одними и теми же именами, но с различными типами
или числом аргументов. При работе такой программы компилятор
сам выбирает нужный метод, учитывая тип и число аргументов, переданных при его вызове (рис. 2.5).
Рис. 2.4
Действия этого оператора присваивания аналогичны действиям оператора присваивания из предыдущей программы, однако его
30
31
Упражнение 2.3. Пример перегрузки методов класса
Рис. 2.5
Результат работы программы представлен на рис. 2.6.
Рис. 2.6
2.5. Структуры и классы
Все рассмотренные выше примеры подтверждали негласный
принцип: структуры предназначены для объединения данных,
а классы – для объединения данных и функций. На самом деле,
в большинстве ситуаций структуры можно использовать так же, как
и классы. Формальная разница между структурами и классами заключается лишь в том, что по умолчанию все члены класса являются скрытыми, а все члены структуры – открытыми.
Формат, используемый при определении классов, выглядит
примерно так:
32
class food
{
private:
int data;
public:
void func();
};
Поскольку ключевое слово private для членов класса подразумевается по умолчанию, указывать его явно не обязательно.
Можно определять класс более компактным способом:
class food
{
int data;
public:
void func();
};
В этом случае поле data сохранит свою закрытость.
Если необходимо выполнять с помощью структуры те же действия, что и при использовании класса, можно отменить действие
принятого по умолчанию ключевого слова public словом private
и расположить открытые поля структуры до слова private, а закрытые – после:
struct food
{
void func();
private:
int data;
};
Однако обычно программисты не используют структуры таким образом, а придерживаются правила, о котором шла речь выше: структуры предназначены для объединения данных, а классы –
для объединения данных и функций.
2.6. Классы, объекты, данные и память
Объекты являются самодостаточными программными единицами, созданными по образцу, представленному определением
класса. Здесь возможна аналогия с машинами, сходящими с кон33
вейера, каждая из которых построена согласно стандарту своей модели. Машины в этом смысле схожи с объектами, а модели –
с классами.
На самом деле все устроено несколько сложнее. С одной стороны, каждый объект имеет собственные независимые поля данных. С другой стороны, все объекты одного класса используют
одни и те же методы. Методы класса создаются и помещаются
в память компьютера всего один раз – при создании класса. Нет никакого смысла держать в памяти копии методов для каждого объекта данного класса, так как у всех объектов методы одинаковые.
А поскольку наборы значений полей у каждого объекта свои, поля
объектов не должны быть общими. Это значит, что при создании
объектов каждый набор данных занимает определенную совокупность свободных мест в памяти (рис. 2.7).
Рис. 2.7
В частности, в предыдущих программах были созданы три
объекта класса Distance – dist1, dist2 и dist3, что означало наличие
в памяти компьютера трех полей с именем meters и трех полей
с именем centimeters. При этом функции get_dist(), show_dist()
и add_dist() хранились в памяти в единственном экземпляре и совместно использовались тремя объектами класса.
34
Между объектами не возникает конкуренции за общий ресурс,
поскольку в каждый момент времени выполняется не более одной
функции.
2.7. Статические данные класса
2.7.1. Статические поля класса: определение и пример
использования
Если поле данных класса описано с ключевым словом static,
то его значение будет одинаковым для всех объектов данного класса. Статические данные класса полезны в тех случаях, когда требуется, чтобы все объекты класса включали в себя какое-либо одинаковое значение. Статическое поле по своим характеристикам схоже
со статической переменной: оно видимо только внутри класса, но
время его жизни совпадает со временем жизни программы. Таким
образом, статическое поле существует даже в том случае, когда не
существует ни одного объекта класса.
Отличия статической переменной функции от статической переменной класса следующие:
• статическая переменная функции предназначена для сохранения значения переменной между вызовами функции;
• статическая переменная класса используется для хранения
данных, совместно используемых объектами класса.
Пример. Предположим, что существует необходимость, чтобы
объект класса располагал информацией о том, сколько еще объектов этого же класса существует на данный момент времени в памяти. Например, если моделируется автомобильная гонка и объекты –
это гоночные машины, то каждому гонщику нужно знать, сколько
всего автомобилей участвует в гонке. В этом случае можно включить в класс статическую переменную count (рис. 2.8), доступную
всем объектам, которые будут видеть одно и то же ее значение.
Упражнение 2.4. Пример использования статического поля класса
В данной программе класс too содержит единственное поле
count, имеющее тип static int. Конструктор класса инкрементирует
значение поля count при создании объекта. В функции main() со35
здаются три объекта класса too. Поскольку конструктор в этом случае вызывается трижды, инкрементирование поля count также происходит трижды. Метод getcount() возвращает значение count. Он
вызывается для каждого объекта и во всех случаях возвращает одну
и ту же величину.
Рис. 2.10
Статические поля класса применяются гораздо реже, чем автоматические, однако существует немало ситуаций, когда их использование удобно. Сравнение статических и автоматических полей класса представлено на рис. 2.11.
Рис. 2.8
Результат работы программы представлен на рис. 2.9.
Рис. 2.11
2.7.2. Раздельное объявление и определение статических
полей класса
Если использовать в данной программе не статическое, а автоматическое поле count, то конструктор будет увеличивать на
единицу значение этого поля для каждого объекта. Результат работы программы в этом случае показан на рис. 2.10.
Статические поля класса определяются не так, как обычные.
Обычные поля объявляются (компилятору сообщается имя и тип
поля) и определяются (компилятор выделяет память для хранения
поля) при помощи одного оператора. Для статических полей эти
действия выполняются двумя операторами: объявление поля находится внутри определения класса, а определение поля, как правило,
располагается вне класса (int too :: count = 0; в программе из
упражнения 2.4) и зачастую представляет собой определение глобальной переменной.
36
37
Рис. 2.9
Определение статического поля вне класса обеспечивает однократное выделение памяти под это поле до того, как программа
будет запущена на выполнение и статическое поле станет доступным всему классу. Каждый объект класса уже не будет обладать
своим собственным экземпляром статического поля, как в случае
полей автоматического типа. В этом отношении просматривается
аналогия статического поля класса с глобальной переменной.
Таким образом, в С++ могут существовать несколько типов
объектов:
• автоматический объект (создается каждый раз, когда его
описание встречается при выполнении программы, и уничтожается
каждый раз при выходе из блока, в котором он появился);
• статический объект (создается один раз при запуске программы и уничтожается один раз при ее завершении);
• объект в свободной памяти (создается с помощью операции
new и уничтожается с помощью операции delete);
• объект-член (объект другого класса или элемент вектора).
го объекта должны суммироваться со значениями полей объекта,
принимаемого методом в качестве аргумента, а полученный результат – возвращаться вызывающей программе. В следующей программе (рис. 2.12) оба эти метода сделаны константными.
Упражнение 2.5. Пример использования константных методов
и константных аргументов
2.7.3. Константные методы и константные аргументы
методов
Константные методы отличаются тем, что не изменяют значений полей своего класса.
Основные особенности работы с константными методами:
• чтобы сделать функцию константной, необходимо указать
ключевое слово const после прототипа функции, но до начала ее
тела;
• если объявление и определение функции разделены, то модификатор const необходимо указывать дважды – и при объявлении
функции, и при ее определении;
• методы, которые лишь считывают данные из поля класса,
имеет смысл делать константными, поскольку у них нет необходимости изменять значения полей объектов класса.
Например, в программе из упражнения 2.2 метод show_dist()
класса Distance можно сделать константным, потому что он не изменяет (и не должен изменять!) поля того объекта, для которого
вызывается. Он предназначен лишь для вывода текущих значений
на экран. Метод add_dist() также не должен изменять данных, хранящихся в объекте, из которого он вызывается. Значения полей это38
Рис. 2.12
39
Как известно, для передачи аргумента в функцию по ссылке
с одновременной его защитой от изменения функцией необходимо
сделать этот аргумент константным при объявлении и определении
функции. Методы классов в этом отношении не являются исключениями. В качестве примера константного аргумента в программе из
упражнения 2.5 использован параметр d2 функции add_dist(), указанный с модификатором const в ее объявлении и определении. Таким образом, происходит передача аргумента в функцию add_dist()
по ссылке, причем функция не сможет изменить значение этого аргумента, в качестве которого при вызове выступает переменная
dist2 функции main().
2.7.4. Константные объекты
Модификатор const может применяться для защиты от изменения значений не только переменных стандартных типов (например, int), но и объектов класса. Если объект класса объявлен с модификатором const, он становится недоступным для изменения.
Это означает, что для такого объекта можно вызывать только константные методы, поскольку лишь они гарантируют, что объект не
будет изменен (рис. 2.13).
Замечание. При создании класса считается хорошим стилем
объявлять константными функции, не изменяющие полей объектов
класса. Это позволяет тому, кто использует данный класс, создавать константные объекты данного класса, из которых можно вызывать только константные функции.
2.8. Массивы объектов
Любой объект, встроенный или созданный пользователем,
может быть сохранен в массиве. Для этого сначала нужно объявить
массив, а затем указать компилятору, для объектов какого типа
массив создан и сколько объектов может содержать.
Компилятор вычислит необходимое для массива количество
памяти, основываясь на размере объекта, заданном при объявлении
класса.
На рис. 2.14 представлена версия программы из упражнения 1.2,
измененной для случая массива объектов.
Упражнение 2.7. Пример использования массивов объектов
Упражнение 2.6. Пример использования константного объекта
Рис. 2.14 (начало)
Рис. 2.13
40
41
Рис. 2.14 (окончание)
В данной программе в функции main() определяется массив
объектов класса phone, число элементов которого задается значением переменной imax. Затем с помощью оператора switch и метода set_phone() производится инициализация элементов массива,
а с помощью константного метода show_phone() – вывод всех элементов массива на экран.
Доступ к методам объектов элементов массива похож на
доступ к членам структуры, являющейся элементом массива: за именем массива следуют квадратные скобки, в которые заключен индекс
элемента, а затем – операция точки и имя вызываемого метода.
Задания для самостоятельной работы
1. Составьте объектно-ориентированную программу, в которой реализуйте класс fraction, описывающий обыкновенные дроби
вида P/Q (P — целое, Q — натуральное) и операции с ними.
В качестве полей данных класса задайте две переменные соответствующих типов.
В состав методов класса включите следующие функции:
• конструктор без аргументов;
• конструктор с двумя аргументами;
• функцию, реализующую вывод на экран результата операции с дробями.
Добавьте в состав методов созданного класса методы, осуществляющие с дробями следующие операции:
42
• сложение;
• вычитание;
• умножение;
• деление;
• сокращение дроби.
Дополнительно реализуйте следующую организацию программы:
• разместите объявление методов класса fraction в описании
класса, а их определения – вне класса;
• разместите объявление класса fraction в отдельном заголовочном файле.
В функции main() с помощью директивы #include подключите
созданный класс и продемонстрируйте использование объектов
класса fraction.
2. Создайте класс line, с помощью которого можно будет рисовать на экране линии произвольной длины, толщины, типа и цвета.
В качестве полей данных класса line задайте:
• координаты двух концов линии;
• цвет линии;
• тип линии;
• толщину линии.
В состав методов класса включите:
• конструктор с пятью параметрами;
• функцию по прорисовке линии.
Реализуйте следующую организацию программы:
• разместите объявление методов класса line в описании класса, а их определения – вне класса;
• разместите объявление класса line в отдельном заголовочном файле.
В функции main() с помощью директивы #include подключите
созданный класс и продемонстрируйте использование массива объектов класса line, реализовав вывод на экран геометрической фигуры «пирамида».
43
Возможный вариант работы программы представлен на рис. 2.15.
Глава 3. УКАЗАТЕЛИ НА ОБЪЕКТЫ. СОЗДАНИЕ
СТРУКТУР ДЛЯ ХРАНЕНИЯ ДАННЫХ
С ИСПОЛЬЗОВАНИЕМ КЛАССОВ
Рис. 2.15
Указатели могут указывать на объекты так же, как и на простые
типы данных и массивы. В предыдущих упражнениях были рассмотрены многочисленные примеры объявления таких объектов, как
Distance dist;
где определен объект dist класса Distance.
Однако в некоторых случаях на момент написания программы
неизвестно количество объектов, которые необходимо создать. Тогда можно использовать операцию new для создания объектов во
время работы программы. Операция new возвращает адрес вновь
созданного неименованного объекта, который можно присвоить
указателю (рис. 3.1).
Упражнение 3.1. Доступ к членам класса через указатель
Для доступа к членам класса в случае использования
указателей применяется синтаксис, отличный от используемого для
обычных переменных. В данном случае для обращения, например,
к методу getdist() нельзя использовать операцию точки ( . )):
disptr.getdist(); // так нельзя, так как disptr – не просто
переменная;
disptr ->getdist(); // так можно.
Рис. 3.1 (начало)
44
45
3.1. Массив указателей на объекты. Сортировка указателей
Массив указателей на объекты – часто встречающаяся в программировании конструкция (рис. 3.3). Этот механизм упрощает
доступ к группе объектов – он более гибок, чем простое создание
массива объектов.
Упражнение 3.2. Пример массива указателей на объекты
Рис. 3.1 (окончание)
Возможный результат работы программы показан на рис. 3.2.
Рис. 3.2
При использовании указателей доступ к членам класса
осуществляется с помощью операции ->, состоящей из дефиса
и знака «больше чем». Операция -> работает с указателями на
объекты так же, как операция точки – с обычными переменными.
Оператор disptr ->getdist() означает: вызвать метод getdist() для
объекта
класса
Distance,
расположенного
по
адресу,
содержащемуся в указателе disptr.
Замечание. Для использования операции точки при работе
с указателями необходимо применить операцию разыменовывания
указателя:
(*disptr).getdist(); // так можно, так как разыменованный
указатель – это переменная.
Рис. 3.3
46
47
Возможный результат работы программы представлен на рис. 3.4.
Рис. 3.4
В рассмотренном примере класс person имеет одно поле
name, которое содержит строку с фамилией клиента. Методы класса setName() и printName() позволяют, соответственно, ввести фамилию клиента и затем вывести ее на экран. Для каждого клиента
с помощью операции new создается объект класса person, указатель на который хранится в массиве persPtr[ ]. Для доступа к методам класса person с использованием указателей применяется
операция ->. Таким образом, оператор persPtr[n] -> setName(); вызывает метод setName() для объекта класса person, на который указывает указатель, являющийся n-м элементом массива persPtr[ ].
В следующем упражнении (рис. 3.5) представлена программа,
осуществляющая сортировку объектов класса person через сортировку массива указателей на них.
Упражнение 3.3. Сортировка объектов через массив указателей
на них
Рис. 3.5 (окончание)
Возможный результат работы программы представлен на
рис. 3.6.
Рис. 3.5 (начало)
48
Рис. 3.6
49
В данной программе каждый указатель из массива persPtr[ ]
указывает на отдельный объект класса person (в данном случае – на
фамилию человека). При сортировке объектов (в программе реализована сортировка по алфавиту) в функции order() происходит работа не с самими объектами, а с указателями на них. В результате
исчезает необходимость перемещения объектов в памяти, которое
может занимать много времени, если объекты очень велики. Кроме
того, такой подход позволяет выполнять одновременно сложные
сортировки (например, одну по имени, а другую – по телефонным
номерам) без многократного сохранения объектов. Для облегчения
сортировки в класс person добавлен метод getName(), дающий доступ к именам из функции order(). Процесс сортировки с использованием массива указателей показан на рис. 3.7.
указателей на person имеет тип person**, то есть адрес указателя –
это указатель на указатель. Схема работы с указателем на массив
указателей показана на рис. 3.8.
Рис. 3.8
В функции order() реализовано лексикографическое сравнение двух строк, а именно расположение их в алфавитном порядке.
Для проведения такого сравнения поле данных name имеет в программе тип string (то есть все имена являются объектами библиотечного класса string для работы со строками). Это позволяет использовать для сравнения двух строк стандартную функцию этого
класса strcmp(s1, s2), которая принимает в качестве аргументов две
строки (s1 и s2) и возвращает одно из значений, представленных
в табл. 3.1.1.
Таблица 3.1.1
Значение
<0
=0
>0
Рис. 3.7
Замечание. Первый аргумент функции bsort() и оба аргумента
функции order() имеют тип person**. Эти аргументы используются
для передачи адреса массива persPtr в первом случае и адресов
элементов массива во втором случае. Если это массив типа person,
то его адрес будет иметь тип person*. Однако адрес массива
50
Описание
s1 идет перед s2
s1 и s2 одинаковы
s1 идет после s2
Доступ к строке осуществляется посредством оператора
(*pp1) -> getName();
Аргумент pp1 является указателем на указатель, поэтому для
доступа к переменной, находящейся по адресу, на который указывает указатель pp1, необходимо дважды осуществить операцию
разыменовывания: для разыменовывания одного указателя исполь51
зуется операция *, предшествующая указателю pp1, а для разыменовывания второго указателя – операция ->.
Замечание. Поскольку существуют указатели на указатели, то
могут существовать и указатели на указатели, которые указывают
на указатели и т. д. Но, к счастью, такие сложные конструкции
встречаются достаточно редко.
Упражнение 3.4. Стек как пример структуры для хранения данных
3.2. Стек
Стек – это один из краеугольных камней архитектуры микропроцессоров, используемых в самых современных компьютерах.
Функции передают аргументы и хранят возвращаемые адреса в стеке. Этот вид стека частично реализован в оборудовании, и доступ
к нему удобнее всего получать через язык ассемблера. Однако стек
можно полностью реализовать в программном обеспечении для
хранения данных в определенной программной ситуации, например
в программах по бухгалтерскому учету для реализации метода
LIFO (“Last Input – First Output” – «последний вошел – первый вышел»).
Замечание. Стек работает как пружинное устройство, удерживающее патроны в обойме пистолета: когда патрон помещается
в обойму, стек немного оседает; когда патрон извлекается из обоймы, стек поднимается. Последний положенный в обойму патрон
всегда извлекается первым.
Программа из следующего упражнения (рис. 3.9) служит
примером использования массивов в качестве полей класса для
программной реализации стека для хранения массива из 10 целых
чисел.
Класс Stack содержит три поля данных: константу MAX,
определяющую размер стека; целочисленный массив st[MAX]
(собственно стек) и целочисленную переменную top, в которой
хранится индекс последнего положенного в стек элемента (этот
элемент всегда располагается на вершине стека).
В программе сначала в стек помещаются два элемента, затем
они извлекаются из стека и выводятся на экран. После этого в стек
помещаются еще четыре элемента, затем они извлекаются и тоже
выводятся на экран.
52
Рис. 3.9
Результат работы программы представлен на рис. 3.10. Как
видно, элементы извлекаются из
стека в обратном порядке: последний помещенный в стек
элемент извлекается первым.
53
Рис. 3.10
Стек схематично показан на рис. 3.11.
Замечание. Поскольку на рисунках принято изображать увеличение адресов в памяти по направлению сверху вниз, то вершина
стека находится в нижней части рис. 3.11.
первый вышел»). Очередь похожа на простую очередь посетителей
магазина, банка и т. д.: первый в ней будет обслужен первым.
Недостаток очереди заключается в том, что количество элементов в ней ограничено размером массива. При его переполнении
необходимо заново выделить память, увеличив ее размер, и скопировать все элементы в новый массив.
3.4. Связный список
Рис. 3.11
При добавлении в стек нового элемента (путем вызова метода
push() с сохраняемым значением в качестве аргумента) индекс переменной top увеличивается – теперь она показывает на новую
вершину стека. При удалении элемента из стека путем вызова метода pop() индекс переменной top уменьшается (стирать старое
значение индекса из памяти при удалении элемента не обязательно,
оно просто становится несущественным).
Класс Stack демонстрирует важную возможность объектноориентированного программирования: он используется для реализации различных механизмов хранения данных. Конкретный механизм выбирается в соответствии с требованиями программы.
Использование существующих классов для хранения данных означает, что программисту не нужно тратить время на их дублирование.
До сих пор в качестве примеров структур для хранения данных рассматривались только массивы и массивы указателей на
данные. Недостаток обеих этих структур состоит в необходимости
объявлять фиксированное значение размера массива до запуска
программы.
Связный список (рис. 3.12) представляет собой более гибкую
систему для хранения данных, в которой массивы не используются
в принципе. Вместо них с помощью операции new выделяется память для каждого элемента данных, а затем они соединяются (связываются) между собой с помощью указателей. При такой организации хранения данных элементы списка не обязательно должны
храниться в смежных участках памяти (как в случае с массивами),
они могут быть разбросаны по всему пространству памяти произвольным образом.
Упражнение 3.5. Пример связного списка
В данном примере связный список является объектом класса
linklist. Элементы списка представляют собой структуры типа link.
Каждый элемент содержит целое число, представляющее собой
данные, и указатель на следующий элемент списка. Указатель на
сам список хранится в начале списка. Структура связного списка
показана на рис. 3.13.
3.3. Очередь
Очередь – это структура для хранения данных, напоминающая
стек. Отличие очереди состоит в том, что элемент, помещенный
в нее первым, извлекается первым. Таким образом, в очереди реализуется метод FIFO (“First Input – First Output” – «первый вошел –
54
55
Класс linklist имеет только одно поле – указатель на начало
списка (link* first). При создании списка конструктор инициализирует этот указатель значением NULL, которое равнозначно нулевому значению.
Замечания
1. Значение NULL служит признаком того, что указатель указывает на адрес, который точно не содержит полезной информации.
В программе из упражнения 3.5 элемент, указатель которого на
следующий элемент имеет значение NULL, является конечным
элементом списка.
Метод additem() класса linklist позволяет добавить новый
элемент в начало списка. Для этого с помощью оператора new для
нового элемента выделяется память, адрес которой сохраняется
в указателе newlink. Затем элемент списка заполняется значением,
вводимым пользователем с клавиатуры, после чего производится
замена адреса, содержащегося в указателе first, на адрес нового
элемента. При этом старый адрес указателя first превратится в адрес второго элемента списка.
2. Действия со структурой похожи на действия с классами:
к элементам структуры можно получить доступ, используя операцию ->.
Процесс добавления нового элемента в начало связного списка показан на рис. 3.14.
Рис. 3.12
Рис. 3.14
Рис. 3.13
Для получения содержимого списка необходимо следовать от
одного указателя к другому до тех пор, пока значение указателя не
56
57
станет равным NULL, что означает конец списка. В методе
display() это условие проверяет оператор while(current).
Поскольку в рассмотренной программе новый элемент добавляется в начало списка, при извлечении содержимого списка он будет получен первым. Результат работы программы представлен на
рис. 3.15.
Рис. 3.15
3. Связные списки – наиболее часто встречающаяся после
массивов структура для хранения данных. В отличие от массивов
в них устранена возможность потери памяти. Недостаток связных
списков заключается в том, что для поиска определенного элемента
списка необходимо пройти сквозь весь список от его начала до
нужного элемента. На это может уйти много времени, тогда как
к элементам массива можно получить мгновенный доступ, используя индекс элемента.
4. Структура link в программе из упражнения 3.5 содержит
указатель на такую же структуру. Этот подход справедлив и для
классов:
class someclass
{
someclass* ptr; // так можно
};
Однако, хотя класс может содержать в себе указатель на такой
же объект, сам этот объект он содержать в себе не может:
class someclass
{
someclass obj; // так нельзя
};
Это замечание справедливо как для классов, так и для структур.
от операций инвестора с ценными бумагами (ЦБ), рассчитанный
тремя разными методами учета:
• по методу LIFO;
• по методу FIFO;
• по методу средней цены.
Реализуйте следующую организацию программы:
• для хранения исходных данных создайте в программе
структуру, например с именем trans (от англ. transaction), полями
которой будут переменная типа int и переменная типа stock;
• для обработки сделок по методу LIFO создайте класс
Stack, представляющий собой стек для хранения данных типа
stock;
• для обработки сделок по методу FIFO создайте класс
Queue, представляющий собой очередь для хранения данных типа
stock;
• для обработки сделок по методу средней цены создайте
класс Average, объекты которого будут представлять собой совокупную позицию по ЦБ: количество ЦБ на позиции (переменная
типа int) и среднюю цену одной ЦБ (переменная типа float);
• в каждом классе создайте необходимые методы, которые
будут соответствующим образом обрабатывать сделки по покупке/продаже ЦБ;
• разместите все вновь созданные классы, а также созданный
ранее класс stock в отдельных заголовочных файлах;
• напишите дополнительные функции, не входящие в состав
методов какого-либо класса, которые будут получать в качестве аргументов данные по сделкам и возвращать результат, рассчитанный
по каждому из трех методов учета;
• в функции main():
- подключите созданные классы с помощью директивы
#include;
- создайте по одному объекту каждого класса;
- протестируйте работу программы на заданных исходных
данных в соответствии со своим вариантом задания.
Задания для самостоятельной работы
Составьте объектно-ориентированную программу, которая
будет выводить на экран финансовый результат (прибыль/убыток)
58
59
Возможный
на рис. 3.16.
результат
работы
программы
представлен
Глава 4. ПЕРЕГРУЗКА ОПЕРАЦИЙ
Рис. 3.16
Перегрузка операций – одна из самых захватывающих возможностей объектно-ориентированного программирования. Она
позволяет превратить сложный для понимания листинг объектноориентированной программы в интуитивно понятный код путем записи операций с пользовательскими данными аналогично операциям со встроенными типами данных (int, float и т. д.). Например,
в результате перегрузки операции сложения для объектов d1 и d2
определенного пользователем класса можно заменить строку
d3 = d1.addobjects(d2);
на более привычную:
d3 = d1 + d2;
Специальные короткие символы, которые используются в С++
(+, -, & и т. д.), называются простыми операторами. Эти операторы (а точнее, их действия) определены для встроенных типов данных, но не определены для пользовательских классов. С++ позволяет определять, что именно будут означать те или иные операторы, если их применять к пользовательским классам. Эта особенность и называется перегрузкой операций.
Замечание. Следует всегда помнить о том, что перегрузка
операций может приводить к трудно выявляемым ошибкам.
4.1. Перегрузка унарных операций
Унарные операции имеют только один операнд (переменную,
на которую действует операция).
Примеры унарных операций:
• операция инкрементирования ++;
• операция декрементирования --;
• унарный минус (например, - 25).
4.1.1. Перегрузка префиксных операций
В упражнении 1.4 был создан класс Counter для хранения
значения счетчика. Увеличение поля count объекта этого класса
осуществлялось путем вызова метода inc_count():
c1.inc_count();
60
61
В упражнении 4.1 (рис. 4.1) использование перегрузки операции инкрементирования позволяет применять более привычную
форму записи этой операции для объектов класса Counter.
Упражнение 4.1. Увеличение объекта класса Counter префиксной
операцией ++
Использование перегрузки операции ++ позволяет инкрементировать значения объектов c1 и c2 привычным способом.
Для перегрузки операции ++ для объектов класса Counter
в состав методов класса была включена функция
void operator ++ ().
Этот синтаксис говорит компилятору о том, что если операнд
принадлежит классу Counter, то при наличии в тексте программы
операции ++ необходимо вызвать функцию с таким именем.
Замечание. Как отмечалось ранее, компилятор может различать перегружаемые функции по типу данных и количеству аргументов. Перегружаемые операции компилятор отличает по типу
данных их операндов. Если операнд (например, переменная intvar)
имеет базовый тип (например, int), то, встретив в тексте программы
оператор ++intvar, компилятор будет использовать свою внутреннюю процедуру для увеличения переменной intvar. Но если операнд является объектом пользовательского класса (например, класса Counter), то компилятор будет использовать написанную программистом функцию operator ++ ().
4.1.2. Аргументы операций
В программе из упражнения 4.1 функция operator ++() не
имеет аргументов. Возникает вопрос: что же она увеличивает? Она
увеличивает переменную count того объекта, к которому применяется (c1 и c2). Поскольку методы класса всегда имеют доступ
к объектам класса, для которых они были вызваны, то для этой
операции не требуются аргументы (рис. 4.3).
Рис. 4.1
Результат работы программы представлен на рис. 4.2.
Рис. 4.2
62
Рис. 4.3
63
4.1.3. Значения, возвращаемые операцией
Если в программе из упражнения 4.1 в функции main() попытаться использовать оператор
c1 = ++c2;
то компилятор выдаст сообщение об ошибке, потому что в программе для возвращаемого функцией operator++() значения определен тип void. В выражении же присваивания будет запрашиваться
переменная типа Counter. То есть компилятор запросит значение переменной c2 после того, как она будет обработана операцией ++.
Чтобы
иметь
возможность
использовать
функцию
operator++() из упражнения 4.1 в выражениях присваивания, необходимо правильно определить тип ее возвращаемого значения (рис. 4.4).
Упражнение 4.2. Операция ++, возвращающая значение
В этой программе функция operator++() создает новый
временный объект temp класса Counter для использования его
в качестве возвращаемого значения. Функция сначала увеличивает
переменную count в своем объекте, а потом создает объект temp
и присваивает ему значение count (то есть то же значение, что
и в собственном объекте). Затем функция возвращает объект temp.
В результате выражение типа ++c1 возвращает значение, которое
можно использовать в других выражениях, таких как c2 = ++c1.
Результат работы программы представлен на рис. 4.5.
Рис. 4.5
4.1.4. Временные безымянные объекты
В предыдущей программе (см. рис. 4.4) был создан временный
объект temp типа Counter, единственной целью которого было
хранение возвращаемого значения для операции ++. Присваивать
имя временному объекту не обязательно. Существует много способов возвращения временного объекта из функции или перегруженной операции. Один из них представлен в следующей программе
(рис. 4.6).
Упражнение 4.3. Операция ++ с использованием
недекларированной переменной
Рис. 4.4
Для возвращения временного объекта в данной программе
в методы класса добавлен конструктор с одним аргументом.
В функции Counter operator++() параметру этого конструктора
можно просто присвоить инкрементированное значение поля count.
Таким образом, в строке return Counter(count); создается объект
типа Counter, который не имеет имени, так как оно нигде не будет
использоваться. Этот объект инициализируется значением, полученным в виде инкрементированного значения поля count. Объект
может быть возвращен функцией и присвоен объекту этого же
класса. Результаты работы обеих программ совпадают.
64
65
Рис. 4.6
Еще один способ возврата временного безымянного объекта –
использование указателя this, который является скрытым параметром и имеется у каждого метода класса. Этот указатель содержит адрес текущего объекта и в случае разыменовывания возвращает объект, поля которого содержат текущие значения (рис. 4.7).
Упражнение 4.4. Операция ++ с использованием указателя this
В данной программе возврат разыменованного указателя this
означает возврат объекта типа Counter, поле count которого содержит текущее (то есть уже инкрементированное) значение. При
этом отпадает необходимость использовать конструктор с одним
параметром.
66
Рис. 4.7
4.1.5. Постфиксные операции
Чтобы получить возможность работать с двумя версиями операции декрементирования – префиксной ++c1 и постфиксной c1++,
следует определить два варианта перегрузки операции ++ (рис. 4.8).
Упражнение 4.5. Префиксная и постфиксная операции ++
для объектов класса Counter
В данной программе присутствует два типа объявления функции operator++ () – для префиксной и для постфиксной операции.
Различие между двумя функциями только одно: во втором случае
в списках аргументов функции стоит слово int. Здесь слово int не играет роли аргумента и не означает целого числа – оно просто служит
компилятору сигналом использовать постфиксную версию операции.
Разработчики С++ нашли полезным повторное использование суще67
ствующих операций и ключевых слов: в данном случае слово int
предназначено просто для обозначения постфиксной операции.
В последних двух строках на рис. 4.9 отображены результаты,
полученные после обработки выражения
c2 = c1++;
Здесь объект c1 увеличивается до трех, но объекту c2 значение
объекта c1 было присвоено до инкрементирования последнего, поэтому объект c2 имеет значение два.
4.2. Перегрузка бинарных операций
Бинарные операции могут быть перегружены так же, как
и унарные.
4.2.1. Арифметические операции сложения
В программе из упражнения 2.1 два объекта класса Distance
складывались между собой с помощью метода add_dist():
dist3.add_dist(dist1, dist2);
Применив перегрузку операции + (рис. 4.10), можно воспользоваться более привычной формой записи для сложения двух объектов:
dist3 = dist1 + dist2;
Упражнение 4.6. Перегрузка операции + для объектов класса Distance
Рис. 4.8
Результат работы программы представлен на рис. 4.9.
Рис. 4.9
68
Рис. 4.10 (начало)
69
Рис. 4.10 (окончание)
Результат работы программы представлен на рис. 4.11.
Рис. 4.11
В данной программе вместо метода add_dist() используется
метод operator+(). Изменилось и определение метода:
• метод возвращает значение типа Distance (то есть операция
сложения в результате дает значение того же типа, что и типы операндов);
70
• перед аргументом метода появилось ключевое слово const,
означающее, что при выполнении данного метода аргумент изменяться не будет;
• ключевое слово const появилось также и после объявления
метода – это означает, что объект, выполняющий метод, также не
будет изменен.
Таким образом, при выполнении операции сложения двух
объектов типа Distance сами объекты не изменяются.
В выражении
dist3 = dist1 + dist2;
важно понимать, к каким объектам будут относиться аргументы
и возвращаемые значения. Когда компилятор встречает это выражение, он просматривает типы аргументов. Обнаружив только аргументы типа Distance, он выполняет операции выражения, используя метод operator+() класса Distance. Но возникает вопрос:
какой объект используется в качестве аргумента этой операции –
dist1 или dist2? И не нужно ли складывать два аргумента, так как
мы складываем два объекта?
Существует следующее правило: объект, стоящий слева от
знака операции (в нашем случае – dist1), вызывает функцию оператора, а объект, стоящий справа, передается в функцию в качестве аргумента. Операция возвращает значение, которое можно использовать
для своих целей (в данном случае оно присваивается объекту dist3).
Таким образом, в функции operator+( ) к левому операнду мы
имеем прямой доступ через использование полей meters и centimeters, так как это объект, вызывающий функцию. К правому операнду мы имеем доступ как к аргументу функции, то есть как
d2.meters и d2.centimeters.
Вывод
Перегруженной операции всегда требуется аргументов на
единицу меньше, чем операндов, так как один из операндов является объектом, вызывающим функцию, поэтому для перегрузки
унарных операций аргументы не нужны.
4.2.2. Операции сравнения
В программе, показанной на рис. 4.12, реализована перегрузка
арифметической операции < («меньше чем») для класса Distance,
что дает возможность сравнивать объекты этого класса.
71
Упражнение 4.7. Перегрузка операции < для объектов класса Distance
Возможные результаты работы программы представлены
на рис. 4.13.
Рис. 4.13
Подход, используемый для функции operator < (), похож на
перегрузку операции + в предыдущей программе, за исключением
того, что здесь функция operator < () имеет тип возвращаемого
значения bool. В зависимости от результата сравнения двух интервалов возвращаемым значением может быть false или true. При
сравнении оба интервала конвертируются в значения метров с плавающей точкой и затем сравниваются с помощью обычной операции < (для этого в программе использована тернарная условная
операция).
4.2.3. Операции арифметического присваивания
Операция арифметического присваивания += выполняет сложение и присваивание за один шаг. При использовании этой операции для сложения объектов класса Distance результат будет записываться в переменную, обозначающую первый интервал
(рис. 4.14).
Упражнение 4.8. Перегрузка операции += для объектов
класса Distance
Рис. 4.12
Программа сравнивает интервал, заданный пользователем,
с определенным в программе интервалом 13 м 6,5 см. Затем, в зависимости от полученного результата, выводится одно из двух предложений.
Рис. 4.14 (начало)
72
73
Различия между функциями operator+() из упражнения 4.6
и operator+=() из упражнения 4.8 заключаются в следующем:
•
в функции operator+=() объектом, принимающим значение суммы, является объект, вызывающий функцию (в данном случае – dist1), поэтому поля meters и centimeters являются заданными величинами, а не временными переменными, используемыми
только для возвращаемого объекта;
•
поскольку функция operator+=() изменяет вызывающий
ее объект dist1, ключевое слово const в описании функции отсутствует;
•
поскольку для операции += возвращаемое значение не
требуется (так как ее результат не присваивается никакой переменной), функция operator+=() не имеет возвращаемого значения, то
есть возвращает тип void.
4.3. Особенности использования перегрузки операций
Рис. 4.15
Перегружая какую-либо операцию, необходимо всегда помнить о том, что если вы перегрузили, например, операцию -, то человеку, не знакомому с вашим листингом, нужно будет многое изучить, чтобы понять, что в действительности означает выражение
a = b - c;
Поэтому использовать перегрузку операций нужно очень аккуратно и только там, где ее необходимость очевидна. В противном
случае лучше вызвать функцию, чем перегружать операцию.
Замечание. Не могут быть перегружены:
• операция доступа к членам структуры или класса (.);
• операция разрешения видимости (::);
• тернарная условная операция (?:);
• операция косвенного доступа к членам класса (->).
Кроме того:
• нельзя создавать новые операции (например, нельзя определить новую операцию возведения в степень (**), которая есть
в некоторых языках) и пытаться их перегрузить – перегружать
можно только существующие операции;
• нельзя изменять установленные приоритеты и ассоциативности операторов (например, нельзя оператор с одним операндом
перегрузить для использования с двумя операндами);
• нельзя перегружать операторы стандартных типов данных
(такие как int).
74
75
Рис. 4.14 (окончание)
В данной программе интервал, полученный от пользователя,
складывается со вторым интервалом, инициализированным в программе значением 13 м 6,5 см. Сумма интервалов записывается
в переменную dist1. Возможный результат работы программы
представлен на рис. 4.15.
4.4. Преобразование типов данных
Как отмечалось выше, операция = используется для присваивания значения одной переменной другой в выражениях типа
intvar1 = intvar2;
где intvar1 и intvar2 – целочисленные переменные. Эту операцию
можно также использовать для присваивания значения одного объекта, определенного пользователем, другому объекту того же типа,
как в строке
dist3 = dist1 + dist2;
Обычно если значение одного объекта присваивается другому
объекту того же типа, то значения переменных объекта просто копируются в новый объект. Компилятору не нужны специальные инструкции, чтобы использовать операцию = для определенных пользователем типов, таких как объекты класса Distance.
Таким образом, присвоение внутри типов (как основных, так
и определенных пользователем) выполняется компилятором без
усилий со стороны программиста, при этом по обе стороны знака =
будут стоять переменные одинаковых типов.
Если с разных сторон знака «=» стоят переменные разных типов (определенных пользователем), компилятор не выполняет такие преобразования автоматически. В этих случаях программисту
следует определить, что должен делать компилятор. К подобным
ситуациям относятся преобразования между основными и определенными пользователем типами и преобразования между различными определенными пользователем типами. Эти задачи также решаются с помощью операций преобразования, похожих на перегрузку операций.
2. Составьте объектно-ориентированную программу, позволяющую производить операции с данными в формате времени
(11:59:59). Для этого:
• создайте класс с именем time, содержащий три поля данных
типа int (например, hrs, mins и secs) для хранения часов, минут
и секунд;
• включите в состав класса два конструктора, один из которых должен инициализировать поля класса нулевыми значениями,
а другой – заданным набором значений;
• перегрузите операции + и - для объектов созданного класса.
В функции main() создайте два инициализированных объекта
time time1 и time time2 и два неинициализированных объекта time
time3 и time time4. Затем с помощью перегруженных операций +
и - вычислите следующие выражения:
time3 = time1 + time2;
time4 = time1 - time2;
Выведите значения всех четырех объектов на экран.
Задания для самостоятельной работы
1. Перегрузите все основные математические операции для
работы с обыкновенными дробями в классе fraction. Удалите ранее
созданные функции для осуществления математических операций
с дробями из состава методов класса. Протестируйте работу всех
перегруженных операций в функции main().
76
77
Глава 5. НАСЛЕДОВАНИЕ
Наследование – это наиболее значимая после классов возможность объектно-ориентированного программирования. В наследовании классов реализуются принципы их иерархической подчиненности, когда один класс может происходить (наследоваться) от
другого класса более высокого уровня.
Наследованием называется механизм, позволяющий производному классу наследовать структуру данных и поведение другого
класса, а также поведение, объявленное в интерфейсах и абстрактных классах.
Таким образом, наследование – это процесс создания новых
классов, называемых наследниками (производными классами), из
уже существующих (базовых) классов. При наследовании производный класс получает все возможности базового класса, но может
быть и усовершенствован за счет добавления собственных возможностей. Базовый класс при этом остается неизменным. Взаимосвязь
классов при наследовании показана на рис. 5.1.
Наследование – важная часть объектно-ориентированного
программирования. Оно позволяет использовать однажды написанный код несколько раз. Имея написанный и отлаженный базовый
класс, можно больше его не модифицировать, при этом механизм
наследования позволит приспособить его для работы в различных
ситуациях. Таким образом, используя уже написанный код, можно
экономить время и деньги, а также увеличивать надежность программ.
Значимым результатом повторного использования кода является упрощение распространения библиотек классов. При этом
программист может использовать созданные кем-то другим классы
без модификации кода, просто создавая производные классы, подходящие для конкретной ситуации.
5.1. Базовый и производный классы
Рис. 5.1
Чтобы показать, что класс B является наследником класса A,
в определении класса B после имени класса ставится двоеточие
и затем перечисляются классы, из которых он наследует:
class A
{
public :
A(); //конструктор класса A
~A(); //деструктор класса A
MethodA(); //метод класса A
};
class B : public A
{
public :
B( ); //конструктор класса B
...
};
Двоеточие указывает на то, что класс B основан на классе A.
Заголовок public указывает, что класс A является общедоступным
базовым классом.
Такой процесс называется общедоступным наследованием,
при котором:
78
79
• объект производного класса включает в себя объект базового класса;
• общедоступные элементы базового класса являются общедоступными элементами производного класса;
• закрытые разделы базового класса являются частью производного класса, но к ним можно обращаться только посредством
общедоступных и защищенных методов базового класса.
Пример:
B b; // создали объект b класса B
b.MethodA(); // вызов метода базового класса A для объекта
b производного класса B
Замечание. Из одного базового класса можно вывести сколько
угодно подклассов (производных классов). В свою очередь, производный класс может служить базовым классом для других классов.
В упражнении 4.3 в качестве объекта класса Counter использовался счетчик. Он мог быть инициализирован нулем или некоторым числом при помощи конструктора, увеличен методом
operator++( ) и прочитан с помощью метода get_count( ). При
необходимости уменьшения счетчика (например, в случае подсчета
посетителей магазина счетчик увеличивает свое значение при входе
посетителя и уменьшает при выходе) можно решить задачу двумя
способами:
1) добавить метод декрементирования прямо в исходный код
класса Counter;
2) использовать механизм наследования для создания производного класса на базе класса Counter.
Для решения задачи вторым способом могут быть следующие
причины:
• класс Counter прекрасно работает, а на его отладку было
затрачено много времени и сил (в данном случае это преувеличение, но подобная ситуация может иметь место для больших
и сложных классов);
• доступ к базовому классу отсутствует (например, если он
распространяется как часть библиотеки классов).
В следующем упражнении (рис. 5.2) представлен вариант решения задачи с использованием производного класса.
80
Упражнение 5.1. Наследование при работе с классом Counter
Рис. 5.2
Результат работы программы представлен на рис. 5.3.
Рис. 5.3
В данной программе класс CountDn, являясь наследником
класса Counter, наследует все его возможности, то есть конструкторы и методы.
На рис. 5.4 представлена диаграмма классов для упражнения 5.1.
81
5.2.2. Подстановка методов базового класса
По аналогии с конструкторами объект производного класса
может использовать и другие методы базового класса. Так, в программе из упражнения 5.1 объект c1 класса CountDn для увеличения значения счетчика использует метод operator++( ) базового
класса Counter, а для вывода значения счетчика на экран – метод
get_count( ) базового класса. В обоих случаях компилятор, не найдя
соответствующих методов в классе, объектом которого является
счетчик c1, использует методы базового класса.
5.2.3. Спецификатор доступа protected
Рис. 5.4
Замечание. Направление стрелки на диаграмме подчеркивает,
что производный класс ссылается на методы и поля базового класса, но при этом базовый класс не имеет доступа к производному
классу.
5.2. Доступ к базовому классу
Вопрос о правах доступа, то есть о знании того, когда для объектов производного класса могут быть использованы методы базового класса, – это важная тема в наследовании.
5.2.1. Подстановка конструктора базового класса
Результаты работы программы из упражнения 5.1 показывают,
что объект с1 класса CountDn при создании инициализируется нулевым значением, несмотря на отсутствие соответствующего конструктора в составе методов класса, поскольку при отсутствии конструктора в производном классе компилятор использует конструктор базового класса. Такая гибкость компилятора (использование
доступного метода взамен отсутствующего) – обычная ситуация
для наследования.
82
Программа из упражнения 5.1 отличается от программы из
упражнения 4.3 тем, что в ней спецификатором доступа к полю
count класса Counter служит спецификатор доступа protected (а не
private). Это обеспечивает возможность сокрытия данных базового
класса.
Как отмечалось выше, методы класса имеют доступ к членам
класса (полям и методам) со спецификаторами доступа public
и private. Однако объект класса, используемый в программе, может
получить доступ только к данным со спецификатором public
(например, с помощью операции «точка»). Так, объект objA класса A,
одним из методов которого является метод funcA( ), может иметь
доступ к этому методу из функции main( ) (или любой другой
функции, не являющейся методом класса A) только в том случае,
если метод funcA( ) открытый (то есть объявлен как public) (рис.
5.5).
В связи с этим возникает вопрос: могут ли методы производного класса иметь доступ к членам базового класса? Например,
может ли метод operator--( ) класса CountDn иметь доступ к полю
count класса Counter? Для реализации такой возможности поле
count должно иметь спецификатор доступа public или protected.
Однако использование спецификатора public разрешит доступ
к полю count из любой функции программы, ликвидировав этим
саму возможность сокрытия данных. Поэтому более приемлемым
вариантом будет использование спецификатора доступа protected.
Член класса, объявленный как protected, доступен методам своего
83
класса и методам любого производного класса, но при этом не доступен из внешних функций, не принадлежащих к этим классам,
например из функции main() (рис. 5.6).
Рис. 5.7
Рис. 5.5
Таким образом, при создании класса, который в дальнейшем
будет использоваться при наследовании как базовый, данные, которые будут доступны из производных классов, следует объявлять со
спецификатором доступа protected.
Замечание. Недостаток спецификатора доступа protected заключается в том, что при публичном распространении созданной
библиотеки классов любой программист сможет получить доступ
к членам классов, объявленным как protected, просто создавая производные классы. Это делает члены класса, объявленные как
protected, значительно менее защищенными, чем объявленные как
private.
5.2.4. Неизменность базового класса
Рис. 5.6
При наследовании базовый класс остается неизменным. Так,
например, если в рассмотренной выше программе в функции
main() объявить еще один объект c2 класса Counter, то он будет
вести себя так, как если бы класс CountDn не существовал.
Следует также отметить, что наследование не работает в обратном направлении. Базовому классу и его объектам недоступны
производные классы. Для рассматриваемой программы это означает, что объекты класса Counter, такие как c2, не могут использовать метод operator--() класса CountDn. Если есть необходимость
в уменьшении счетчика объекта c2, то он должен быть объявлен
как объект класса CountDn, а не Counter (рис. 5.8).
На рис. 5.7 представлены возможности использования спецификаторов доступа в различных ситуациях.
84
85
Упражнение 5.2. Неизменность базового класса
Упражнение 5.3. Конструкторы в производном классе
Рис. 5.8
Результат работы программы представлен на рис. 5.9.
Рис. 5.9
5.3. Конструкторы производного класса
При попытке инициализировать объект класса CountDn каким-либо отличным от нуля значением компилятор будет выдавать
сообщение об ошибке, так как он будет пытаться использовать для
инициализации создаваемого объекта конструктор базового класса
без аргументов (рис. 5.10).
Рис. 5.11
Результат работы программы представлен на рис. 5.12.
Рис. 5.10
Для решения этой задачи необходимо написать новый конструктор для производного класса (рис. 5.11).
86
Рис. 5.12
87
Данная программа использует два новых конструктора класса
CountDn. Первый – это конструктор без аргументов. В нем использована новая возможность: после двоеточия стоит имя функции.
Такая форма записи позволяет использовать конструктор класса
CountDn для вызова конструктора Counter() базового класса.
В результате при создании объекта c1 компилятор создаст объект
класса CountDn и вызовет конструктор CountDn() для его инициализации. Этот конструктор, в свою очередь, вызовет конструктор
Counter(), который и выполнит инициализацию объекта.
Замечания
1. Конструктор CountDn() кроме вызова конструктора базового класса может выполнять и собственные операции, но в данном
случае этого не требуется, поэтому пространство в круглых скобках
у него пустое.
В строке CountDn c2(11); функции main() используется конструктор класса CountDn с одним аргументом. Он также вызывает
соответствующий конструктор с одним аргументом из базового
класса и передает ему аргумент, который тот конструктор использует для инициализации поля count объекта c2.
2. Использование в производном классе своего конструктора
позволило изменить и сам метод operator--( ), сделав его методом
класса CountDn.
В следующей программе (рис. 5.13) для исправления этих дефектов создается класс Stack1, производный от Stack. Класс Stack
здесь тот же, что и в предыдущей программе, но его данные объявлены как protected. Производный класс Stack1 содержит два метода – put() и pop(), имеющие те же имена, аргументы и возвращаемые значения, что и методы класса Stack.
Для принятия решения о том, какой метод должен быть вызван при работе с объектом производного класса, существует следующее правило: если названия методов базового и производного
классов совпадают, то при вызове метода для объекта производного
класса будет выполнен метод производного класса. В этом случае
метод производного класса перегружает метод базового класса.
Замечание. Поскольку объектам базового класса ничего не
известно о производном классе, то для них всегда будут выполняться методы базового класса.
Для доступа методов put() и pop() производного класса Stack1
к методам put() и pop() базового класса Stack в программе используется операция разрешения (::), в противном случае компилятор
решит, что методы put() и pop() класса Stack1 вызывают сами себя,
что приведет к ошибке программы.
5.4. Перегрузка функций
Упражнение 5.4. Перегрузка функций базового и производного
классов
При использовании наследования для производного класса
можно определять методы, имеющие такие же имена, как и у методов базового класса. То есть в этом случае может иметь место перегрузка функций. Такая возможность может понадобиться, если
в программе для объектов базового и производного классов используются одинаковые вызовы.
Рассмотрим модернизацию программы из упражнения 3.4, моделирующей простое устройство для хранения данных в виде стека,
позволяющее помещать числа в стек, а затем извлекать их. Эта программа несовершенна, так как в ней отсутствует защита от попыток
отправить в стек слишком много чисел (в этом случае произойдет
переполнение массива st[ ] с непредсказуемыми последствиями)
или извлечь из стека больше чисел, чем в нем находится (в этом
случае начнут считываться данные, расположенные в памяти компьютера за пределами массива).
88
Рис. 5.13 (начало)
89
задачи все работники разбиваются на три категории: администрация, преподаватели и сотрудники.
В базе данных хранятся фамилии и идентификационные номера работников всех категорий. Кроме того, информация об административных работниках включает названия их должностей, а информация о преподавателях – названия кафедр и количество опубликованных статей (рис. 5.15, 5.16).
Работник
Фамилия
Номер
Администратор
Должность
Преподаватель
Сотрудник
Кафедра
Публикации
Рис. 5.15
Рис. 5.13 (окончание)
Упражнение 5.5. База данных сотрудников с использованием
наследования
Результат работы программы представлен на рис. 5.14.
Рис. 5.14
5.5. Иерархия классов
Во всех рассмотренных ранее примерах механизм наследования использовался только для добавления новых возможностей существующим классам. В следующем упражнении наследование
применяется как часть первоначальной разработки программы по
созданию базы данных работников университета. Для упрощения
Рис. 5.16 (начало)
90
91
Рис. 5.16 (окончание)
Программа начинается с описания базового класса employee,
содержащего фамилии и регистрационные номера работников. Базовый класс порождает три производных класса: manager, professor и co_worker (классы manager и professor содержат дополнительную информацию об этих категориях работников).
В функции main() объявлены четыре объекта различных классов: один объект класса manager, два объекта класса professor
и один объект класса co_worker. Они вызывают метод getdata() для
получения информации о каждом работнике и метод putdata(), обеспечивающий вывод этой информации на экран. Объекты классов
manager и professor используют для этого перегруженные методы
своих классов, а объект класса co_worker – методы базового класса.
Результат работы программы представлен на рис. 5.17.
Рис. 5.16 (продолжение)
Рис. 5.17 (начало)
92
93
Классы, подобные классу employee, которые используются
только как базовые классы для производных классов, иногда ошибочно называют абстрактными классами, подразумевая, что у них
нет объектов. Однако ниже будет показано, что термин «абстрактный класс» имеет иной смысл.
Задания для самостоятельной работы
Рис. 5.17 (окончание)
5.6. Абстрактный базовый класс
В предыдущей программе не было создано ни одного объекта
класса employee – этот класс использовался как общий, предназначенный исключительно для того, чтобы стать базовым классом для
производных классов.
Класс co-worker ничем не отличается от класса employee
и выполняет те же самые функции, однако он присутствует в программе по двум причинам:
1) его наличие подчеркивает, что все классы имеют один источник – класс employee;
2) при возникновении необходимости в модификации класса
co-worker вносить какие-либо изменения в класс employee не потребуется.
94
1. Используя механизм наследования, расширьте возможности
класса fraction для работы с обыкновенными дробями, добавив
следующие операции:
• возведение дроби в степень n (n – натуральное число);
• операции отношения («равно», «не равно», «больше»,
«меньше»).
Для этого:
• создайте новый класс, например fraction_full, который будет являться общим наследником класса fraction;
• включите в состав методов класса fraction_full необходимые функции.
В функции main() создайте несколько объектов класса fraction_full и продемонстрируйте работу методов класса.
2. Используя механизм наследования, создайте объектноориентированную программу по выводу на экран различных геометрических фигур (прямоугольников, треугольников, кругов, эллипсов, ромбов и т. п.).
Требования к структуре программы:
• все фигуры должны представлять собой объекты производных классов – наследников одного базового класса shape;
• базовый класс shape должен содержать общие для всех фигур характеристики (расположение на экране, цвет и т. д.);
• количество производных классов определите самостоятельно (но не менее трех);
• все классы должны быть реализованы в отдельных заголовочных файлах.
В функции main():
• с помощью директивы #include подключите все созданные
классы;
• создайте несколько объектов разных классов;
95
• с помощью соответствующего метода show() выведите их на
экран.
Возможный результат работы программы представлен на рис.
5.18 и 5.19.
Глава 6. ОБЩЕЕ, ЧАСТНОЕ И МНОЖЕСТВЕННОЕ
НАСЛЕДОВАНИЕ
6.1. Общее и частное наследование
Рис. 5.18
Рис. 5.19
Вопрос о доступе к членам класса при реализации механизма
наследования играет очень важную роль. Язык С++ предоставляет
большие возможности для регулирования доступа к членам классов. В частности, ключевое слово public при объявлении производного класса означает, что объект производного класса может иметь
доступ к методам базового класса, объявленным как public. Такой
механизм наследования называется общим наследованием. При использовании в аналогичной ситуации ключевого слова private
у объектов производного класса будет отсутствовать доступ к методам базового класса, объявленным как public. Такой механизм
наследования называется частным наследованием. В случае частного наследования всем объектам производного класса доступ ко
всем членам базового класса будет закрыт, поскольку доступ к методам базового класса, объявленным как private или protected,
у них отсутствует по определению.
Программа из упражнения 6.1 (рис. 6.1) демонстрирует различные комбинации доступа при наследовании.
Упражнение 6.1. Комбинации доступа при наследовании
Рис. 6.1 (начало)
96
97
Рис. 6.2
Рис. 6.1 (окончание)
В данной программе описан класс A, являющийся базовым
для двух производных классов B и C. Класс B является общим
наследником класса A, а класс C – частным наследником. Класс A
содержит данные с различными спецификаторами доступа: private,
protected и public.
Текст программы подтверждает сказанное выше: видно, что
методы обоих производных классов имеют доступ к данным базового класса, объявленным как public или protected. У объектов же
обоих производных классов отсутствует доступ к членам базового
класса, объявленным как private или protected. И, наконец, объекты частного наследника (класса C), в отличие от объектов общего
наследника (класса B), не имеют доступа к членам базового класса,
объявленным как public.
Все варианты доступа при общем и частном наследовании показаны на рис. 6.2.
98
Замечание. Если при создании класса спецификатор доступа
не указывается, то по умолчанию будет использован спецификатор
public.
Вопрос выбора спецификаторов доступа при использовании
наследования решает сам программист. Как правило, производный
класс представляет собой улучшенную или более специализированную версию базового класса. В таких случаях имеет смысл воспользоваться общим наследованием, чтобы объекты производного
класса могли предоставлять доступ как к общим методам базового
класса, так и к более специализированным методам своего класса.
Но если производный класс создается для полной модификации
действий базового класса и при этом скрывает либо изменяет его
первоначальный вид, имеет смысл использовать частное наследование, позволяющее скрыть все методы базового класса от доступа
через объекты производного класса.
6.2. Уровни наследования
Производные классы могут являться базовыми для других
производных классов. Уровень вложенности классов теоретически
бесконечен.
99
Пример:
class A
{ };
class B: public A
{ };
class C: public B
{ };
В упражнении 6.2 совершенствуется база данных сотрудников
университета из упражнения 5.5. В программу добавлен класс head,
содержащий сведения о заведующих кафедрами. Предположим, что
каждый заведующий отвечает за работу всех преподавателей кафедры, а эффективность его работы измеряется долей обучающихся на кафедре студентов, не имеющих задолженности по результатам сессии. Чем больше таких студентов, тем лучше поставлен
учебный процесс и, соответственно, тем лучше работает заведующий кафедрой.
Класс head является производным от класса professor. Поле
percent класса head представляет собой долю успевающих студентов, выраженную в процентах. На рис. 6.3 представлена диаграмма
классов для новой программы (рис. 6.4).
Упражнение 6.2. Несколько уровней наследования
Работник
Фамилия
Номер
Администратор
Должность
Преподаватель
Сотрудник
Кафедра
Публикации
Зав. кафедрой
Доля успевающих студентов
Рис. 6.3
100
Рис. 6.4 (начало)
101
6.3. Множественное наследование
Класс может быть производным от нескольких базовых классов. В этом случае говорят о множественном наследовании. Диаграмма классов для случая, когда класс C является производным
сразу двух классов (A и B), представлена на рис. 6.5.
Рис. 6.5
Следует отметить, что иерархия классов отличается от схемы
организации. На схеме организации обычно отображаются линии
команд. Иерархия классов осуществляется на основе обобщения
схожих характеристик: чем более общим является класс, тем выше
его место на схеме. Например, «преподаватель» – это более общая
характеристика, чем «заведующий кафедрой», поэтому класс professor расположен в иерархии над классом head (хотя заведующий
кафедрой получает более высокую зарплату, чем преподаватель).
Синтаксис описания множественного наследования похож на
синтаксис простого наследования: в строке описания производного
класса базовые классы перечисляются после двоеточия и разделяются запятыми.
Пример:
class A
{ };
class B
{ };
class C : public A, public B
В следующей программе реализован вариант множественного
наследования для базы данных работников университета из упражнения 5.5. Предположим, что в этой базе необходимо хранить сведения об образовании работников. Допустим, что в другой программе существует класс trainees, в котором указывается образование каждого слушателя курсов повышения квалификации. Тогда
для решения поставленной задачи вместо изменения класса
employee можно воспользоваться данными класса trainees при помощи множественного наследования.
В классе trainees содержатся сведения об учебном заведении,
которое закончил слушатель курсов, и об уровне полученного им
102
103
Рис. 6.4 (окончание)
образования. Для ввода и вывода этих данных предназначены методы get_edu() и print_edu().
Предположим, что не для всех работников университета необходимо хранить информацию об образовании: допустим, что сведения об образовании сотрудников не нужны. Тогда необходимо
изменить классы manager и professor так, чтобы они являлись производными от классов employee и trainees (рис. 6.6). Программа,
реализующая указанную схему, показана на рис. 6.7.
Работник
Упражнение 6.3. Множественное наследование
Слушатель курсов
Администратор
Преподаватель
Сотрудник
Рис. 6.6
Упрощенно взаимосвязь между классами можно представить
следующим образом:
class trainees
{ };
class employee
{ };
class manager : private employee, private trainees
{ };
class professor : private employee, private trainees
{ };
class co_worker : public employee
{ };
Рис. 6.7 (начало)
104
105
Функции getdata() и putdata() классов manager и professor
включают в себя вызовы таких функций класса trainees, как trainees::get_edu() и trainees::print_edu(). Эти методы доступны классам manager и professor, поскольку они наследуются от класса
trainees.
Классы manager и professor – частные производные классы от
классов employee и trainees. В данном случае нет необходимости
использовать общее наследование, так как объекты классов manager и professor не пользуются методами базовых классов employee
и trainees. Однако класс co_worker должен быть общим наследником класса employee, так как пользуется его методами.
Результат работы программы представлен на рис. 6.8.
Рис. 6.7 (окончание)
106
Рис. 6.8
107
Задания для самостоятельной работы
1. Для издательской компании, торгующей книгами и их аудиозаписями, создайте объектно-ориентированную программу, которая будет работать с базой данных по имеющейся продукции.
Создайте класс publication, где в качестве полей данных хранятся название title (массив типа char или string) и цена price (тип
float) книги. От этого класса наследуются еще два класса: класс
book, хранящий информацию о количестве страниц в книге (поле
pages типа int), и класс tape, содержащий время записи книги в минутах (поле time типа float). В каждом из трех классов должен быть
метод getdata() для получения данных от пользователя с клавиатуры и метод putdata() для вывода данных на экран.
Протестируйте работу классов в функции main(), создав по
одному объекту классов book и tape.
Результаты работы программы показаны на рис. 6.9.
• измените классы book и type так, чтобы они стали производными от двух классов – publication и sales; в результате объекты классов book и type должны вводить и выводить данные о продажах вместе с другими своими данными.
Протестируйте работу классов в функции main().
Результат работы программы показан на рис. 6.10.
Рис. 6.10
Рис. 6.9
2. Примените механизм множественного наследования для
предыдущей программы:
• добавьте в нее базовый класс sales, содержащий массив из
трех значений типа float, куда записывается общая стоимость книг,
проданных за последние 3 месяца;
• включите в состав методов класса функции getdata() –
для получения значений стоимости от пользователя и putdata() –
для вывода их на экран;
108
109
Глава 7. ПОТОКОВЫЕ КЛАССЫ. ИСПОЛЬЗОВАНИЕ
ФАЙЛОВ ДЛЯ ВВОДА И ВЫВОДА ДАННЫХ
Большинству программ необходимо сохранять данные на
диске и считывать их оттуда. Любая информация хранится на диске
в виде файлов.
Можно выделить следующие основные особенности работы
с файлами:
• файл идентифицируется по своему имени на диске;
• один и тот же файл можно использовать как для записи
информации, так и для ее считывания;
• поскольку файлы могут иметь разные имена, файловый
объект невозможно объявить компилятору заранее.
В С++ работа с файлами аналогична работе с дисплеем или
клавиатурой и основана на использовании потоков. Потоки С++
обеспечивают универсальные методы обработки данных,
поступающих с клавиатуры или диска, а также выводимых на экран
(записываемых на диск).
В С++ можно обрабатывать два вида файлов:
1) текстовые файлы;
2) бинарные (двоичные) файлы.
Текстовые файлы организованы в символьные строки, каждая
из которых отсечена символом конца строки (endl или \n). При
этом все данные хранятся в удобочитаемом виде, а поля данных
разделены пробелами, поэтому содержимое файла можно легко
понять.
Бинарные файлы не организованы в строки. В них одна часть
информации непрерывно переходит в другую, а числа хранятся
в том виде, в каком содержатся в памяти компьютера, то есть
в виде двоичных кодов, а не привычных десятичных цифр. Поэтому
прочесть распечатанный бинарный файл весьма затруднительно.
7.1. Потоковые классы
назначены для представления различных типов данных. В предыдущих упражнениях для обмена данными с экраном и клавиатурой
мы использовали потоковые объекты cout и cin класса iostream,
а также методы вставки (<<) и извлечения (>>), принадлежащие
тому же классу.
Поскольку выходной поток был всегда направлен на экран,
а входной поток шел с клавиатуры, не было необходимости назначать потоковым объектам cout и cin специальные имена. При работе с файлами может понадобиться несколько файлов для ввода
и вывода (раньше для этого требовались, соответственно, один объект cin и один объект cout). Поэтому при работе с файлами необходимо сначала объявить объекты соответствующего потокового
класса, а затем инициализировать их именами нужных файлов.
Пользоваться функциями-членами потокового класса можно
только после объявления объектов этого класса. Задать поток ввода/вывода можно точно так же, как для объектов cout и cin, благодаря тому, что операции вставки (<<) и извлечения (>>) перегружены в потоковых классах для работы с файлами. При этом доступны
все манипуляторы и флаги форматирования, использовавшиеся ранее при работе с объектами cout и cin.
Потоковые классы имеют достаточно сложную иерархическую структуру (рис. 7.1).
ios
ostream
istream
fstreambase
iostream
ifstream
istream_witthassig
fstream
iostream_withassig
Поток – это общее название потока данных. В С++ поток
представляет собой объект некоторого класса. Разные потоки пред-
Рис. 7.1
110
111
ofstream
ostream_withassign
Из рис. 7.1 видно, что класс ios является базовым для всех потоковых классов. Он содержит множество констант и методов, общих для операций ввода/вывода любых видов (например, флаги
форматирования).
Классы istream и ostream являются наследниками класса ios
и предназначены, соответственно, для ввода и вывода. Класс
istream содержит функции get(), getline(), read() и перегружаемую
операцию извлечения (>>), а класс ostream – функции put(), write()
и перегружаемую операцию вставки (<<).
Класс iostream – наследник классов istream и ostream (пример множественного наследования). Его производные классы можно использовать при работе с такими объектами, как дисковые
файлы, которые могут быть открыты одновременно для записи
и чтения.
Классы istream_withassign, ostream_withassign и iostream_
withassign являются наследниками классов istream, ostream
и iostream соответственно. В частности, объект cout, представляющий собой стандартный выходной поток, который обычно выводит данные на экран, является предопределенным объектом класса
ostream_withassign. Аналогично cin – это предопределенный объект класса istream_withassign.
Набор специальных классов ifstream, ofstream и fstream создан для работы с дисковыми файлами. Объекты этих классов могут быть ассоциированы с дисковыми файлами, а методы классов
могут использоваться для обмена данными с ними.
Класс ifstream является производным от класса istream
и предназначен для считывания данных из файлов.
Класс ofstream является производным от класса ostream
и предназначен для записи данных в файл.
Класс fstream является производным от класса iostream
и предназначен как для считывания данных из файлов, так и для
записи данных в файл.
Все три вышеописанных файловых класса используют принцип множественного наследования, наследуя свойства одного базового класса – fstreambase, который содержит объект класса filebuf
(файлового буфера), а также ассоциированные методы, унаследованные от более общего класса streambuf.
112
Классы, используемые для вывода данных на экран и ввода
данных с клавиатуры, описаны в заголовочном файле iostream.
Классы, используемые для обмена данными с файлами, объявлены
в заголовочном файле fstream.
7.2. Работа с текстовыми файлами
При форматированном вводе/выводе данных числа хранятся
на диске в виде серии символов. Таким образом, вещественное число 15,265 хранится не в виде четырехбайтного значения типа float
или восьмибайтного значения типа double, а в виде последовательности символов `1` `5` `,` `2` `6` `5` и занимает, соответственно,
шесть байт (один байт под каждый символ). Это может показаться
несколько странным с точки зрения экономии памяти, особенно
в случае многоразрядных чисел, но зато легко применимо на практике и в некоторых ситуациях гораздо удобнее хранения чисел
в двоичном формате.
7.2.1. Запись данных в файл
Чтобы приступить к записи в файл, необходимо создать объект класса ofstream (или, в общем случае, класса fstream), а затем
связать его с определенным файлом на диске (рис. 7.2).
Замечание. Использование объектов классов ifstream,
ofstream и fstream требует подключения в программе заголовочного файла <fstream>.
Упражнение 7.1. Простейший пример записи в файл символьной
строки
Рис. 7.2
113
В результате работы программы на диске создается текстовый
файл с именем prim_1.txt, в который записывается строка “Hello,
World !”. Открыв этот файл в любом текстовом процессоре, можно
просмотреть его содержимое (рис. 7.3).
Рис. 7.3
полей фиксированной длины, это единственный шанс узнать при
извлечении, где кончается одно число и начинается другое. Вовторых, между строками тоже должны быть разделители – по тем
же причинам. Это подразумевает, что внутри строки не может быть
пробелов.
В следующем примере (рис. 7.4) рассмотрена запись в дисковый файл символа, целого числа, вещественного числа и символьной строки.
Упражнение 7.2. Форматированный вывод в файл данных различных типов в одну строку
Замечание. Поскольку каталог для хранения файла не был
указан, файл был создан в текущем каталоге (то есть в том, где
находится создаваемый проект). Если необходимо записать файл
в конкретное место на диске, то при открытии файла нужно указывать его полное имя, например:
char * file = "C:\\Users\\BukuS\\Desktop\\output_2.txt";
При этом обратная косая черта \ в полном имени пути к файлу
в аргументе функции заменяется двойной обратной чертой \\.
В данной программе сначала создается объект myfile класса
ofstream, а затем происходит его инициализация файлом
prim_1.txt. Инициализация резервирует для дискового файла
с данным именем различные ресурсы и получает доступ к нему
(открывает файл). Если файл не существует – он создается, если
файл уже существует – он переписывается (старые данные в нем
заменяются новыми).
Для записи в открытый файл текстовых данных, как и для
консольного вывода данных на экран, используется операция
вставки (<<), то есть объект myfile ведет себя подобно объекту cout
из предыдущих программ. Это возможно благодаря тому, что оператор вставки (<<) перегружен в классе ostream, который является
базовым для класса ofstream.
По завершении работы программы объект myfile вызывает
свой деструктор, который закрывает файл, поэтому нет необходимости делать это явным образом.
При форматированном выводе данных в дисковые файлы может возникнуть несколько проблем. Во-первых, надо разделять
числа (например, 88 и 54,09) нечисловыми символами: поскольку
числа хранятся в виде последовательности символов, а не в виде
При сохранении данных на диск можно записывать их в отдельных строках. Для этого необходимо после вывода каждой переменной вставлять в поток манипулятор endl, осуществляющий
переход на новую строку (аналогично выводу на экран с использо-
114
115
Рис. 7.4
Результат работы программы представлен на рис. 7.5.
Рис. 7.5
ванием объекта cout) (рис. 7.6). В этом случае отпадает необходимость использовать символ-разделитель.
Упражнение 7.4. Форматированное чтение из файла данных
различных типов
Упражнение 7.3. Форматированный вывод в файл данных
различных типов в разные строки
Рис. 7.8
Рис. 7.6
Результат работы программы представлен на рис. 7.7.
В данной программе объект класса ifstream действует так же,
как и объект cin в предыдущих программах. При считывании
данные сохраняются в соответствующих переменных, после чего
выводятся на экран. При этом числа приводятся к двоичному
представлению, чтобы с ними можно было работать в программе.
Так, число 88 сохраняется в переменной i типа int, число 54.09 –
в переменной d типа double и т. д.
Результат работы программы представлен на рис. 7.9.
Рис. 7.7
7.2.2. Чтение данных из файла
Прочитать файл prim_2.txt, созданный предыдущей программой, можно с использованием объекта ifstream, инициализированного именем файла (рис. 7.8). Файл автоматически открывается при
создании объекта, после чего можно считать из него данные с помощью операции извлечения (>>).
116
Рис. 7.9
Замечания
1. Расположение данных в файле (в одной строке или в разных) не принципиально при считывании. Программа из упражнения 7.4 корректно считает и данные из файла prim_2.txt, расположенные в одной строке, и данные из файла prim_2_1.txt,
расположенные на разных строках. Принципиальным является разделение данных в файле любым символом-разделителем (в первом
случае это пробел, во втором – символ конца строки).
117
2. При построчном выводе на экран считанных данных, как
и при построчной записи в файл, отпадает необходимость в использовании символа-разделителя. То есть при использовании манипулятора endl при выводе данных на экран (рис. 7.10) будет получен
результат, показанный на рис. 7.11.
Упражнение 7.5. Запись в файл строк с пробелами
Рис. 7.10
Рис. 7.13
В результате работы программы будет создан файл prim_4.txt
(рис. 7.14).
Рис. 7.11
7.2.3. Чтение и запись строк с пробелами
Программа из упражнения 7.4 не позволяет обрабатывать
строки, содержащие пробелы. Например, если программа из
упражнения 7.3 запишет в файл строку
то при выводе данных из файла на экран программой из упражнения 7.4 будет получен результат, показанный на рис. 7.12.
Рис. 7.14
Следующая программа (рис. 7.15) реализует чтение из файла
prim_4.txt строк с пробелами.
Упражнение 7.6. Чтение из файла строк с пробелами
Рис. 7.12
Для корректного считывания из файла символьных строк, содержащих пробелы, необходимо при записи данных в файл в конце
каждой строки вставлять специальный символ-ограничитель (как
правило, endl или \n) (рис. 7.13), а при чтении данных из файла
вместо операции извлечения (>>) использовать метод getline().
Рис. 7.15
118
119
Результатом работы программы будет корректный вывод на
экран строк, содержащихся в файле prim_4.txt (рис. 7.16).
Значение каждого флага можно проверить с помощью функций класса ios, возвращающих значения TRUE или FALSE
(табл. 7.2.2).
Таблица 7.2.2
Функция
int = eof();
int = fail();
Рис. 7.16
В программе из упражнения 7.6 для считывания строк используется функция getline(), являющаяся методом класса istream. Она
считывает все символы, включая разделители, пока не дойдет до
специального символа endl или \n, а затем помещает результат чтения в буфер, переданный ей в качестве аргумента. Максимальный
размер буфера передается с помощью второго аргумента. Содержимое буфера выводится на экран после считывания каждой строки.
Замечание. Подход, использованный в программе из упражнения 7.6, нельзя применять к чтению произвольных файлов, так
как в данной программе подразумевается, что каждая строка считываемого текста заканчивается символом endl или \n. При попытке прочитать таким способом файл с другой структурой данных
программа будет работать некорректно («зависать»).
7.2.4. Флаги статуса ошибок при работе с файлами
Объекты порожденных из ios классов содержат флаги статуса
ошибок, с помощью которых можно проверить результаты выполнения операций ввода/вывода (табл. 7.2.1).
Таблица 7.2.1
Название флага
goodbit
eofbit
failbit
badbit
hardfail
Значение флага
Ошибок нет
Достигнут конец файла
Операция не выполнена
Недопустимая операция
Неисправимая ошибка
120
int = bad();
int = good();
clear(int = 0);
Назначение
Возвращает TRUE, если установлен флаг eof
Возвращает TRUE, если установлены флаги failbit,
badbit или hardfail
Возвращает TRUE, если установлены флаги badbit или
hardfail
Возвращает TRUE, если ошибок не было
При использовании без аргумента снимает все флаги
ошибок, в противном случае устанавливает указанный
флаг, например clear(ios::badbit);
Результат работы функций из табл. 7.2.2 можно описать следующим образом:
• eof() – возвращает значение TRUE, если при чтении информации из объекта класса ifstream или fstream достигнут конец
файла (end of file – конец файла);
• bad() − возвращает значение TRUE при попытке выполнить
ошибочную операцию;
• fail() − возвращает значение TRUE при попытке выполнить
ошибочную либо невыполнимую в данных условиях операцию;
• good() − возвращает значение TRUE, когда все идет хорошо
(то есть в тех случаях, когда все остальные функции возвращают
значение FALSE).
Функция eof() очень полезна при считывании информации из
файлов, поскольку объем данных в файле, как правило, заранее не
известен. Именно поэтому функция eof() была использована в программе из упражнения 7.6 для определения момента прекращения
считывания данных из файла (использовался вариант цикла с предусловием: пока не достигнут конец файла).
Следует иметь в виду, что при проверке конкретного флага
признака окончания файла программа не проверяет все остальные
флаги ошибок. Флаги failbit и badbit также могут возникнуть при
работе программы, хотя такое случается довольно редко. Поэтому
121
для проверки всех возможных ошибок при работе с файлом можно
в программе из упражнения 7.6 изменить условие цикла (рис. 7.17).
В данной программе длина строки, созданной как объект
класса string, находится с помощью метода size(), а символы записываются в файл prim_6.txt в цикле for функцией put().
Результат работы программы показан на рис. 7.20.
Рис. 7.17
Можно также проверять поток напрямую. Любой потоковый
объект (например, объект myfile) имеет значение, которое может
тестироваться на предмет выявления наиболее распространенных
ошибок, включая eof. Если какое-либо из условий ошибки имеет
значение TRUE, объект возвращает ноль; если процесс работы
с файлом идет хорошо, объект возвращает ненулевое значение. Поэтому в программе из упражнения 7.6 возможен еще один вариант
организации цикла (рис. 7.18).
Рис. 7.20
Для считывания текста, записанного в файл prim_6.txt, можно
использовать функцию get() (рис. 7.21–7.24).
Упражнение 7.8. Пример некорректного посимвольного
считывания из файла
Рис. 7.18
7.2.5. Ввод/вывод символов
Для записи в файл и считывания из него единичных символов
можно использовать функции put() и get(), являющиеся методами
классов ostream и istream соответственно (рис. 7.19).
Упражнение 7.7. Посимвольная запись в файл
Рис. 7.21
Рис. 7.19
122
В данной программе считывание данных из файла prim_6.txt
осуществляется с помощью функции get(). Считывание
производится до признака окончания файла или до возникновения
ошибки. Каждый прочитанный символ выводится на экран
с помощью объекта cout.
Результат работы программы представлен на рис. 7.22.
123
Результат работы программы представлен на рис. 7.24.
Рис. 7.22
Замечание. При внимательном рассмотрении рис. 7.22 видно,
что выведенная на экран строка заканчивается четырьмя
восклицательными знаками, в то время как в файле prim_6.txt
в конце строки их три (см. рис. 7.20). Ошибочное появление на
экране лишнего знака стало следствием неправильной организации
проверки на наличие символа конца файла. Важно вводить такую
проверку сразу после начала считывания данных, так как символ
конца файла является последним символом файла.
Поскольку в программе из упражнения 7.8 эта проверка
осуществляется в начале цикла, то на последней итерации идет
работа с неправильными данными: после получения символа конца
файла на экран еще раз выводится последний считанный символ `!`.
Возможный вариант исправления данной ошибки показан на
рис. 7.23.
Существует еще один способ считывания символов из файла:
использование функции rdbuf(), являющейся методом класса ios.
Функция rdbuf() возвращает указатель на объект класса streambuf
или filebuf, ассоциированный с потоковым объектом. В этом объекте находится буфер символов, считанных из потока, поэтому указатель на него можно использовать в качестве объекта данных.
У функции rdbuf() есть отличительная особенность: она сама знает,
что нужно завершить работу программы при достижении конца
файла. Это позволяет еще больше сократить размер программного
кода, реализующего посимвольный вывод данных с помощью этой
функции (рис. 7.25).
Упражнение 7.9. Пример корректного посимвольного считывания
из файла
Упражнение 7.10. Самая короткая программа посимвольного
считывания из файла
Рис. 7.24
Рис. 7.25
Рис. 7.23
124
125
7.2.6. Настройки открытия/закрытия файлов для ввода/вывода
данных. Режимы доступа к файлам
Во всех предыдущих программах создавались файловые объекты, предназначенные либо для ввода, либо для вывода данных.
Однако возможно создание файла, который может использоваться
и для записи, и для считывания данных. Чтобы файл одновременно
поддерживал ввод и вывод данных, он должен быть ассоциирован
с объектом класса fstream, являющегося наследником класса
iostream.
После объявления такого файла операции над ним можно совершать с помощью функций-членов класса fstream, наиболее
важными из которых, наряду с функцией eof(), являются функции
open() и close().
Функция open() устанавливает соответствие между файловым
объектом и данными на диске и определяет режим доступа к файлу
(ввод, вывод, добавление и др.). Синтаксис функции open():
open(char*имя_файла, int режим_доступа);
Функция получает два аргумента: строку символов
(имя_файла), определяющую имя файла на диске, и флаг (режим_доступа), определяющий режим доступа к файлу.
В символьной строке можно задать полный путь к файлу,
включая диск, каталог и т. д. При этом обратная косая черта \
в полном пути к файлу в аргументе функции заменяется двойной
обратной косой чертой \\.
Функция close() обеспечивает правильное закрытие файла
и сохранение данных на диске. Иногда данные попадают на диск не
сразу и при неправильном закрытии файла могут быть потеряны.
Функция close() не имеет параметров.
При связывании объекта класса fstream с именем файла по
умолчанию создается новый файл с указанным именем (если таковой не существует) или удаляется содержимое уже существующего
файла с таким же именем. Изменить установки по умолчанию можно с помощью соответствующих флагов класса ios, которые могут
устанавливать и другие режимы доступа к файлу (табл. 7.2.3).
Флаги режима доступа можно комбинировать, используя логический оператор дизъюнкции |.
126
Таблица 7.2.3
Флаг режима
ios::in
ios::out
ios::app
ios::ate
ios::_Nocreate
ios::_Noreplace
ios::trunc
ios::binary
Результат установки флага
Открытие файла для чтения (для ifstream – по умолчанию)
Открытие файла для записи (для ofstream – по умолчанию)
Открытие файла для записи, начиная с конца файла
Открытие файла для чтения, начиная с конца файла
Открытие файла только в том случае, если он уже существует
Если файл уже существует, то операция открытия не
выполняется, если не установлен флаг ios::app или
ios::ate
Полное удаление текущего содержимого файла (устанавливается по умолчанию)
Открытие файла в бинарном (не текстовом) режиме
Пример:
myfile.open(“prim_6.txt”, ios::out | ios::app | ios:_Nocreate);
В этом случае программа попытается открыть файл prim_6.txt
для добавления данных в конец файла. Если этот файл не будет
найден в текущем каталоге, то новый файл создан не будет и открытие файла не состоится.
При работе с файлами всегда полезно проверять, насколько
правильно они открываются (например, файл может не открыться,
если его нет в указанном месте). Для проверки правильности открытия файла необходимо вызвать функцию is_open(). Если она
возвращает ноль, значит файл не открылся.
Пример:
myfile.open(“prim_6.txt”, ios::in | ios::_Nocreate);
if(myfile.is_open()==0)
cout << “Ошибка открытия файла”;
else
В программе из следующего упражнения показан пример работы с одним файлом в режимах записи и считывания данных.
Программа реализует запись в файл и считывание из него информации о сотрудниках некоторого предприятия. Каждая строка
в файле с данными содержит идентификационный номер сотрудника, имя, фамилию и оклад. При этом идентификационные номера не
127
образуют непрерывной последовательности (некоторые из них
пропущены), что типично для большинства файлов.
Информация о сотрудниках хранится на диске в двух текстовых файлах: list.txt и list_1.txt (рис. 7.26).
Рис. 7.26
В результате работы программы на экран сначала выводятся
данные из файла list.txt, затем данные из файла list_1.txt. После
этого данные из файла list_1.txt добавляются к данным из файла
list.txt и на экран выводится обновленный список, находящийся
в файле list.txt (рис. 7.27).
Упражнение 7.11. Пример одновременной работы с файлом
в режиме ввода/вывода
Рис. 7.27 (окончание)
Рис. 7.27 (начало)
128
129
Результат работы программы представлен на рис. 7.28 и 7.29.
Рис. 7.28
тогда как его текстовая версия – пять байт; вещественное число
8.12785e14 также занимает четыре байта, а не десять.
В С++ для двоичных файлов поддерживается широкий диапазон функций ввода/вывода, позволяющих точно контролировать
процессы записи и считывания данных.
Для считывания и записи блоков двоичных данных чаще всего
используются функции read() (метод класса ifstream) и write() (метод класса ofstream). Они работают с данными в терминах байтов
(тип char). Этим функциям безразлично, как организованы данные
и каково их содержание, они просто переносят байты из буфера
в файл и обратно. Параметрами обеих функций служат адрес буфера и его длина в байтах (а не число элементов данных в нем). Адрес должен быть приведен к типу char*. Кроме того, при работе
с двоичными (бинарными) данными необходимо указывать в аргументах функций соответствующий флаг форматирования –
ios::binary.
На рис. 7.30 показан пример записи в файл целочисленного
массива в двоичном формате, который затем считывается из файла
и выводится на экран.
Упражнение 7.12. Двоичный ввод/вывод в файл целых чисел
Рис. 7.29
7.3. Файловый ввод/вывод двоичных файлов
7.3.1. Двоичный ввод/вывод данных стандартных типов
Форматированный файловый ввод/вывод чисел целесообразно
использовать только при их малых размерах и небольшом количестве. Во всех прочих случаях эффективнее оказывается двоичный
ввод/вывод, при котором числа обрабатываются в том виде, в котором хранятся в памяти компьютера, а не в виде символьных строк.
При этом целочисленное значение 54321 занимает четыре байта,
Рис. 7.30
130
131
Результат работы программы представлен на рис. 7.31 и 7.32.
Упражнение 7.13. Двоичный ввод/вывод данных структурного
типа
Рис. 7.31
Рис. 7.32
В программе используется явное приведение буфера данных
типа int к типу char. Из рис. 7.32 видно, что при открытии файла
с двоичными данными в текстовом редакторе нельзя просмотреть
записанную информацию в привычном виде.
Замечание. В программе и входной, и выходной потоки связаны с одним и тем же файлом (prim_11.txt), поэтому первый поток должен быть закрыт до того, как откроется второй. Для этого
в программе используется функция close().
7.3.2. Двоичный ввод/вывод данных пользовательского типа
Поскольку С++ является объектно-ориентированным языком,
имеет смысл рассмотреть вопрос о том, как происходит обмен
данными с файлами в случае пользовательских типов данных.
В упражнении 7.13 (рис. 7.33) представлена программа, которая
последовательно записывает в файл в двоичном виде данные
о сотрудниках фирмы, вводимые пользователем с клавиатуры,
а затем выводит весь список на экран. Для хранения данных
в программе создается структура person.
Рис. 7.33
132
133
Результаты работы программы представлены на рис. 7.34 и 7.35.
Рис. 7.34
сительно начала файла), с которого будет производиться чтение
или запись.
Как правило, чтение файла начинается с его начала и продолжается до его конца. Запись информации в файл тоже обычно
начинается либо с начала файла (при этом вся имеющаяся в файле
информация удаляется), либо с его конца (режим дозаписи файла
с использованием флага ios::app). Однако иногда возникает необходимость считывать или записывать информацию, начиная с произвольного места файла. В таких случаях для установки и проверки
текущего указателя чтения используются функции seekg() и tellg().
Для указателя записи те же действия выполняют функции seekp()
и tellp().
Для установки файлового указателя в произвольное место
файла используются два варианта функций seekg() и seekp().
В первом варианте функции имеют один аргумент, представляющий собой абсолютную позицию от начала файла, которое
принимается за ноль (рис. 7.36).
Рис. 7.35
В программе из упражнения 7.13 информация о каждом
сотруднике записывается в структурную переменную pers. Затем
содержимое переменной с помощью функции write() записывается
в двоичном формате на диск в файл list_2.txt. Для нахождения
длины данных переменной pers (в байтах) используется метод
sizeof(), а для чтения всей содержащейся в файле информации –
функция read().
7.4. Указатели файлов. Чтение и запись данных в произвольное
место файла
У каждого файла есть два связанных с ним значения: указатель чтения и указатель записи (текущая позиция чтения и текущая позиция записи). Эти значения определяют номер байта (отно134
Рис. 7.36
Во втором варианте используются функции с двумя аргументами: первый из них задает сдвиг указателя относительно определенной позиции в файле, а второй – саму эту позицию (рис. 7.37).
Возможно три варианта значения второго аргумента:
• beg – начало файла;
• end – конец файла;
• cur – текущая позиция указателя файла.
Например, выражение
seekg(-8, ios::end);
установит указатель чтения за восемь байт до конца файла.
135
В программе присутствует функция tellg(), которая возвращает текущую позицию указателя чтения, использующуюся для вычисления длины файла в байтах. Поскольку длина одной записи известна (количество байт, отводимое под один элемент типа int),
то, найдя общую длину файла в байтах, можно определить количество элементов, хранящихся в файле.
Возможный результат работы программы представлен
на рис. 7.39.
Рис. 7.37
В следующей программе (рис. 7.38) вариант функции seekg()
с двумя аргументами используется для нахождения произвольного
элемента целочисленного массива, сформированного и записанного
в файл prim_11.txt в упражнении 7.12.
Упражнение 7.14. Использование указателя файла при чтении
произвольного элемента массива
Рис. 7.39
Задания для самостоятельной работы
1. Составьте программу, которая будет считывать заданные
текстовые файлы (F101.txt, F123.txt, F134.txt и F135.txt) с разными
формами бухгалтерской отчетности банка УРАЛСИБ и записывать
все данные в один новый файл с именем Уралсиб.txt.
Считывание и запись файлов реализуйте в отдельной функции, аргументами которой будут служить имена входного и выходного файлов. Кроме того, реализуйте вывод на экран сообщений по
факту записи каждого файла.
Местоположение файлов на диске – произвольное; вывод содержимого каждого файла на экран – по желанию.
По окончании работы программы откройте вновь созданный
файл Уралсиб.txt в текстовом редакторе и убедитесь в корректности записи всех файлов.
Возможный результат работы программы показан на рис. 7.40.
Рис. 7.38
136
137
Глава 8. ВИРТУАЛЬНЫЕ ФУНКЦИИ. АБСТРАКТНЫЕ
КЛАССЫ. ДРУЖЕСТВЕННЫЕ ФУНКЦИИ
Рис. 7.40
2. Составьте программу, которая будет по выбору пользователя считывать из произвольного места файла записи о сотрудниках
фирмы. В качестве исходного файла используйте двоичный файл
list_2.txt, сформированный программой из упражнения 7.13.
Результат работы программы показан на рис. 7.41.
Рис. 7.41
138
Виртуальные и дружественные функции, а также перегружаемые операции и методы относятся к прогрессивным особенностям
языка С++. Однако их использование не всегда оправдано и необходимо – небольшие программы вполне могут обойтись и без них.
В то же время в сложных и серьезных программах они применяются достаточно часто. В частности, виртуальные функции необходимы для реализации на практике полиморфизма − одного из краеугольных понятий объектно-ориентированного программирования
наряду с инкапсуляцией и наследованием.
8.1. Виртуальные функции
Виртуальный − означает «видимый, но не существующий
в реальности». Использование виртуальных функций позволяет
хранить в одном массиве объекты разных классов, а также вызывать методы разных классов с помощью одного и того же выражения.
Способность одной и той же функции обрабатывать данные
разных типов называется полиморфизмом.
Для использования полиморфизма необходимо выполнение
двух условий:
1) все классы должны являться наследниками одного и того
же базового класса;
2) функция должна быть объявлена виртуальной (virtual)
в базовом классе.
В следующем примере (рис. 8.1) базовый и производные классы содержат функции с одним именем, а обращение к ним осуществляется с помощью указателей, но без использования виртуальных функций.
139
Упражнение 8.1. Доступ к обычным функциям через указатели
Ответ на вопрос, какая из трех функций show() будет вызываться в каждом случае, содержится в результатах работы программы (рис. 8.2).
Рис. 8.2
Из рис. 8.2 видно, что компилятор игнорирует содержимое
указателя ptr и выбирает ту функцию, которая соответствует типу
указателя (рис. 8.3).
Рис. 8.3
Рис. 8.1
В данном примере классы Derv1 и Derv2 являются общими
наследниками базового класса Base. В состав методов каждого из
трех классов входит функция show().
В функции main() создаются два объекта производных классов и указатель на базовый класс. Затем указатель инициализируется сначала адресом объекта одного класса, а затем адресом объекта
другого класса (это можно делать, поскольку указатели на объекты
производных и базового классов совместимы по типу). После инициализации указателя каждый раз происходит вызов функции
show().
140
Если в программе из упражнения 8.1 перед объявлением
функции show() в базовом классе Base поставить ключевое слово
virtual, то результат будет иным (рис. 8.4).
Упражнение 8.2. Пример использования указателей
Рис. 8.4 (начало)
141
Рис. 8.6
При наличии виртуальных функций на этапе компиляции программы содержимое указателя ptr неизвестно, поэтому компилятор
откладывает принятие решения до фактического запуска программы.
Такой подход называется поздним (динамическим) связыванием.
Рис. 8.4
Результат работы программы представлен на рис. 8.5.
Рис. 8.5
Как видно, теперь один и тот же вызов функции ptr ->show()
приводит к выполнению разных функций в зависимости от содержимого указателя (рис. 8.6).
При отсутствии виртуальных функций (упражнение 8.1) решение о том, какую из функций show() вызывать, принимается
компилятором на этапе компиляции программы. В этом случае, как
было показано выше, всегда компилируется вызов функции show()
из базового класса Base. Такой подход называется ранним (статическим) связыванием.
142
8.2. Абстрактные классы и чистые виртуальные функции
Абстрактным классом называется базовый класс, объекты
которого никогда не будут созданы. Он создается, чтобы служить:
• родительским для производных классов, объекты которых
будут создаваться в программе;
• звеном для создания иерархической структуры классов.
Исключить возможность создания объектов базового класса
можно следующим образом:
• заявить об этом в документации к программе (но это никак
не гарантирует от использования базового класса не по назначению);
• защитить базовый класс программными средствами.
Для программной защиты базового класса достаточно ввести
в состав его методов хотя бы одну чистую виртуальную функцию
(рис. 8.7).
Чистая виртуальная функция – это функция, после объявления которой добавлено выражение = 0. Знак равенства в данном
143
случае не имеет ничего общего с операцией присваивания. Конструкция = 0 – это просто способ сообщить компилятору, что
функция будет чистой виртуальной.
Упражнение 8.3. Чистая виртуальная функция
Замечание. При использовании чистой виртуальной функции
ее определение не является обязательным, достаточно только ее
объявления или прототипа (см. упражнение 8.3). При необходимости можно написать и определение функции.
Особенности работы с абстрактными классами:
• при наличии в базовом классе чистой виртуальной функции
необходимо исключить ее употребление во всех производных классах, иначе производный класс сам станет абстрактным и создавать
его объекты будет невозможно (однако на производные от него
классы это ограничение уже не будет действовать);
• в принципе, все виртуальные функции базового класса можно сделать чистыми.
В программе из упражнения 8.3, в отличие от программы из
упражнения 8.2, вместо одного указателя на базовый класс Base
используется массив указателей. Каждый элемент массива инициализируется адресом объекта соответствующего производного класса. Таким образом, в массиве указателей хранятся адреса объектов
разных классов, но при этом вызовы функций для них выглядят абсолютно одинаково: ptr[i] -> show(). Результаты работы обеих программ совпадают.
В программе из упражнения 8.4 (рис. 8.8) показан один из вариантов использования абстрактного класса на примере класса person.
Упражнение 8.4. Виртуальные функции в классе person
Рис. 8.7
В программе из упражнения 8.3 виртуальная функция show()
объявлена как чистая виртуальная функция. Результатом этого является невозможность создания объектов базового класса Base
в функции main().
Рис. 8.8 (начало)
144
145
В данной программе к классу person добавлены два его общих
наследника – классы sportsman и inventor, каждый из которых содержит метод isOutstanding(), определяющий наиболее выдающихся лиц из числа введенных пользователем.
Класс person здесь – абстрактный, поскольку содержит чистые виртуальные функции getdata() и isOutstanding(). Никакие
объекты класса person не могут быть созданы – он предназначен
исключительно для того, чтобы играть роль базового для классов
sportsman и inventor.
Возможный результат работы программы представлен на рис. 8.9.
Рис. 8.9
8.3. Виртуальные деструкторы
При использовании абстрактных классов деструкторы
базового класса обязательно должны быть виртуальными. В противном случае при удалении объекта производного класса путем
применения операции delete к указателю базового класса,
указывающему на этот объект, деструктор, будучи обычным
методом, вызовет деструктор для базового класса вместо
деструктора для производного класса. Это приведет к удалению
только той части объекта, которая относится к базовому классу
(рис. 8.10).
Рис. 8.8 (окончание)
146
147
Упражнение 8.5. Тестирование обычных и виртуальных
деструкторов
Теперь обе части объекта производного класса удалены корректно.
Замечания
1. Если ни один из деструкторов не совершает важных
действий (например, они просто освобождают память, занятую
с помощью операции new), то их виртуальность не является
обязательной. Однако в общем случае для уверенности в корректном удалении объектов производных классов следует
деструкторы в базовых классах делать виртуальными.
2. В большинстве библиотек классов имеется базовый класс
с виртуальным деструктором, что гарантирует наличие виртуальных деструкторов в производных классах.
8.4. Виртуальные базовые классы
Рис. 8.10
При использовании в объектно-ориентированной программе
механизма множественного наследования могут возникнуть
проблемы с доступом методов производного класса к данным или
методам базового класса.
Рассмотрим возможность возникновения таких проблем
для программы, использующей иерархию классов, показанную
на рис. 8.13.
parent
Результат работы программы представлен на рис. 8.11.
Рис. 8.11
Как видно, деструктор для объекта класса Derv не вызывается,
так как деструктор базового класса в программе не виртуальный.
Исправить это можно, закомментировав первую строку определения конструктора и активизировав вторую строку.
Результат показан на рис. 8.12.
child1
child2
grandchild
Рис. 8.13
Рис. 8.12
148
149
Здесь класс parent является базовым, а классы child1 и child2 –
его производными классами. Класс grandchild является
производным от двух классов – child1 и child2.
Программа из упражнения 8.6 (рис. 8.14) демонстрирует,
какие проблемы могут возникнуть, если метод класса grandchild
попытается получить доступ к данным или методам класса parent.
Упражнение 8.7. Виртуальные базовые классы
Упражнение 8.6. Неоднозначная ссылка на базовый класс
Рис. 8.15
Рис. 8.14
Ошибка компилятора возникла из-за того, что метод getdata()
класса grandchild попытался получить доступ к полю basedata
класса parent. Причина ошибки заключается в том, что каждый из
производных классов (child1 и child2) наследует свою копию базового класса, называемую подобъектом. Каждый из двух созданных
подобъектов содержит собственную копию данных базового класса, включающую и поле basedata. Когда метод класса grandchild
пытается получить доступ к полю basedata, компилятор не понимает, к какой из двух копий базового класса нужно обращаться. Об
этой неоднозначности он и сообщает на этапе компиляции программы.
Для устранения проблемы необходимо сделать классы child1
и child2 наследниками виртуального базового класса (рис. 8.15).
150
Использование ключевого слова virtual в классах child1 и
child2 приводит к тому, что они наследуют единый общий подобъект базового класса parent. Теперь оба производных класса используют одну копию поля basedata и неоднозначность при обращении
к базовому классу устраняется.
Замечание. Необходимость использования виртуального базового класса показывает, что при множественном наследовании возможно возникновение ряда концептуальных проблем, поэтому
использовать такой подход при написании объектно-ориентированных программ нужно с осторожностью.
8.5. Дружественные функции
Принцип инкапсуляции и ограничения доступа к данным запрещает функциям, не являющимся методами класса, доступ
к скрытым (private) или защищенным (protected) данным объекта
класса. Однако в некоторых ситуациях такие жесткие ограничения
могут приводить к определенным неудобствам.
Предположим, существует необходимость, чтобы функция работала с объектами двух разных классов (например, она должна
получать объекты двух классов в качестве аргументов и обрабатывать их скрытые данные). Для решения таких задач в С++ исполь151
зуются дружественные функции (рис. 8.16), играющие роль своеобразных мостов между различными классами.
Упражнение 8.8. Дружественные функции
Рис. 8.16
В данной программе созданы два класса (first и second) с конструкторами без аргументов, а затем создана функция friendfunc(),
не входящая в состав методов классов. Чтобы эта функция имела
доступ к скрытым полям обоих классов, ее необходимо сделать
152
дружественной для них. Для этого внутри каждого класса нужно
объявить функцию friendfunc() как дружественную, используя
ключевое слово friend.
В программе объект каждого класса передается в функцию
friendfunc() как аргумент и функция имеет доступ к скрытым полям данных обоих классов.
Замечания
1. Объявление дружественной функции может быть расположено в любом месте класса – как в public-секции, так и в privateсекции.
2. К классу нельзя обращаться до того, как он объявлен в программе. Так, объявление функции friendfunc() в классе first ссылается на класс second, поэтому он должен быть объявлен раньше,
чем класс first. В связи с этим объявление класса second сделано
в начале программы, а его определение написано после определения класса first.
3. Следует отметить, что сама идея дружественных функций
весьма спорна. С одной стороны, они повышают гибкость языка,
с другой стороны, их наличие противоречит принципу инкапсуляции – основополагающему постулату объектно-ориентированного
программирования, утверждающему, что только функции-члены
класса могут иметь доступ к скрытым данным класса.
Насколько серьезно это противоречие? Дружественная функция объявляется таковой в том классе, к данным которого захочет
впоследствии получить доступ. Таким образом, не имея доступа
к исходному коду класса, невозможно сделать функцию дружественной. В этом смысле целостность данных сохраняется. Однако
наличие большого количества дружественных функций потенциально может привести к так называемому “spaghetti-code” (коду со
слишком большим числом передач управления), когда множество
дружественных функций фактически стирает границы между классами. Поэтому дружественные функции должны быть насколько
возможно разбросаны по программе и встречаться как можно реже.
В противном случае стоит подумать об изменении программы
в целом.
153
8.6. Дружественные функции и перегрузка операций
В некоторых ситуациях дружественные функции все-таки оказываются слишком удобными, чтобы от них отказываться. Типичным примером служит их использование для повышения многосторонности перегружаемых операций. Ограниченность применения
таких операций без использования дружественных функций показана в программе из упражнения 8.9 (рис. 8.17) на примере класса
Distance.
Упражнение 8.9. Ограниченность перегрузки операции +
для объектов класса Distance
Рис. 8.17 (окончание)
В данной программе при попытке сложить объект класса Distance с вещественной переменной типа float компилятор выдает
ошибку, поскольку в классе Distance перегруженная операция сложения позволяет складывать только два объекта класса Distance.
Эту проблему можно решить двумя способами:
1) перегрузить соответствующим образом операцию сложения, «научив» ее складывать данные типа Distance с данными типа
float, однако в этом случае сложение двух объектов типа Distance
станет невозможным;
2) добавить в состав методов класса Distance еще один конструктор, который будет преобразовывать данные типа float в данные типа Distance (рис. 8.18).
Упражнение 8.10. Ограниченность перегрузки операции +
для объектов класса Distance
Рис. 8.17 (начало)
154
Рис. 8.18 (начало)
155
Рис. 8.19
Второй вариант сложения не будет скомпилирован, потому
что слева от знака «+», в соответствии с синтаксисом функции
operator +, должен стоять объект класса Distance. Разрешить эту
проблему можно, создав объект класса Distance с помощью конструктора с одним аргументом, что и сделано в третьем варианте
сложения. Однако получаемое при этом выражение не является ни
очевидным, ни красивым.
Чтобы написать для рассматриваемого случая красивое выражение, которое содержало бы нужные типы данных как справа, так
и слева от оператора, можно использовать дружественные функции
(рис. 8.20).
Упражнение 8.11. Дружественная перегружаемая операция +
для класса Distance
Рис. 8.18 (окончание)
Новый конструктор с одним аргументом, переводящий значение типа float в значение типа Distance, позволяет складывать данные различных типов, но только в том случае, когда переменная
типа float стоит справа от знака «+». Результат работы программы
представлен на рис. 8.19.
Рис. 8.20 (начало)
156
157
Рис. 8.20 (окончание)
Рис. 8.21
Поскольку перегружаемая операция + стала дружественной функцией, все варианты взаимного расположения данных различных типов отрабатываются корректно.
Результат работы программы
представлен на рис. 8.21.
158
Будучи обычным методом, функция operator + оперировала
одним из объектов как объектом, членом которого являлась сама,
а вторым – как аргументом. Став дружественной функцией, она
начала рассматривать оба объекта как аргументы. Следствием этого
стала замена переменных meters и centimeters, использовавшихся
для прямого доступа к данным объекта, вызывавшего метод operator +, на переменные d1.meters и d1.centimeters, поскольку этот
объект теперь тоже стал аргументом.
Замечание. Чтобы сделать функцию дружественной, необходимо добавить ключевое слово friend только перед ее объявлением
внутри класса. В определении, а также при вызове функции это
ключевое слово не нужно.
В упражнении 8.12 (рис. 8.22) представлена программа, в которой дружественные функции используются для перегрузки операций вставки данных в поток (<<) и извлечения данных из потока
(>>) для объектов класса Distance. Перегрузка этих операций позволяет вместо функций show_dist() и get_dist() использовать для
вывода на экран и считывания с экрана значений объектов этого
класса обычную форму записи соответствующих операторов. Таким образом, в данном случае функции show_dist() и get_dist()
можно исключить из состава методов класса Distance.
Функции operator << и operator >> объявлены в программе
дружественными для класса Distance. Они получают два аргумента
по ссылке. Первый аргумент – это ссылка на поток, в который будут вставляться или из которого будут извлекаться данные (объект
потокового класса ostream или istream). Второй аргумент − это
ссылка на объект класса Distance, который должен выводиться на
экран или считываться с него; для функции operator << ссылка на
объект класса Distance должна быть константной, поскольку операция вставки данных в поток не должна изменять выводимый объект.
Возвращаемым значением для обеих операций должна являться ссылка на соответствующий поток (чтобы несколько операций
могли быть выполнены в одном выражении).
159
Упражнение 8.12. Дружественные перегружаемые операции
<< и >> для класса Distance
Результат работы программы представлен на рис. 8.23.
Рис. 8.23
8.7. Дружественные классы
Методы класса могут быть преобразованы в дружественные
функции одновременно с определением всего класса как дружественного (рис. 8.24).
Упражнение 8.13. Дружественные классы
Рис. 8.22
160
Рис. 8.24
161
В программе весь класс second был объявлен дружественным
для класса first. В результате все методы класса second получили
доступ к скрытым данным класса first.
Результат работы программы представлен на рис. 8.25.
Рис. 8.25
Замечание. Объявить класс дружественным можно и другими
способами:
• объявить обычный класс second: class second;
• определить класс first, внутри которого объявить дружественность класса second: friend second;
• определить класс second.
8.8. Статические функции
В главе 2 было дано представление о статических данных
класса, то есть о данных, являющихся общими для всех объектов
класса. Функции также могут быть статическими (рис. 8.26).
Упражнение 8.14. Статические функции
Рис. 8.26 (начало)
162
Рис. 8.26 (окончание)
В данной программе создается статический элемент данных
с именем total, принадлежащий классу race. В этой переменной
хранится информация о том, сколько объектов класса race создано
на текущий момент. Значение переменной total увеличивается
конструктором и уменьшается деструктором.
Замечания
1. При объявлении какого-либо элемента данных статическим
на весь класс выделяется единственная его копия. При этом
совершенно не важно, сколько объектов класса создано. В принципе, может вообще не существовать ни одного объекта, но с помощью
статических данных мы можем об этом знать.
Предположим, что нам необходимо получить доступ
к переменной total, находясь вне класса (например, из функции
main()). Это можно сделать с помощью функции show_total(),
объявленной статической, что позволяет вызвать ее из функции
main() явным образом, то есть просто указывая имя класса
и применяя оператор явного задания функции (рис. 8.27).
163
Рис. 8.27
Если бы функция show_total() была обычным методом класса
race, то для ее вызова из функции main() нужно было бы создать
объект класса, который и вызвал бы функцию через операцию
точки (рис. 8.28).
Рис. 8.28
Однако по правилам хорошего тона при написании объектноориентированных программ не следует обращаться к объекту, если
мы делаем что-то, относящееся к классу в целом. Поэтому в случае,
когда нужно иметь доступ к функции, используя только имя класса,
следует объявлять эту функцию статической (рис. 8.29).
Рис. 8.29
Результат работы программы представлен на рис. 8.30.
еще кое-что, а именно работа деструктора, который удаляет
созданные в программе объекты. Вставив в деструктор выражение,
выводящее на экран номер удаляемого объекта, можно увидеть, что
объекты удаляются в порядке, обратном созданию. Отсюда легко
сделать вывод, что локальные объекты хранятся в стеке и при
работе с ними используется LIFO-подход.
Задания для самостоятельной работы
1. Усовершенствуйте программу из задания 2 главы 5, сделав
базовый класс shape абстрактным. Для этого добавьте в состав его
методов чистую виртуальную функцию draw().
В функции main():
• создайте массив указателей на базовый класс shape;
• запишите в созданный массив адреса объектов разных производных классов;
• используя оператор цикла и одинаковый для всех объектов
вызов функции draw(), выведите изображения всех объектов на
экран.
Результаты работы обеих программ должны совпадать.
2. Используя концепцию дружественных функций, перегрузите операцию вставки данных в поток << для класса time из задания 2
главы 4.
В функции main() создайте два объекта класса time, затем
сложите их с помощью перегруженной операции + и выведите результаты на экран с помощью перегруженной операции <<.
Удалите функцию show_time() из состава методов класса
time.
Рис. 8.30
2. В программе функция show_id() является обычным методом
класса race, поэтому ее вызов может осуществляться только через
какой-либо объект класса.
3. Программа демонстрирует интересное свойство деструкторов. Результаты работы программы показывают, что после
вывода строки об окончании ее работы на самом деле происходит
164
165
Глава 9. ШАБЛОНЫ И ИСКЛЮЧЕНИЯ
Шаблоны позволяют использовать одни и те же функции или
классы для обработки данных разных типов. С помощью исключений можно удобным и универсальным способом обрабатывать
ошибки, возникающие при работе программы.
9.1. Шаблоны функций
9.1.1. Шаблоны функций с одним аргументом
Рассмотрим идею создания шаблонов функций на примере
вычисления модуля для чисел разных типов. Очевидно, что сама
функция вычисления модуля будет одинаковой как для чисел типа
int, так и для чисел типов float, double и т. д. (рис. 9.1).
нии программы, увеличивает количество кода и затрудняет его восприятие.
Замечание. Поскольку в языке С отсутствует понятие перегрузки функций, в нем нельзя даже использовать одинаковые имена
для обозначения функций разных типов. Поэтому для данного случая в программе на языке С пришлось бы создавать три функции с
разными именами, например abs(), labs() и fabs().
Применение шаблонов функций в языке C++ позволяет один
раз написать функцию, а затем использовать ее как для работы
с данными разных типов, так и для возвращения результатов разного типа.
В упражнении 9.1 (рис. 9.2) показан пример создания шаблона
простой функции, вычисляющей модуль числа для всех базовых
типов данных.
Упражнение 9.1. Шаблон функции вычисления модуля числа
Рис. 9.1
Из приведенного примера видно, что тело всех трех функций
одинаково. Тем не менее это три разные функции, поскольку они
отличаются по типам аргумента и возвращаемого значения. Несмотря на то, что в С++ все функции могут быть перегружены
и иметь одинаковые имена, для каждой функции все равно необходимо писать отдельное определение. Многократное переписывание
одинаковых по сути функций повышает трудозатраты при написа166
Рис. 9.2
167
Результат работы программы представлен на рис. 9.3.
Рис. 9.3
Можно видеть, что функция abs() действительно корректно
работает с любыми типами данных (тип данных определяется
функцией при передаче аргумента).
Замечания
1. Функция abs() из упражнения 9.1 может работать и с пользовательскими типами данных. Единственным необходимым условием для этого является соответствующая перегрузка операций
«меньше» (<) и унарного минуса (-).
Шаблоном функции называется в данной программе вся конструкция, начинающаяся с ключевого слова template в начале первой строки и включающая определение функции abs(). Ключевое
слово template сообщает компилятору, что определяется не функция, а ее шаблон. Переменная, следующая за словом class (в данном
случае переменная Module), называется аргументом шаблона. Аргумент шаблона заменяет любой конкретный тип данных (например, int) внутри определения шаблона.
2. Ключевое слово class можно заменить ключевым словом
type.
Поскольку на этапе компиляции программы неизвестно, с каким типом данных будет работать функция, компилятор не генерирует какой-либо код до тех пор, пока функция не будет вызвана
в ходе выполнения программы. При вызове функции с конкретным
типом данных, например типом int, происходит реализация шаблона функции, то есть генерация кода функции abs() для типа int путем подстановки везде вместо слова Module слова int. Каждый реализованный шаблон функции называется шаблонной функцией.
3. При использовании шаблона объем оперативной памяти,
занимаемой программой, не меняется (по сравнению с программой,
не использующей шаблон). Следовательно, для программы из
упражнения 9.1 также будут сгенерированы три функции. Исполь168
зование шаблона просто позволяет не писать трижды одно и то же
в исходном файле, благодаря чему уменьшается код программы.
Если необходимо будет изменить исходный код самой функции, то
при использовании шаблона это опять-таки нужно будет сделать
только один раз.
4. Чтобы принять решение о том, как именно компилировать
функцию, компилятору необходимо знать только тип ее аргумента,
тип возвращаемого ею значения роли не играет. Таким образом, работа компилятора с шаблоном напоминает работу с перегруженными функциями.
5. Концепция шаблонов логично вписывается в общую концепцию объектно-ориентированного программирования. Шаблон,
как и класс, не представляет собой ничего конкретного – это просто
база (модель, трафарет) для создания множества схожих функций.
9.1.2. Шаблоны функций с несколькими аргументами
Шаблон функции может иметь несколько аргументов, причем
не все они должны быть шаблонного типа.
В программе из упражнения 9.2 (рис. 9.4) используется шаблон функции, предназначенной для поиска в массиве заданного
числа. Шаблон имеет три аргумента: два шаблонных и один базового типа int. Шаблонному аргументу дано имя a_type. Оно
используется в аргументах шаблона функции два раза: как тип
указателя на массив и как тип искомого значения. Третий аргумент
шаблона (размер массива) всегда имеет стандартный тип int.
Упражнение 9.2. Шаблон функции с несколькими аргументами
Рис. 9.4 (начало)
169
Упражнение 9.3. Использование нескольких аргументов в одном
шаблоне
Рис. 9.4 (окончание)
Результат работы программы представлен на рис. 9.5.
Рис. 9.5
Как видно, если функция не находит заданного числа
в соответствующем массиве, то она возвращает число –1.
Замечание. При вызове шаблонной функции все экземпляры
соответствующего аргумента шаблона (в данном случае все
экземпляры a_type) должны быть строго одного типа. То есть
компилятор сможет сгенерировать код функции
find(int*, int, int);
но не сможет сгенерировать код функции
find(int*, float, int);
потому что первый и второй аргументы имеют разные типы.
9.1.3. Различные аргументы одного шаблона
В шаблоне функции можно использовать несколько шаблонных аргументов. Например, если для программы из упражнения 9.2
тип размера массива заранее неизвестен, то размер массива можно
сделать еще одним аргументом шаблона (рис. 9.6).
170
Рис. 9.6
В данной программе для размера массива (переменная size)
можно использовать как любой стандартный тип данных (int или
long), так и любой пользовательский тип.
Замечание. Следует помнить о том, что количество экземпляров шаблонной функции в памяти компьютера резко возрастает
с увеличением числа аргументов шаблона. Так, добавление еще одного аргумента к шаблонной функции из упражнения 9.2 в совокупности с шестью базовыми типами данных приведет к созданию
в памяти компьютера 36 копий функции. Соответственно, при
больших размерах функций это может привести к дополнительным
затратам памяти. С другой стороны, наличие шаблона не означает
обязательного вызова шаблонной функции.
9.2. Шаблоны классов
Принцип шаблона можно распространить и на классы. Как
правило, шаблоны используются при работе с различными структурами для хранения данных (стеками, очередями, связными списками). Все подобные структуры, рассмотренные нами выше, могли
хранить данные только одного типа. Так, например, класс Stack из
упражнения 3.4 главы 3 мог хранить только целочисленные значения типа int. Сокращенная версия класса приведена на рис. 9.7.
Для хранения в таком стеке значений типа float нужно было
бы написать абсолютно новое определение класса (рис. 9.8).
Для хранения данных других типов (как базовых, так и пользовательских) пришлось бы создавать подобные классы.
171
Упражнение 9.4. Реализация стека в виде шаблона
Рис. 9.7
Рис. 9.8
С помощью шаблона можно написать общую спецификацию
класса Stack для хранения данных любого типа.
9.2.1. Определение методов шаблона класса внутри класса
В программе из упражнения 9.4 (рис. 9.9) класс Stack является
шаблонным классом. Шаблонный аргумент Type используется
вместо фиксированного типа данных во всех местах спецификации
класса, где есть ссылка на тип массива st: в определении массива,
в типе аргумента функции push() и в типе данных, возвращаемых
функцией pop().
В функции main() с помощью одного шаблона создаются два
разных стека: стек f1 для хранения вещественных чисел и стек dist1
для хранения данных типа Distance (для работы с ними
в программе подключен заголовочный файл Distance.h,
содержащий класс Distance, создание которого рассматривалось
нами ранее). В каждый стек последовательно помещаются и извлекаются три значения соответствующего типа.
172
Рис. 9.9
173
Результат работы программы представлен на рис. 9.10.
к которому относится соответствующий метод. Для обычного
нешаблонного метода достаточно указать только имя Stack.
Рис. 9.10
Шаблоны классов отличаются от шаблонов функций способом
реализации:
• для создания шаблонной функции необходимо осуществить
ее вызов с аргументами нужного типа, например:
abs(ivar1);
• для создания шаблонного класса необходимо создать объект
класса, использующий шаблонный аргумент, например:
Stack<Distance> dist1;
При создании объекта шаблонного класса компилятор
выделяет память не только для данных объекта, но и для методов
соответствующего класса, и заносит созданный объект в память.
Имя типа объекта шаблонного класса состоит из имен класса
(Stack) и шаблонного аргумента, например: Stack<float>. Это
отличает данный объект от объектов других классов, которые
могут быть созданы из того же шаблона (например, Stack<int> или
Stack<time>), а также от обычных нешаблонных классов.
9.2.2. Определение методов шаблона класса вне класса
При реализации определения методов шаблона вне описания
класса используется другой синтаксис (рис. 9.11).
Упражнение 9.5. Определение методов шаблона вне класса
Из текста программы видно, что в данном случае выражение
template<class Type> должно присутствовать не только перед
определением класса, но и перед определением каждого метода.
Имя Stack<Type> используется для идентификации класса,
174
Рис. 9.11
Таким образом, имя шаблонного класса выглядит по-разному
в зависимости от типа данных.
В упражнении 9.6 (рис. 9.12) приведена программа, где класс
linklist из главы 3 переделан в шаблон. Такая модификация
позволяет использовать однажды написанный класс для создания
связных списков, хранящих данные разных типов.
Очевидно, что в данном случае для корректной работы
программы необходимо переделать в шаблон не только класс
175
linklist, но и структуру link, хранящую один элемент списка.
В программе из упражнения 9.6 и класс linklist, и структура link
используют шаблонный аргумент Type для подстановки данных
произвольного типа.
Упражнение 9.6. Использование шаблона для класса
«Связный список»
Рис. 9.12 (окончание)
Возможный
на рис. 9.13.
результат
работы
программы
представлен
Рис. 9.13
Рис. 9.12 (начало)
176
Замечания
1. Приведенные примеры свидетельствуют, что не только
функции или классы могут быть преобразованы в шаблоны.
Реализовать шаблон можно для любых элементов программ,
в которых используются изменяющиеся типы данных (например,
для структуры link из упражнения 9.6).
2. К использованию шаблонов при работе с пользовательскими типами данных надо подходить осторожно. В таких
случаях необходимо проверять возможность работы шаблонного
класса с объектами данного типа. Так, в программе из упражнения 9.6
шаблонный класс linklist смог работать с объектами класса
Distance только потому, что в классе Distance была перегружена
177
операция вставки данных в поток (<<), которая используется
в классе linklist для вывода хранящихся в списке объектов.
9.3. Исключения
Исключения – это ошибки, возникающие в процессе работы
программы. Использование концепции исключений при написании
объектно-ориентированных программ позволяет применить системный объектно-ориентированный подход к обработке ошибок,
возникающих в классах (ввод ошибочных данных, переполнение
массива, деление на ноль, недопустимые параметры функций и др.).
При стандартном подходе к обработке ошибок, как правило,
используются конструкции типа if…else. Многие функции в случае
возникновения какой-либо ошибки возвращают некоторое значение, часто называемое флагом ошибки. Для проверки таких возвращаемых значений тоже необходимо использовать конструкции
типа if…else. В результате этих многочисленных проверок код программы существенно разрастается и может стать трудночитаемым.
Проблема диагностирования ошибок, возникающих при использовании классов, усугубляется еще и тем, что ошибки могут
появляться и при отсутствии явных вызовов функции. Например,
если ошибка возникнет в конструкторе класса при создании объекта, то никакого возвращаемого значения вызывающая программа не
получит, так как конструктор не возвращает значений.
Еще сложнее отслеживать ошибки при работе с библиотеками
классов, поскольку их реализация неизвестна.
Обработка исключений осуществляется для получения возможности перехватить и обработать ошибку прежде, чем она произойдет и наступят неприятные последствия. Обработка исключений используется в тех случаях, когда система может быть восстановлена для работы после возникновения ошибки.
9.3.1. Основы обработки исключений в С++
Возникновение ошибки при использовании исключений называется генерацией исключительной ситуации. Возможности С++ по
обработке таких ситуаций позволяют программисту вынести опера178
торы обработки ошибок из «основной линии» выполнения программы.
Для обработки исключительной ситуации в программе создается отдельная секция, содержащая операции по обработке ошибок.
Этот программный код называется обработчиком исключений, или
улавливающим блоком (catch-блоком).
Любой код программы, использующий объекты класса, помещается в блок повторных попыток (try-блок). Ошибки, возникающие в try-блоке, будут пойманы catch-блоком и обработаны.
Замечание. В блок повторных попыток не нужно включать
части программы, не имеющие отношения к классу, так как с помощью исключений отлавливаются только ошибки, генерируемые
методами класса.
Схема работы механизма исключений в общем виде представлена на рис. 9.14.
Рис. 9.14
При работе с исключениями в С++ используются три новых
слова: catch, throw (англ. «через») и try. Необходимо также создание новой конструкции – класса exception (класса исключений).
Общая схема программы с использованием исключений представлена в упражнении 9.7 (рис. 9.15).
179
Упражнение 9.7. Демонстрация общей схемы работы исключений
Рис. 9.15
В данной программе создается класс Example, в котором
могут возникнуть ошибки. В общедоступной части класса
создается класс исключений (класс Error). В методе func() класса
Example реализуется проверка на возникновение ошибок. Если
таковые обнаруживаются, то с помощью ключевого слова throw
и конструктора класса исключений происходит генерация исключительной ситуации. Точка, в которой происходит генерация
исключения (throw), называется точкой генерации.
В функции main() часть программы, взаимодействующая
с классом Example, помещается в блок повторных попыток (tryблок). Если в методе func() возникает ошибка, то генерируется
исключение, после чего управление программой переходит к улавливающему блоку (блоку ловушек, catch-блоку), который следует
сразу же за блоком повторных попыток. Если никакие исключения
180
в try-блоке не генерируются, то обработчик исключений для этого
блока пропускается и программа продолжает выполняться, начиная
с операторов, следующих за catch-блоком.
Замечания
1. Количество catch-блоков может быть произвольным.
Каждый catch-блок определяет тип исключений, которые может
перехватывать и обрабатывать. В catch-блоке содержится программа по обработке исключения – обработчик исключения. При
генерации исключения происходит выход из try-блока и последовательный поиск в catch-блоках соответствующего обработчика.
Если точка генерации расположена в глубоко вложенной
области внутри try-блока или в глубоко вложенных вызовах
функции, управление все равно будет передано найденному
обработчику.
2. После генерации исключения управление программой не
всегда возвращается в точку генерации. Как правило, обработчик
просто завершает работу программы.
3. Количество try-блоков также может быть произвольным.
В упражнении 9.8 (рис. 9.16) приведен пример применения
механизма исключений для программы из упражнения 3.4 (глава 3),
демонстрирующей работу стека. В программе реализована
обработка исключительных ситуаций, связанных с попытками
добавить данные в заполненный стек и извлечь данные из пустого
стека.
Упражнение 9.8. Демонстрация механизма работы исключений
на примере стека
Рис. 9.16 (начало)
181
Рис. 9.16 (окончание)
Результат работы программы представлен на рис. 9.17.
Рис. 9.17
Как видно, при попытке поместить в стек четвертый элемент
было сгенерировано исключение, которое обработал обработчик
182
(в данном случае он только выводит на экран сообщение об ошибке), после чего работа программы завершилась.
При работе с исключениями основные роли играют следующие части программы:
1) класс исключений (класс Exept);
2) генератор исключения (оператор throw Exept());
3) блок повторных попыток (try-блок);
4) улавливающий блок, или блок ловушек (catch-блок).
Класс исключений описывается внутри класса (в данной программе – внутри класса Stack). Тело класса исключений пусто (но
это не обязательно), следовательно объекты этого класса не обладают ни данными, ни методами. Имя класса исключений используется для связывания выражения генерации исключения с улавливающим блоком и должно обязательно включать наименование того класса, в котором он находится (в данном случае Stack::Exept).
Генерация исключения реализуется с помощью оператора
throw Exept(). Второе слово оператора неявно запускает конструктор класса Exept для создания объекта этого класса, а первое слово
передает управление соответствующему обработчику ошибок.
В блок повторных попыток включается только та часть программы, которая работает с методами класса Stack (она предваряется словом try и заключается в фигурные скобки).
Улавливающий блок – это код, содержащий операции по обработке ошибок. Этот блок всегда следует сразу за try-блоком, начинается со слова catch и заключается в фигурные скобки.
Таким образом, при использовании механизма исключений
работа программы осуществляется по следующему алгоритму:
1) код нормально выполняется вне try-блока;
2) управление переходит в try-блок;
3) выполнение какого-либо оператора в try-блоке приводит
к возникновению ошибки в методе класса;
4) метод генерирует исключение;
5) управление переходит к одному из catch-блоков.
Замечание. Ошибка может произойти в любом операторе
try-блока, но беспокоиться о получении возвращаемого значения
для каждого из них теперь не нужно, так как это делается автоматически.
183
9.3.2. Многократные исключения
В программе из упражнения 9.8 генерация исключения возможна в двух случаях: при попытке добавить данные в заполненный стек либо извлечь данные из пустого стека. В обоих случаях
обработчик будет выводить на экран одно и то же сообщение:
«Ошибка: стек переполнен или пуст».
В программе из упражнения 9.9 (рис. 9.18) каждая исключительная ситуация обрабатывается своим обработчиком.
Упражнение 9.9. Демонстрация обработчиков для двух исключений
на примере стека
Рис. 9.18 (окончание)
Результат работы программы представлен на рис. 9.19.
Рис. 9.19
Рис. 9.18 (начало)
184
В данной программе в классе Stack описаны два класса
исключений (class Full и class Empty) и для каждого исключения
используется свой блок ловушек. В результате для каждого исключения выводится свое сообщение об ошибке.
Замечания
1. Работа группы отлавливающих блоков (цепи ловушек)
напоминает работу оператора switch (в том смысле, что исполняет185
ся только один соответствующий блок кода). После обработки исключения управление программой передается тому месту программы, которое следует за всеми блоками-ловушками. Однако, в отличие от оператора switch, в данном случае не нужно заканчивать
каждый catch-блок оператором break (в каком-то смысле
catch-блоки напоминают функции).
2. Блок повторных попыток получил такое название потому,
что с него может начинаться повторный запуск программы после
сбоя. Однако после выполнения обработчика исключения работа
программы не всегда возвращается в точку генерации (то есть на
начало блока повторных попыток).
9.3.3. Исключения с аргументами
Если одно и то же исключение генерируется разными методами класса, то для определения источника возникновения ошибки
можно использовать исключения с аргументами. Для этого нужно
добавить необходимые данные в класс исключений и инициализировать их при создании объекта класса исключений с помощью
конструктора. После генерации исключения обработчик может извлечь эти данные из созданного объекта, тем самым идентифицировав проблему.
В следующей программе (рис. 9.20) механизм исключений
с аргументами применен для обработки ошибочной инициализации
объектов класса Distance, а именно при неправильном задании количества сантиметров (свыше 100).
Упражнение 9.10. Исключения с аргументами
Рис. 9.20
Рис. 9.20 (начало)
186
187
Возможные результаты работы программы представлены
на рис. 9.21 и 9.22.
Рис. 9.21
Рис. 9.22
Отличительная особенность программы – наличие данных
и конструктора с двумя аргументами в классе исключений (класс
Exept). Данные класса (переменные mis_name и mis_value) сделаны общедоступными для обеспечения прямого доступа к ним из
обработчика ошибок. В переменных mis_name и mis_value хранятся сведения об имени вызываемого метода и об ошибочных значениях сантиметров.
При генерации исключения происходят создание объекта
класса Exept и инициализация его полей данных с помощью конструктора с двумя параметрами. Созданный объект передается
в catch-блок, в котором, благодаря общедоступности данных объектов класса Exept, реализуется вывод этих данных на экран.
В результате при генерации исключения обработчик выведет
на экран строку, содержащую сообщение о том, в каком методе
класса Distance произошла ошибка, а также ошибочное значение,
заданное программистом при инициализации полей объекта или
введенное пользователем с клавиатуры. Такой способ обработки
исключений позволяет программисту или пользователю точнее
определять причину сбоя в программе.
Замечания
1. Выражение, приводящее к исключительной ситуации, не
обязательно находится в блоке повторных попыток. Оно может
располагаться и в функции, вызываемой каким-либо оператором
этого блока, или в функции, вызванной функцией, вызванной
функцией из блока повторных попыток и т. д. (см., например,
188
функцию operator >> из упражнения 9.10). Поэтому блок повторных попыток следует всегда делать верхним уровнем программы.
2. Механизм исключений нецелесообразно применять ко всем
типам ошибок. Так, ошибки ввода данных (ввод символов вместо
цифр и т. д.) проще ликвидировать традиционными способами:
проверять в цикле, что вводит пользователь, и при необходимости
просить повторить ввод.
3. Если после генерации исключения нужно исправить ошибку
или повторить ввод данных, то можно замкнуть try-блоки и catchблоки в цикл таким образом, чтобы после работы обработчика исключений управление в программе возвращалось в начало
try-блока.
4. Если обработчика для сгенерированного исключения не
нашлось, операционная система закрывает программу.
5. Механизм исключений решает важную проблему выявления
ошибок в библиотечных классах. В библиотечных функциях легко
могут возникнуть ошибки, но где конкретно они появились и что
с ними делать, как правило, непонятно, так как алгоритмы библиотек написаны другими программистами. Поэтому при написании
программ, использующих сторонние библиотеки, необходимо
предусматривать блок повторных попыток и блок-ловушку для любых исключений, которые может выдавать библиотека.
Задания для самостоятельной работы
1. Напишите шаблон класса Queue для работы с очередью.
Расположите класс в отдельном заголовочном файле. Добавьте
в программу еще один заголовочный файл с классом Distance с перегруженными операциями вставки (<<) и извлечения (>>) данных.
В функции main() создайте несколько очередей для данных
разных типов (например, для char-строк, float и Distance) и поработайте с ними.
Возможный результат работы программы представлен
на рис. 9.23.
189
Рекомендуемая литература
1. Страуструп Б. Программирование : Принципы и практика использования С++ / Б. Страуструп. – М. : ИД «Вильямс», 2011. – 1248 с.
2. Прата С. Язык программирования С++ : Лекции и упражнения /
С. Прата. – М. : ИД «Вильямс», 2012. – 1248 с.
3. Объектно-ориентированный анализ и проектирование с примерами
приложений / Г. Буч [и др.]. – М. : ИД «Вильямс», 2010. – 720 с.
Рис. 9.23
2. Добавьте в предыдущую программу механизм исключений.
Рассмотрите два исключения:
• при попытке добавить данные в заполненную очередь;
• при попытке извлечь данные из пустой очереди.
Программа должна генерировать исключение, если счетчик
элементов очереди превысит максимальный размер массива или
примет отрицательное значение.
В функции main() протестируйте работу программы на нескольких очередях.
Возможный результат работы программы представлен
на рис. 9.24.
Рис. 9.24
190
191
Оглавление
Введение …………………………………………………………………..
Глава 1. КЛАССЫ И ОБЪЕКТЫ. ЧАСТЬ I ………………………...
1.1. Основные понятия …………………………………………….
1.2. Определение класса. Управление доступом ………………...
1.3. Данные класса …………………………………………………
1.4. Методы класса ………………………………………………...
1.5. Методы класса внутри определения класса …………………
1.6. Определение объектов ………………………………………..
1.7. Вызов методов класса ………………………………………...
1.8. Объекты программы и объекты реального мира ……………
1.9. Класс как тип данных …………………………………………
1.10. Конструкторы ………………………………………………...
1.10.1. Имя конструктора, виды конструкторов, списки
инициализации ………………………………………………
1.10.2. Перегрузка конструкторов ………………………….
1.10.3. Конструктор копирования по умолчанию ………...
1.11. Деструкторы ………………………………………………….
Задания для самостоятельной работы …………………………………...
Глава 2. КЛАССЫ И ОБЪЕКТЫ. ЧАСТЬ II ………………………..
2.1. Определение методов класса вне класса …………………….
2.2. Объекты в качестве аргументов функций …………………...
2.3. Объекты, возвращаемые функцией ………………………….
2.4. Перегрузка методов класса …………………………………...
2.5. Структуры и классы …………………………………………..
2.6. Классы, объекты, данные и память …………………………..
2.7. Статические данные класса …………………………………..
2.7.1. Статические поля класса: определение и пример
использования ……………………………………………….
2.7.2. Раздельное объявление и определение статических
полей класса …………………………………………………
2.7.3. Константные методы и константные аргументы
методов ……………………………………………………....
2.7.4. Константные объекты ………………………………..
2.8. Массивы объектов …………………………………………….
Задания для самостоятельной работы …………………………………..
Глава 3. УКАЗАТЕЛИ НА ОБЪЕКТЫ. СОЗДАНИЕ
СТРУКТУР ДЛЯ ХРАНЕНИЯ ДАННЫХ
С ИСПОЛЬЗОВАНИЕМ КЛАССОВ …………………………………
3.1. Массив указателей на объекты. Сортировка указателей …...
3.2. Стек …………………………………………………………….
3.3. Очередь ………………………………………………………...
3.4. Связный список ……………………………………………….
Задания для самостоятельной работы …………………………………..
192
3
4
4
7
8
8
9
10
10
12
13
15
15
19
20
22
24
26
26
28
29
31
32
33
35
35
37
38
40
41
42
45
47
52
54
55
58
Глава 4. ПЕРЕГРУЗКА ОПЕРАЦИЙ ………………………………...
4.1. Перегрузка унарных операций ……………………………….
4.1.1. Перегрузка префиксных операций ………………….
4.1.2. Аргументы операций …………………………………
4.1.3. Значения, возвращаемые операцией ………………...
4.1.4. Временные безымянные объекты …………………...
4.1.5. Постфиксные операции ………………………………
4.2. Перегрузка бинарных операций ……………………………...
4.2.1. Арифметические операции сложения ………………
4.2.2. Операции сравнения ………………………………….
4.2.3. Операции арифметического присваивания …………
4.3. Особенности использования перегрузки операций …………
4.4. Преобразование типов данных ……………………………….
Задания для самостоятельной работы …………………………………...
Глава 5. НАСЛЕДОВАНИЕ ……………………………………………
5.1. Базовый и производный классы ……………………………...
5.2. Доступ к базовому классу …………………………………….
5.2.1. Подстановка конструктора базового класса ………..
5.2.2. Подстановка методов базового класса ……………...
5.2.3. Спецификатор доступа protected …………………….
5.2.4. Неизменность базового класса ………………………
5.3. Конструкторы производного класса …………………………
5.4. Перегрузка функций …………………………………………..
5.5. Иерархия классов ……………………………………………...
5.6. Абстрактный базовый класс ………………………………….
Задания для самостоятельной работы …………………………………...
Глава 6. ОБЩЕЕ, ЧАСТНОЕ И МНОЖЕСТВЕННОЕ
НАСЛЕДОВАНИЕ ………………………………………………………
6.1. Общее и частное наследование ………………………………
6.2. Уровни наследования …………………………………………
6.3. Множественное наследование ………………………………..
Задания для самостоятельной работы …………………………………...
Глава 7. ПОТОКОВЫЕ КЛАССЫ. ИСПОЛЬЗОВАНИЕ
ФАЙЛОВ ДЛЯ ВВОДА И ВЫВОДА ДАННЫХ …………………….
7.1. Потоковые классы …………………………………………….
7.2. Работа с текстовыми файлами ………………………………..
7.2.1. Запись данных в файл ………………………………..
7.2.2. Чтение данных из файла ……………………………..
7.2.3. Чтение и запись строк с пробелами …………………
7.2.4. Флаги статуса ошибок при работе с файлами ……...
7.2.5. Ввод/вывод символов ………………………………...
7.2.6. Настройки открытия/закрытия файлов
для ввода/вывода данных. Режимы доступа к файлам …...
7.3. Файловый ввод/вывод двоичных файлов…………………….
193
61
61
61
63
64
65
67
69
69
71
73
75
76
76
78
79
82
82
83
83
85
86
88
90
94
95
97
97
99
103
108
110
110
113
113
116
118
120
122
126
130
7.3.1. Двоичный ввод/вывод данных стандартных
типов …………………………………………………………
7.3.2. Двоичный ввод/вывод данных пользовательского
типа …………………………………………………………..
7.4. Указатели файлов. Чтение и запись данных в произвольное
место файла ………………………………………………………...
Задания для самостоятельной работы …………………………………..
Глава 8. ВИРТУАЛЬНЫЕ ФУНКЦИИ. АБСТРАКТНЫЕ
КЛАССЫ. ДРУЖЕСТВЕННЫЕ ФУНКЦИИ ……………………….
8.1. Виртуальные функции ………………………………………..
8.2. Абстрактные классы и чистые виртуальные функции ……..
8.3. Виртуальные деструкторы ……………………………………
8.4. Виртуальные базовые классы ………………………………...
8.5. Дружественные функции ……………………………………..
8.6. Дружественные функции и перегрузка операций …………..
8.7. Дружественные классы ……………………………………….
8.8. Статические функции …………………………………………
Задания для самостоятельной работы …………………………………..
Глава 9. ШАБЛОНЫ И ИСКЛЮЧЕНИЯ …………………………...
9.1. Шаблоны функций …………………………………………….
9.1.1. Шаблоны функций с одним аргументом ……………
9.1.2. Шаблоны функций с несколькими аргументами ......
9.1.3. Различные аргументы одного шаблона ……………..
9.2. Шаблоны классов ……………………………………………...
9.2.1. Определение методов шаблона класса внутри
класса ………………………………………………………...
9.2.2. Определение методов шаблона класса вне класса …
9.3. Исключения ……………………………………………………
9.3.1. Основы обработки исключений в С++ ……………...
9.3.2. Многократные исключения ………………………….
9.3.3. Исключения с аргументами ………………………….
Задания для самостоятельной работы …………………………………...
Рекомендуемая литература ………………………………………………
130
132
134
137
139
139
143
147
149
151
154
161
162
165
166
166
166
169
170
171
172
174
178
178
184
186
189
191
Учебное издание
Букунов Сергей Витальевич,
Букунова Ольга Викторовна
ОСНОВЫ ОБЪЕКТНО-ОРИЕНТИРОВАННОГО
ПРОГРАММИРОВАНИЯ
Учебное пособие
Редактор Т. В. Ананченко
Корректор М. А. Молчанова
Компьютерная верстка И. А. Яблоковой
Подписано к печати 17.05.2017. Формат 60×84 1/16. Бум. офсетная.
Усл. печ. л. 11,4. Тираж 100 экз. Заказ 43. «С» 24.
Санкт-Петербургский государственный архитектурно-строительный университет.
190005, Санкт-Петербург, 2-я Красноармейская ул., д. 4.
Отпечатано на ризографе. 190005, Санкт-Петербург, ул. Егорова, д. 5/8, лит. А.
194
195
ДЛЯ ЗАПИСЕЙ
196
Документ
Категория
Без категории
Просмотров
9
Размер файла
6 114 Кб
Теги
bukunov, osnovy, obektno, orient
1/--страниц
Пожаловаться на содержимое документа