Текст
                    Министерство образования и науки Российской Федерации
Федеральное государственное бюджетное образовательное учреждение
высшего профессионального образования
Московский государственный университет печати имени Ивана Федорова

В.Н. Шурыгин

Объектно-ориентированное
программирование
Конспект лекций
для студентов, обучающихся по направлению
230400 — Информационные системы и технологии

Москва
2014


УДК 004.424 ББК 22.18 Ш 95 Ш 95 В.Н. Шурыгин Объектно-ориентированное программирование : Конспект лекций / В.Н. Шурыгин ; Моск. гос. ун-т печати имени Ивана Федорова. — М. : МГУП имени Ивана Федорова, 2014. — 164 с. Дисциплина «Объектно-ориентированное программирование» является продолжением, развитием и углублением курса «Информатика» для направления 230400 «Информационные системы и технологии». Конспект лекций наряду с темами структурного программирования охватывает ряд специфических вопросов, характерных для объектноориентированного программирования: объекты и классы, наследование, перегрузка операций и преобразование типов, виртуальные и дружественные функции, шаблоны. Изучение проводится на основе языка C++. УДК 004.424 ББК 22.18 © Шурыгин В.Н., 2014 © Московский государственный университет печати имени Ивана Федорова, 2014 2
Содержание Введение ...........................................................................................................4 Лекция 1. Основы программирования на С++ ..............................................6 Лекция 2. Циклы и ветвления .......................................................................16 Лекция 3. Структуры и перечисления..........................................................24 Лекция 4. Функции ........................................................................................30 Лекция 5. Объекты и классы.........................................................................39 Лекция 6. Массивы и строки.........................................................................48 Лекция 7. Перегрузка операций и преобразование типов..........................57 Лекция 8. Наследование ................................................................................74 Лекция 9. Указатели ......................................................................................94 Лекция 10. Виртуальные и дружественные функции ...............................107 Лекция 11. Потоки и файлы ........................................................................122 Лекция 12. Шаблоны и исключения...........................................................139 Лекция 13. Стандартная библиотека шаблонов .......................................152 Библиографический список ........................................................................163 3
ВВЕДЕНИЕ Дисциплина «Объектно-ориентированное программирование» (ООП) является продолжением, развитием и углублением курса «Информатика» для направления 230400 «Информационные системы и технологии». Конспект лекций наряду с темами структурного программирования охватывает ряд специфических вопросов, характерных для ООП: объекты и классы, наследование, перегрузка операций и преобразование типов, виртуальные и дружественные функции, шаблоны. Изучение проводится на основе языка C++. Основными целями изучения дисциплины являются изучение ООП на примере языка C++, получение практических навыков разработки программ в средах Xcode, Microsoft Visual C++, Dev-C++ Основными задачами изучения дисциплины являются: изучение концепций ООП; изучение объектно-ориентированного языка программирования C++; обучение разработке программ в среде Xcode, обучение разработке программ в среде Dev-C++, обучение разработке программ в среде Microsoft Visual C++. В результате освоения дисциплины студент должен: • знать концепцию ООП, правила составления программ на языке, программирования C++, основные возможности сред программирования Xcode, DEV-C++, Microsoft Visual; • уметь составлять программы на языке программирования C++, использовать среду программирования Xcode, DEV-C++, Microsoft Visual C++ для разработки и отладки программ на языке C++; • иметь навыки проектирования классов, их программной реализации и разработки терминальных приложений. В результате освоения дисциплины студент осваивает следующие компетенции: 4
ОК-6. владение широкой общей подготовкой (базовыми знаниями) для решения практических задач в области информационных систем и технологий; ПК-5. способность проводить моделирование процессов и систем; ПК-12. способность разрабатывать средства реализации информационных технологий (методические, информационные, математические, алгоритмические, технические и программные); ПК-19. способность осуществлять организацию рабочих мест, их техническое оснащение, размещение компьютерного оборудования; ПК-27. способность оформлять полученные рабочие результаты в виде презентаций, научно-технических отчетов, статей и докладов на научно-технических конференциях. Благодарности Выражаю благодарность всем студентам направления 230400 «Информационные системы и технологии», слушавшим и конспектировавшим мои лекции и тем самым вносившим свой вклад в подготовку этого конспекта. Особая благодарность студентке группы ДЦИС-2-1 Костенко Александре, Попадич Ольге, Сергееву Илье за помощь в приведение материалов студенческих конспектов к виду, приемлемому для печати. 5
Лекция 1. Основы программирования на С++ Структура программы Программа на языке С++ состоит из директив препроцессора, указаний компилятору, объявлений переменных и/или констант, объявлений и определений функций. Любая программа на С++ состоит из одной или нескольких функций. Обязательно должна быть определена единственная главная функция main(), именно с нее всегда начинается выполнение программы, остальные функции равноправны. Структура программы С++ имеет следующий вид (листинг 1.1). Листинг 1.1 Директивы Объявление глобальных переменных Объявление или определение функций int main(список параметров) { последовательность операторов } тип_возвращаемого_значения имя_функции(список парамет‐ ров) { последовательность операторов } В языке С++ различаются верхний и нижний регистры символов. Использование ключевых слов в качестве переменной или имени функции не допускается. Ключевое слово — это зарезервированное слово, имеющее особое значение. Примерами ключевых слов могут служить int, class, if, while. 6
Объявление функции задаёт имя функции, тип возвращаемого значения и количество и перечень типов, которые должны присутствовать при вызове функции. Указание void в качестве возвращаемого значения означает, что функция не возвращает значения. Определением функции является объявление функции, в котором присутствует тело функции, заключенное в фигурные скобки. Операторы управляют процессом выполнения программы. Они содержат все управляющие конструкции структурного программирования. Составной оператор ограничивается фигурными скобками. Все другие операторы заканчиваются точкой с запятой. Основные типы данных представлены в табл. 1.1. Кроме них существуют: структуры, объединения, объекты классов, перечислимый тип (enum) и пустой тип (void). Тип void используется для объявления функций, не возвращающих никакого значения, а также для объявления указателей на значение типа void. Такие указатели могут быть преобразованы к указателям на любой другой тип. В языке C++ нет специальных типов для массивов и строк. Таблица 1.1 Имя типа 1 Размер Значения bool 2 1 байт 3 логические (signed) char 1 байт символы, целые числа wchar_t 2 байта символы Unicode Диапазон значений 4 false, true от –128 до 127 от 0 до 65535 (signed) short int 2 байта целые числа от -32768 до 32767 (signed) int зависит от реализации 4 байта целые числа (signed) long int 4 байта целые числа от-2147483648 до 2147483647 (signed) long long int 8 байт (signed) __int64 (MS) целые числа от–9,223,372,036,854,775,808 до 9,223,372,036,854,775,807 unsigned char 1 байт символы целые числа от 0 до 255 unsigned short int 2 байта целые числа 0 до 65535 7
Окончание табл. 1.1 1 unsigned int 2 3 4 байта целые числа 4 зависит от реализации unsigned long int 4 байта целые числа от 0 до 4294967295 (unsigned) long long int 8 байт целые числа от 0 до 18,446,744,073,709,551,615 float 4 байта вещественные числа от 1.175494351e–38 до 3.402823466e+38 double, long double 8 байт вещественные числа от 2.2250738585072014e–308 до 1.7976931348623158e+308 Препроцессор обрабатывает текст программы до компилятора. Работа препроцессора управляется директивами. С помощью препроцессора можно выполнять следующие операции: • включение в программу текстов из указанных файлов; • определение пространства имен; • макроподстановка; • исключение из программы отдельных частей текста (условная компиляция). Пример использования директив представлен в листинге 1.4. Включение файлов. Директива #include включает содержимое файла, путь к которому задан в компилируемый файл вместо строки с директивой (листинг 1.2). Она имеет следующий синтаксис: Листинг 1.2 #include "спецификация‐пути" #include <спецификация‐пути> «Спецификация пути» это имя файла, которому может предшествовать абсолютный или относительный путь к нему. Включаемые файлы используются для хранения объявлений внешних переменных и абстрактных типов данных, разделяемых несколькими исходными файлами. Функции ввода/вывода и дина8
мического распределения памяти не являются элементом языка. Они входят в стандартные библиотеки. Для использования функций стандартных библиотек необходимо включать заголовочные файлы библиотек. Директива #define осуществляет макроподстановку, т. е. заменяет все вхождения идентификатора в исходном файле на текст, следующий в директиве за идентификатором. Замены в тексте можно отменить директивой #undef. Синтаксис директив #define и #undef представлен в листинге 1.3: Листинг 1.3 #define <идентификатор> <текст> #define <идентификатор>(<список параметров>) <текст> #undef <идентификатор> Пространства имен помогают избегать конфликтов имен (функций, переменных и т. д.). Например, если две разные библиотеки функций содержат функции с одинаковыми названиями, следует объявить уникальное пространство имен для функций и классов отдельной библиотеки. Тогда можно либо при вызове определенных функций использовать префикс пространства имен, либо объявить, что все функции по умолчанию будут вызываться из определенного пространства имен. Директива using namespace std; означает, что все определенные ниже имена в программе будут относиться к пространству имен с именем std. Листинг 1.4 #include <iostream> //подключение библиотеки iostream #define god 12 //с этого места препроцессор будет заменять "god" на "12" using namespace std; // использовать пространство имен std int main () { 9
int a[god]; for (i = 0; i < god; i++) { a[i] = i; cout << i << endl; } #undef god //c этой точки препроцессор больше не будет заменять "god" на "12" int god = 13; cout << god << endl; return 0; } Для хранения различных данных в языках программирования используют переменные. Переменная — это область памяти, имеющая имя (идентификатор). Объявление переменной задаёт имя и атрибуты переменной. Атрибутами могут быть тип, количество элементов (для массивов), спецификация класса памяти, а также инициализатор. Инициализатор — это константа соответствующего типа, задающая значение, которое присваивается переменной при создании. Синтаксис объявления переменной имеет следующий вид: Листинг 1.5 [<спецификация класса памяти>] <тип> <имя> [= <инициализатор>]; Константы — переменные, значение которых нельзя изменить. Они используются только для чтения. Чтобы объявить объект константой, в объявление нужно добавить ключевое слово const, константа должна быть инициализирована при объявлении. Типичным является использование констант в качестве размера массивов и меток в инструкции case. Пример работы с переменными и константами представлен в листинге 1.6. Листинг 1.6 #include <iostream> 10
using namespace std; int main() { float rad; // переменная вещественного типа const float PI = 3.14159F; // вещественная константа cout << "Введите радиус окружности: "; cin >> rad; float dlina = 2 * PI * rad; cout << "Длина круга равна " << dlina << endl; return 0; } В языке С++ предусмотрено автоматическое и явное преобразование типов. Автоматическое приведение типов имеет следующую иерархию. Если тип одного операнда long double, другой преобразуется в long double: иначе, если тип одного операнда double, другой операнд преобразуется в double; иначе, тип одного операнда float, другой преобразуется в float; иначе преобразуются оба операнда: char, signed char, unsigned char, short int и unsigned short int преобразуются в int, если int может представить все значения исходных типов, иначе они преобразуются в unsigned int; bool преобразуется в int. Затем если тип одного операнда unsigned long, другой операнд преобразуется к типу unsigned long: иначе, если тип одного операнда long int, а другого unsigned int, то если long int может представить все значения типа unsigned int, unsigned int преобразуется в long int, иначе оба операнда преобразуются в unsigned long int; иначе, если тип одного операнда long int, другой операнд преобразуется в long int; иначе, если тип одного операнда unsigned int другой операнд преобразуется в unsigned int; иначе оба операнда имеют тип int. Пример использования автоматического преобразования типов представлен в листинге 1.7. 11
Листинг 1.7 #include <iostream> using namespace std; int main() { int sajen = 7; float koeff = 2.1336F; double rasst = sajen * koeff; // автоматическое приве‐ дение к типу double cout << "Расстояние " << sajen << " равно " << rasst << " м" << endl; return 0; } Для явного преобразования типов используются операции, представленные в табл. 1.2. Таблица 1.2 Знак операции static_cast dynamic_cast reinterpret_cast const_cast Наименование Преобразование с проверкой во время компиляции Преобразование с проверкой во время выполнения Преобразование без проверки Константное преобразование Операция static_cast осуществляет преобразование родственных типов, например, указателя на один тип к указателю на другой тип из той же иерархии классов, целый тип в перечисление или тип с плавающей точкой в интегральный. Операция reinterpret_cast управляет преобразованиями между несвязанными типами, например, целых в указатели или указателей в другие (несвязанные) указатели. Преобразование dynamic_cast выполняется и проверяется на этапе выполнения (листинг 1.8). Преобразование const_cast аннулирует действие модификатора const. Операции приведения типов имеют следующий синтаксис: Листинг 1.8 операция<новый тип>(выражение) 12
Пример использования явного преобразования типов представлен в листинге 1.9. Листинг 1.9 #include <iostream> using namespace std; int main() { int intVar = 1500000000; intVar = (static_cast<double>(intVar)*10)/10; //явное приведение к double cout << "Значение intVar равно " << intVar << endl; return 0; } Для ввода/вывода в С++ используют потоки ввода и вывода. Для этого необходимо включить заголовочный файл <iostream>. Для ввода используется операция >>, для вывода — операция <<. Компилятор определяет тип вводимой/выводимой переменной и соответствующим образом форматирует её. Если при вводе или выводе произошла ошибка, в переменной состояния потока устанавливается соответствующий флаг. Проверить его значение можно с помощью функции fail. Пример использования потоков вводавывода представлен в листинге 1.10. Листинг 1.10 #include <iostream> using namespace std; int main() { int ftemp; cout << "Введите температуру по Фаренгейту: "; cin >> ftemp; if (cin.fail()) //Обнаружение ошибок cout << "Произошла ошибка при вводе" << endl; int ctemp = (ftemp‐32)*5 / 9; cout << "Температура no Цельсию равна " << ctemp << endl; return 0; } 13
Манипуляторы ввода-вывода управляют форматом вводимого/выводимого значения. Это функции, которые вставляются между вводимыми/выводимыми значениями и изменяют состояние потока. Для использования манипуляторов необходимо включить заголовочный файл <iomanip>. Стандартные манипуляторы вводавывода приведены в табл. 1.3 Пример использования манипулятора setw представлен в листинге 1.11 Таблица 1.3 Манипулятор Описание 1 boolalpha 2 Значения переменных типа bool выводятся как true и false. dec Целые значения выводятся в десятичной системе счисления. fixed Для вещественных чисел используется фиксированный формат. hex Целые значения выводятся в шестнадцатеричной системе счисления. internal Знак выравнивается по левому краю, а само число — по правому краю. left Выравнивание по левому краю. noboolalpha Значения переменных типа bool выводятся как 1 и 0. noshowbase Префиксы 0 и 0х, обозначающие систему счисления, не выводятся. noshowpoint Вывод вещественного числа как целого, если дробная часть равна 0. noshowpos Знак перед положительными числами не выводится. noskipws Пробел используется как признак завершения ввода. nouppercase Шестнадцатеричные цифры и символ экспоненты в научном формате вещественного числа выводятся строчными буквами. oct Целые значения выводятся в восьмеричной системе счисления. right Выравнивание по правому краю. scientific Для вещественных чисел используется научный формат. setfill(c) Задаёт символ для заполнения. По умолчанию используется пробел. setprecision(n) Задаёт точность для вещественных чисел. Если число слишком велико, оно автоматически отображается в научном формате. 14
Окончание табл. 1.3 1 setw(n) 2 Устанавливает минимальное количество символов, используемых для вывода значения. Выравнивание задаётся манипуляторами left, right и internal. n=0 задает столько символов, сколько необходимо. showbase Вывод префиксов 0 и 0х для обозначения системы счисления. showpoint Вывод и целой, и дробной частей вещественного числа, даже если дробная часть равна 0. showpos Вывод знака перед положительным числом. skipws Пробелы рассматриваются как разделители между значениями. uppercase Шестнадцатеричные цифры и символ экспоненты в научном формате вещественного числа выводятся прописными буквами. Листинг 1.11 #include <iostream> #include <iomanip> // для использования setw using namespace std; int main() { long nas1 = 8425785, nas2 = 4761, nas3 = 9761; cout << setw(9) << "Город" << setw(12) << "На‐ селение" << endl << setw(9) << "Москва" << setw(12) << nas1 << endl << setw(12) << nas2 << endl << setw(9) << "Киров" << setw(9) << "Угрюмовка" << setw(12) << nas3 << endl; return 0; } 15
ЛЕКЦИЯ 2. ЦИКЛЫ И ВЕТВЛЕНИЯ Циклы необходимы, когда нам надо повторить некоторые действия несколько раз, как правило, пока выполняются определенные условия. В программах предусмотрены переходы из одной ее части в другую, в зависимости от выполнения или невыполнения условия. Операторы, реализующие подобные переходы, называются условными операторами. Операции отношения используются для сравнения значений в условных выражениях. Значения могут быть как стандартных типов языка C++ , так и типов, определяемых пользователем (в таком случае оператор должен быть переопределен). Сравнение устанавливает одно из трех возможных отношений между переменными: равенство, больше, меньше. Результатом сравнения является значение истина(1) или ложь(0). Несмотря на то, что в большинстве случаев, для представления истинного значения используют 1, любое отличное от нуля число будет воспринято как истинное (табл. 2.1). Обратите внимание на то, что операция эквивалентности, в отличие от операции присваивания, обозначается с помощью двойного знака равенства. Примеры условных выражений: a>0; num<=100; ‘c’== ‘C’; ‘c’!=‘C’. Таблица 2.1. Операция > Название больше < меньше == эквивалентно != не равно >= больше или равно <= меньше или равно Действие циклов заключается в последовательном повторении определенной части программы до тех пор, пока выполняется со16
ответствующее условие. Когда значение выражения, задающего условие, становится ложным, цикл завершается, а управление передается следующему оператору. Известно три вида оператора цикла: for, while, do-while. Оператор цикла for организует пошаговое выполнение фрагмента программы фиксированное число раз. Имеет следующий вид: for (выражение1; выражение2; выражение3) оператор. Первое поле используется для присвоения начального значения параметру цикла. Второе поле — условное выражение, определяет, когда цикл должен быть завершен. Третье поле используется для изменения параметра цикла каждый раз при повторении цикла. Эти три поля должны быть разделены точкой с запятой. Выполнение происходит до тех пор, пока условное выражение истинно. Далее начинает выполняться оператор, следующий за циклом for. Рассмотрим пример (листинг 2.1). Листинг 2.1 Результат: Введите число: 10 Факториал числа равен 3628800 // подсчет факториала числа с помощью цикла for #include <iostream> using namespace std; int main() { unsigned int numb; unsigned long fact = 1; // тип long для рез. cout << "Введите целое число: " ; cin >> numb; // ввод числа for(int j=numb; j>0; j‐‐) // умножение 1 на fact *= j; // numb, numb‐1,,2,1 cout << "Факториал числа равен " << fact << endl; return 0; } 17
Оператор цикла while — вид цикла, использующийся при неизвестном количестве повторений. Имеет вид: while ( условие ) оператор. Оператор может быть простым, составным или пустым. Условие — это просто выражение. Цикл выполняется до тех пор, пока условие принимает значение «истинно». Когда же значение — «ложно», программа передает управление следующему оператору. Цикл while, как и for, называют циклом с предусловием (листинг 2.2). Листинг 2.2. // Цикл while‚ возведение в четвертую степень целых чисел #include <iostream> #include <iomanip> //для setw using namespace std; int main() { int numb = 1; // первое возводимое число равно 1 int pow = 1; // 1 в 4‐й степени равна 1 while(pow < 10000) // цикл, пока степень < 10000 { cout << setw(2) << numb; // вывод числа cout << setw(5) << pow <<” “; // и его 4‐й степени ++numb; // инкремент текущего числа pow = numb*numb*numb*numb; //вычисление 4‐й степ. } cout << endl; return 0; } Результат: 1 1 2 16 3 81 . . . 9 6561 Оператор цикла do-while. В отличие от предыдущих видов, в цикле do-while условие проверяется в конце оператора цикла: Do {последовательность операторов} while (условие). Оператор do-while относится к циклам с постусловием. Какое бы условие в конце оператора ни стояло, набор операторов в фигурных скобках один (первый) раз выполнится обязательно. 18
Управление циклом сводится к вопросу: продолжать выполнение или нет? Ветвление — это переход в другую часть программы в зависимости от значения соответствующего выражения. Условный оператор if. Форма этого оператора следующая: if ( условие ) оператор; else оператор. Если значение условия «истинно», то выполняется оператор (им может быть составной оператор-блок), следующий за условием. Если же условие принимает значение «ложно», то выполняется оператор, следующий за ключевым словом else. Часто в программе необходимо использовать функцию if-else-if. В алгоритмах и программах ветвления могут быть последовательными (расположенными одно за другим) и вложенными (одно внутри другого). Вложенные группы ветвлений называются деревом ветвлений. Условный оператор switch — встроенный оператор множественного выбора. Основная форма имеет вид: switch (выражение){ case constant1: последовательность операторов; break; case constant2: последовательность операторов; break; … case constantN: последовательность операторов; break; default: последовательность операторов; } Сначала вычисляется выражение в скобках за ключевым словом switch. Затем просматривается список меток (case constant N и т. д.) до тех пор, пока не найдется метка, соответствующая значению вычисленного выражения. Далее происходит выполнение операторов, следующих за двоеточием. Если же значение выраже19
ния не соответствует ни одной из меток switch, то выполняется последовательность, следующая за оператором default. В случае, когда после последовательности операторов встречается слово break, выполнение этого оператора приводит к выходу из switch и переходу к следующему оператору программы. Рассмотрим пример с использованием ветвления switch (листинг 2.3). Листинг 2.3. // применение ветвления switch #include <iostream> using namespace std; int main() { int speed; // скорость вращениЯ грампластинки cout << "\n‚ведите число 33,45 или 78: "; cin >> speed; // ввод скорости пользователем switch(speed) // действия, зависящие от скорости { case 33: // если пользователь ввел 33 cout << "Долгоиграющий формат\n"; break; case 45: // если пользователь ввел 45 cout << "Формат сингла\n"; break; case 78: // если пользователь ввел 78 cout << "Устаревший формат патефона\n"; break; } return 0; } Результат: Введите 33, 45 или 78: 45 Формат сингла Условная операция. На практике широко распространена условная операция: (alpha < beta) ? alpha : beta // условное выражение 20
Условие, стоящее перед вопросительным знаком — проверяемое условие. Знак вопроса ? и двоеточие : обозначают условную операцию. Если значение проверяемого условия истинно, то условное выражение становится равным alpha, в противном случае — beta. Логические операции — операции, позволяющие производить действия над булевыми переменными, то есть переменными, обладающими только двумя значениями — истина и ложь (табл. 2.2). Таблица 2.2 Операция Обозначения Условия Краткое описание И && (a=5 && b>1000) Составное условие истинно, если истинны оба простых условия ИЛИ || (a==5 || b>1000) Составное условие истинно, если истинно, хотя бы одно из простых условий НЕ ! !(b==1000) Условие истинно, если b не равно 1000 Сведем в единую таблицу приоритеты операций. Приоритет убывает сверху вниз. Операции, находящиеся на одной строке, имеют одинаковый приоритет (табл. 2.3). Таблица 2.3 Тип операции Операции Унарные !,++,--,+,- Арифметические Мультипликативные *,/,% Приоритет Высший Аддитивные +,Неравенства <,>, <=, >= Равенства ==,!= Логические И&& ИЛИ|| Условная Присваивания ?: =,+=,-=,*=,/=,%= Низший 21
Операторы перехода Оператор break применяется в двух случаях. Во-первых, в операторе switch с его помощью прерывается выполнение case. В этом случае управление передается первому оператору, следующему за конструкцией switch. Во-вторых, оператор break используется для немедленного прекращения выполнения цикла без проверки его условия, в этом случае оператор break передает управление оператору, следующему после оператора цикла. Оператор continue — прерывание текущей итерации цикла и осуществление перехода к следующей итерации. При этом все операторы до конца тела цикла пропускаются. В цикле for оператор continue вызывает выполнение операторов приращения и проверки условия цикла. В циклах while и do-while оператор continue передает управление операторам проверки условий цикла (листинг 2.4). Оператор return используется для выхода из функции. Отнесение его к категории операторов перехода обусловлено тем, что он заставляет программу перейти в точку вызова функции. Оператор return может иметь ассоциированное с ней значение, тогда при выполнении данного оператора это значение возвращается в качестве значения функции. В функциях типа void используется оператор return без значения. Общая форма оператора return следующая: return выражение; Листинг 2.4. // применение оператора continue #include <iostream> using namespace std; int main() { long dividend, divisor; char ch; do { cout << "Введите делимое: "; cin >> dividend; cout << "Введите делитель: " ; cin >> divisor; if( divisor == 0 ) // при попытке { // деления на ноль cout << "Некорректный делитель!\n"; // вывод сообщения 22
continue; // возврат в начало цикла } cout << "Частное равно " << dividend / divisor; cout << ", остаток равен " << dividend % divisor; cout << "\nЕще раз?(y/n): "; cin >> ch; } while( ch != 'n' ); return 0; } Результат: Введите делимое: 10 Введите делитель: 0 Некорректный делитель! Оператор goto весьма непопулярен, более того, считается, что в программировании не существует ситуаций, в которых нельзя обойтись без оператора goto. В некоторых случаях его применение все же уместно. Этот оператор может оказаться весьма полезным, если нужно покинуть глубоко вложенные циклы. 23
ЛЕКЦИЯ 3. СТРУКТУРЫ И ПЕРЕЧИСЛЕНИЯ Синтаксис определения структуры и структурной переменной. Структуры в С++ используются для логического и физического объединения данных произвольных типов (переменные стандартного типа, указатели, переменные пользовательского типа). Переменные, входящие в состав структуры, называются полями структуры. Объекты структур можно присваивать, передавать в качестве аргументов и возвращать в качестве значений функций. Другие операторы (такие как == и != не определены). Объекты структур могут являться элементами массивов. Для более сложных пользовательских типов данных в языке С++ используются классы. Структура обязательно объявляется перед объявлением переменной. Структура в С++ задаётся следующим образом: struct <имя_структуры> { члены (элементы) структуры }; Далее приведен пример структуры: struct part { int modelnumber; int partnumber; float cost; }; Определение структуры начинается с ключевого слова struct, затем следует имя структуры. Объявления полей структуры за24
ключены в фигурные скобки. После закрывающей фигурной скобки следует точка с запятой (;). Далее рассмотрим определение переменной. Переменная типа, определенного пользователем, объявляется двумя способами: 1) при определении структуры struct part { int modelnumber; int partnumber; float cost; } p1, p2; 2) после определения структуры part p1, p2; Определение переменной означает, что под эту переменную выделяется память. Под структурную переменную всегда отводится столько памяти, сколько достаточно для хранения всех ее полей. Доступ к полям структуры. Когда структурная переменная определена, доступ к ее полям возможен с применением операции точки. В следующей строчке первому из полей структуры part присваивается значение при помощи оператора «=»: p1.modelnumber = 6244; Поле структуры идентифицируется с помощью трех составляющих: имени структурной переменной p1, операции точки (.) и имени поля modelnumber. Инициализация полей структуры. Инициализация структурных переменных может производиться разными способами: 1) после объявления структуры; part p1 = { 6244, 373, 217.55F }; 2) инициализация каждого поля отдельно, используя операцию точки «.» p2.modelnumber = 7000; p2.partnumber = 400; p2.cost = 160.11; 25
В основном инициализация происходит первым способом, в силу его лаконичности. Вложенные структуры. Структуры допускают вложенность, то есть использование структурной переменной в качестве поля какой-либо другой структуры: struct Distance // длина в английской системе { int feet; float inches; }; ////////////////////////////////////////////////////// struct Room // размеры прямоугольной комнаты { Distance length; // длина Distance width; // ширина }; Инициализация полей таких структур имеют свою специфику. Далее приведен листинг 3.1 программы, в котором описана полная концепция вложенных структур. В нем показано, что для доступа к полям внутренней структуры необходимо дважды применить операцию точки (.): dining.length.arsheen = 13; Листинг 3.1 #include <iostream> using namespace std; ///////////////////////////////////////////// struct Length // длина в русской системе { int arsheen; // аршины float vershok; // вершки }; ////////////////////////////////////////////////////// struct Room // размеры прямоугольной комнаты { Length length; // длина Length width; // ширина }; 26
////////////////////////////////////////////////////// int main() { Room dining; // переменная dining типа Room dining.length.arsheen = 13; // задание параметров комнаты dining.length.vershok = 6.5; dining.width.arsheen = 10; dining.width.vershok = 0.0; // преобразование длины и ширины в вещественный формат float l = dining.length.arsheen + din‐ ing.length.vershok / 3; float w = dining.width.arsheen + din‐ ing.width.vershok / 3; // вычисление площади комнаты и вывод на экран cout << "Площадь комнаты равна " << l * w << " квадратных футов\n"; return 0; } Следующий оператор инициализирует переменную dining теми же значениями, что и в последней программе: Room dining = { {13, 6.5}, {10, 0.0} }; Присваивание структурных переменных: p2=p1; Значение каждого поля переменной p1 присваивается соответствующему полю переменной p2. Нельзя присваивать переменные разных структур друг другу, даже если шаблоны структур совпадают. Синтаксис перечисления. Перечисляемый тип задаёт тип, который является подмножеством целого типа. Объявление переменной перечисляемого типа задаёт имя переменной и определяет список именованных констант, называемый списком перечисления: enum [<тег>] {<список перечисления>} <описатель> [, <описатель> ...]; enum <тег> <описатель> [, <описатель> ...]; 27
Тег предназначен для различения нескольких перечислимых типов, объявленных в одной программе. Список перечисления содержит одну или более конструкций вида: <идентификатор> [= <константное выражение>] Конструкции в списке разделяются запятыми. Каждый идентификатор именует элемент списка перечисления. По умолчанию, если не задано константное выражение, первому элементу присваивается значение 0, следующему элементу — значение 1 и т. д. Запись = <константное выражение> изменяет умалчиваемую последовательность значений. Элемент, идентификатор которого предшествует записи = <константное выражение>, принимает значение, задаваемое этим константным выражением. Константное выражение должно иметь тип int и может быть как положительным, так и отрицательным. Следующий элемент списка получает значение, равное <константное выражение> + 1, если только его значение не задаётся явно другим константным выражением. В списке перечисления могут содержаться элементы, которым сопоставлены одинаковые значения, однако каждый идентификатор в списке должен быть уникальным. Кроме того, идентификатор элемента списка перечисления должен быть отличным от идентификаторов элементов всех остальных списков перечислений, а также от других идентификаторов: enum Weekdays {SA, SU, MO, TU, WE, TH, FR}; enum Weekdays {SA, SU = 0, MO, TU, WE, TH, FR}; Следует заметить, что SA и SU имеют одинаковое значение (0). Переменным перечисляемого типа можно присваивать любое из значений, указанных при объявлении типа. Перечисляемые типы данных допускают применение основных арифметических операций, операций сравнения. Далее представлен листинг 3.2, который предлагается изучить самостоятельно. Листинг 3.2 #include <iostream> using namespace std; 28
// объявление перечисляемого типа enum days_of_week { Sun, Mon, Tue, Wed, Thu, Fri, Sat }; int main() { days_of_week day1, day2; // определения переменных, хранящих дни недели day1 = Mon; // инициализация переменных day2 = Thu; int diff = day2 — day1; // арифметическая операция cout << "Разница в днях: " << diff << endl; // сравнение if(day1 < day2) cout << "day1 наступит раньше, чем day2\n"; return 0; } 29
4. ФУНКЦИИ Определение функции задает тип возвращаемого значения, имя функции, типы и число формальных параметров, а также объявления переменных и операторы, называемые телом функции и определяющие действия функции (листинг 4.1). Формат определения: Листинг 4.1 тип_возвращаемого_значения имя_функции (список параметров) { последовательность операторов } Типы в определении и объявлениях функции должны совпадать. Однако имена параметров не являются частью типа и не обязаны совпадать. Список формальных параметров может заканчиваться многоточием (…) — это означает, что число аргументов функции переменное. Однако предполагается, что функция имеет, по крайней мере, столько обязательных аргументов, сколько формальных параметров задано перед последней запятой в списке параметров. Такой функции может быть передано большее число аргументов, но над дополнительными аргументами не проводится контроль типов. Если функция не имеет параметров, то наличие круглых скобок обязательно, а вместо списка параметров рекомендуется указать слово void. В языке С++ существует 3 способа передачи аргументов в функцию: • передача аргумента по значению; • передача с использованием указателя на аргумент; • передача аргумента по ссылке. 30
Примеры различных способов передачи аргументов содержит листинг 4.1. Передача аргументов по значению означает, что в вызываемой функции для каждого формального аргумента создаётся локальный объект, который инициализируется значением фактического аргумента. Следовательно, при такой передаче изменения значений формальных параметров функции не приводит к изменению значений соответствующих им фактических аргументов. Пример передачи аргумента по значению представлен в функции pass_by_value листинга 4.1. В случае, если функция должна менять свои аргументы, можно использовать указатели. Указатели также передаются по значению, внутри функции создается локальная переменная-указатель. Но, так как этот указатель инициализируется адресом переменной из вызываемой программы, то эту переменную можно менять, используя этот адрес. В частности, при использовании массивов в качестве аргументов, имя массива преобразуется к указателю на его первый элемент, т.е. при передаче массива происходит передача указателя. По этой причине вызываемая функция не может отличить, относится ли передаваемый ей указатель к началу массива или к одному единственному объекту. Пример передачи аргумента с помощью указателя представлен в функции pass_by_pointer листинга 4.2. Ссылки часто используются в качестве формальных параметров функций. При их применении в параметр копируется адрес аргумента. Это значит, что, в отличие от вызова по значению, изменения значения параметра приводят к точно таким же изменениям значения аргумента. С помощью ссылок можно добиться изменения значений фактических параметров из вызывающей программы без применения указателей. Пример передачи аргумента с помощью указателя представлен в функции pass_by_reference листинга 4.2. Листинг 4.2 #include <iostream> using namespace std; 31
void pass_by_value (int a) { a ++; cout<<"В функции pass_by_value a = " << a << endl; } void pass_by_pointer (int* a) { *a +=2; cout << "В функции pass_by_pointer a = " << *a << endl; } void pass_by_reference (int& a) { a +=3; cout << "В функции pass_by_reference a = " << a << endl; } int main() { int a; cout << "Введите перевенную <a> "; cin >> a; cout << "Вы ввели " << a << endl; pass_by_value (a); cout << "Сейчас в функции main переменная a = " << a << endl; pass_by_pointer (&a); cout << "Сейчас в функции main переменная a = " << a << endl; pass_by_reference (a); cout << "Сейчас в функции main переменная a = " << a << endl; return 0; } Тип возвращаемого значения, задаваемый в определении функции, должен соответствовать типу в объявлении этой функции. Функция может возвращать значение любого типа. Если тип не задан, то по умолчанию функция возвращает значение типа int. 32
Функция не может возвращать массив, но может возвращать указатель на любой тип, в том числе и на массив, объект, структуру или функцию. Функция возвращает значение, если ее выполнение заканчивается оператором return, содержащим некоторое выражение. Указанное выражение вычисляется, преобразуется, если необходимо, к типу возвращаемого значения и посылается в точку вызова функции в качестве результата. Оператор return прерывает выполнение функции, т. е. он должен быть последним в теле функции. Функция может иметь несколько операторов return. Для функций, которые не должны ничего возвращать, может быть использован тип void, указывающий на отсутствие возвращаемого значения. Пример использования операции return представлен в листинге 4.3. Листинг 4.3 #include <iostream> using namespace std; int summa (int& a, int &b) { return a+b; } int main() { int a, b; cout << "Введите два целых числа "; cin >> a >> b; cout << a << " + " << b << " = " << summa( a, b) << endl; return 0; } Перегруженные функции — это функции с одним именем, но с разными типами аргументов. Их используют, когда несколько функций выполняют одинаковые действия, только над разными типами. Перегруженные функции могут отличаться не только ти33
пами аргументов, но и их количеством, но они должны возвращать значение одного типа. Пример перегруженных функций представлен в листинге 4.4. Листинг 4.4 #include <iostream> #include <cmath> using namespace std; int summa(int& a, int& b) { cout << "Складываем два целых числа "; return a+b; } int summa(int& a, int& b, int&c) { cout << "Складываем три целых числа "; return a+b+c; } int summa(float& a, float& b) { cout << "Складываем два дробных числа "; return floor(a+b); //выделение целой части } int main() { int a, b, c; float d, e; cout << "Введите два целых числа "; cin >> a >> b; cout << a <<" + "<< b <<" = "<< summa(a,b) << endl; cout << "Введите еще одно целое число "; cin >> c; cout << a <<" + "<< b <<" + "<< c <<" = "<< summa(a,b,c) << endl; cout << "Введите два дробных числа "; 34
cin >> d >> e; cout << d <<" + "<< e <<" = "<< summa(d,e) << endl; return 0; } Вызов функции, передача в неё значений, возврат значения, все эти операции занимают довольно много процессорного времени. При использовании небольших функций (состоящих из несколько операторов), время выполнения тела функции будет меньше, чем процесс вызова/передачи аргументов/возврата значения. Чтобы сократить время работы программы, можно воспользоваться встраиваемыми функциями. Перед объявлением/определением такой функции нужно добавить спецификатор inline. Во время компиляции все вызовы функции будут заменены непосредственно кодом функции. За счет этого программа будет выполняться быстрее, но займёт больше памяти (если функция вызывается много раз). Компилятор решает делать функцию встраиваемой или нет, в зависимости от ее размера. Если функция, содержащая сотню операторов, объявлена встраиваемой, компилятор сочтёт её слишком большой, и будет её использовать как обычную. Пример использования встраиваемой функции представлен в листинге 4.5. Листинг 4.5 #include <iostream> using namespace std; inline float sum_cvad(float x = 0.0F, float y = 0.0F) { return (x * x + y * y); } int main() { float a, b; cout << "Введите два числа "; cin >> a >> b; cout << "a^2 + b^2 = " << sum_cvad(a,b) << endl; 35
cout << "a^2 = " << sum_cvad(a) << endl; /*верно, т.к. по умолчанию второй аргумент равен нулю.*/ return 0; } Аргументы по умолчанию. В языке С++ можно задавать так называемые параметры функции по умолчанию. Если в объявлении формального параметра задано выражение, то оно воспринимается как умолчание этого параметра. Все последующие параметры также должны иметь умолчания. Умолчания параметров подставляются в вызов функции при отсутствии в нём последних по списку параметров. Примером аргументов по умолчанию являются переменные x и у функции sum_cvad в листинге 4.4. Константные аргументы функции. При передаче аргумента по значению, переменная из вызывающего окружения не изменяется. Если использовать данный способ передачи, будет происходить копирование, а это значит замедление программы. Константные аргументы функции применяются, когда нужно передать аргумент по ссылке и при этом запретить его изменение. В списке аргументов объявления/определения нужно поставить ключевое слово const перед соответствующим аргументом. Пример приведен в листинге 4.6. Листинг 4.6 void aFunc( int& a, const int& b); // прототип функции int main() { int alpha = 7; int beta = 11; aFunc(alpha, beta); return 0; } void aFunc(int& a, const int& b) // определение функции { a = 107; // корректно b = 111; /* ошибка при попытке изменить кон‐ стантный аргумент*/ } 36
Область видимости и класс памяти. У каждой функции есть своя область видимости. В область видимости функции входят все глобальные переменные и переменные, объявленные в этой функции. Глобальные переменные — это переменные, которые определены за пределами любой функции. Глобальные переменные существуют с того момента как они встретились в коде и до конца программы. Если глобальную переменную объявить после какойнибудь функции, то в этой функции данную переменную нельзя будет использовать. Локальные переменные объявлены внутри функций и видны только в них самих. Обычные локальные переменные, когда функция завершается, уничтожаются. При каждом выполнении функции они создаются заново. Статические переменные определяются только один раз — когда функция вызывается в первый раз. Когда функция заканчивает выполнение операторов, статические переменные остаются в памяти. Когда функция снова вызывается, она продолжает их использовать. Например, в листинге 4.2 есть две локальные переменные а, одна принадлежит функции main, другая — pass_by_value. Рекурсивные функции. Ситуацию, когда функция тем или иным образом вызывает саму себя, называют рекурсией. Рекурсия, когда функция обращается сама к себе непосредственно, называется прямой; в противном случае она называется косвенной. Все функции языка С++ (кроме функции main) могут быть использованы для построения рекурсии. В рекурсивной функции обязательно должно присутствовать хотя бы одно условие, при выполнении которого последовательность рекурсивных вызовов должна быть прекращена. Рекурсивные функции обычно выполняются медленнее, чем их нерекурсивные (итеративные) аналоги. Это связано с затратами времени на вызов функции. Однако, как правило, они компактнее и понятнее. Примером рекурсивной функции служит функция factor в листинге 4.7. 37
Листинг 4.7 #include <iostream> long factor (int n) { if ( n<1 ) return 1; else return n*fact(n‐1); } int main() { std::cout << "Факториал числа 5 равен " << factor(5) << std::endl; return 0; } 38
ЛЕКЦИЯ 5. ОБЪЕКТЫ И КЛАССЫ Определение класса (поля, методы, доступ к членам класса). Классы являются основой С++. Для того чтобы определить объект, нужно сначала определить его форму с помощью ключевого слова class. Объект находится в таком же отношении к своему классу, в каком переменная находится по отношению к своему типу. Объект является экземпляром класса, так же, как и автомобиль — колесного средства передвижения. Класс может содержать приватную часть (private) и общую (public). Приватные элементы не могут использоваться никакими функциями, не являющимися членами класса. Также можно определить и приватные функции, которые могут вызываться только другими функциями — членами класса. Это — один из путей реализации принципа инкапсуляции. По умолчанию все элементы класса приватные, поэтому ключевое слово private можно опустить. Поле класса — это данные, содержащиеся внутри класса. Методы класса — это функции, входящие в состав класса: Имя класса Фигурные class name_1 { private: ----------------------Ключевое слово private и (:) int date;---------------------Скрытые функции и данные public:-------------------------Ключевое слово public и (:) void memfunc(int d) Общедоступные функции {date = d;} } ; Точка с запятой Определение методов в классе и вне класса. Доступ к методам класса возможен только через конкретный объект этого класса. Для этого используют операцию точки, т. е. операцию доступа к члену класса (.). Рассмотрим пример определения метода в классе (листинг 5.1). 39
Листинг 5.1 // детали изделия в качестве объектов #include <iostream> using namespace std; ////////////////////////////////////////////////////// class part // определение класса { private: int modelnumber; // номер изделия int partnumber; // номер детали float cost; // стоимость детали public: // установка данных void setpart(int mn, int pn, float c) { modelnumber = mn; partnumber = pn; cost = c; } void showpart() // вывод данных { cout << "модель " << modelnumber; cout << ", деталь " << partnumber; cout << ", стоимость $" << cost << endl; } }; ////////////////////////////////////////////////////// int main() { part part1; // определение объекта // класса part part1.setpart(6244, 373, 217.55F); // вызов метода part1.showpart(); // вызов метода return 0; } Результат: Модель 6244, деталь 373, цена $217.55 Метод класса не обязательно определять внутри самого класса. Для этого определение класса должно содержать прототип функ40
ции. В таком случае форма записи, что устанавливает взаимосвязь функции и класса, к которому относится эта функция, имеет вид void part :: setpart(int mn, int pn, float c) Аргументы Имя Операция разрешения Имя класса Возвращаемый Символ (::) является знаком операции глобального разрешения. А имя класса, операция разрешения и имя функции вместе называют — квалификационным именем функции. Конструкторы и деструкторы. Необходимость инициализации является общим требованием, поэтому С++ предоставляет возможность делать это автоматически при объявлении объекта. Эта автоматическая инициализация реализуется использованием функции, называемой конструктором. Конструктор — специальная функция, являющаяся членом класса и имеющая то же имя. Наиболее часто возлагаемая на конструктор задача — это инициализация полей объекта касса. Однако инициализация не проводится в теле конструктора. Инициализирующее значение расположено в скобках после имени поля. И если необходимо инициализировать сразу несколько полей, то значения разделяются запятыми и образуется список инициализации: someClass(): m1(0), m2(10) {/*пустое тело*/} Нельзя вызывать функцию конструктора в явном виде. Она может иметь параметры, может быть перегруженной. Если в классе не объявлен ни один конструктор, то компилятор сам создает конструктор класса. Во время создания объекта, при выделении памяти вызывается конструктор, а для освобождения памяти из-под объекта, т. е. при прекращении действия объекта, выполняется деструктор. Он имеет такое же имя, как и класс, и перед ним ставится знак тильды (~). 41
Деструктор не имеет параметров. Не возвращает значений (листинг 5.2). Листинг 5.2 // графические объекты "круг" и конструкторы #include "msoftcon.h " // для функций консольной графики ////////////////////////////////////////////////////// class circle // графический объект "круг" { protected: int xCo,yCo; // координаты центра int radius; color fillcolor; // цвет fstyle fillstyle; // стиль заполнения public: // конструктор circle( int x, int y, int r, color fc, fstyle fs ): xCo(x), yCo(y), radius(r), fillcolor(fc), fillstyle(fs) { } // рисование круга void draw() { set_color(fillcolor); // установка цвета и set_fill_style(fillstyle); // стиля заполнения draw_circle(xCo,yCo,radius); // вывод круга на экран } }; ////////////////////////////////////////////////////// int main() { init_graphics(); // инициализация графики // создание кругов circle c1(15, 7, 5, cBLUE, X_FILL); circle c2(41, 12, 7, cRED, O_FILL); circle c3(65, 18, 4, cGREEN, MEDIUM_FILL); c1.draw(); // рисование кругов c2.draw(); c3.draw(); set_cursor_pos(1,25); // левый нижний угол return 0; } 42
Объекты как аргументы методов и доступ к их членам. Объекты можно передавать в функции в качестве аргументов точно так же, как передаются данные других типов. Следует помнить, что С++ методом передачи параметров, по умолчанию является передача объектов по значению. Это означает, что внутри функции создается копия объекта — аргумента, и эта копия, а не сам объект, используется функцией. Следовательно, изменения копии объекта внутри функции не влияют на сам объект. Можно передать параметр объекта и по ссылке, т. е. передать адрес объекта. Тогда изменения, произведенные функцией в объекте, могут изменить этот объект. При передаче в качестве параметра функции по значению объекта, как и при передаче простой переменной, произойдет создание копии объекта, тем самым вызовется конструктор, а затем деструктор (листинг 5.3). Листинг 5.3 class OBJ { int i; public: void set_i(int x){i=x;} void out_i{cout<<i<<endl;} }; void my_function(OBJ x); // прототип int main(void) { OBJ A; A.set_i(10) ; my_function(A); A.out_i(); return 0; } void my_function(OBJ x) { x. out_i(); // вывод x = 10 x. set_i(100) ;// изменение x x. out_i(); // вывод x=100 } 43
Каждый вызов метода класса обязательно связан с конкретным объектом этого класса (исключением является вызов статической функции). Метод может прямо обращаться по имени к любым, открытым и закрытым членам этого объекта. Кроме этого, метод имеет непрямой (через операцию точки(.)) доступ к членам других объектов своего класса; последние выступают в качестве аргументов метода. Конструктор копирования по умолчанию. Конструктор без аргументов может инициализировать поля объекта константными значениями. Конструктор, имеющий хотя бы один аргумент, может инициализировать поля значениями, переданными ему в качестве объектов. Существует и третий способ инициализации объекта, использующий значения полей уже существующего объекта. Такой конструктор представляется компилятором для каждого создаваемого класса и называется копирующим конструктором по умолчанию. Этот конструктор имеет единственный аргумент, являющийся объектом того же класса, что и конструктор: имя_класса(const Имя_класса &). Конструктор копирования создается всякий раз, когда создается новый объект и в качестве его значения выбирается существующий объект того же самого типа. Пусть у нас имеются инициализированный объект класса myClass obj1. Тогда, определив еще два объекта класса myClass, мы можем инициализировать их следующим образом: myClass obj2(obj1); myClass obj3 = obj1. В обоих случаях вызывается конструктор копирования по умолчанию, действия которого сводятся к копированию значений полей объекта obj1 в соответствующие поля obj2 и obj3. Также компилятор использует конструктор копирования, когда программа генерирует копии объектов при передаче объектов по значению или возврате их. Конструктор копирования по умолчанию производит почленное копирование нестатических членов — поверхностное копирование. Каждый член копируется по значению. 44
Размещение в памяти членов объектов одного класса и способ создания общих полей (свойств). Если поле данных класса описано с ключевым словом static, то значение этого поля будет одинаковым для всех объектов данного класса. Статические данные класса полезны, когда необходимо, чтобы все объекты включали какие-либо одинаковые значения. Они видны только внутри класса и время жизни совпадает со временем выполнения программы. Статические поля применяются гораздо реже, чем автоматические. Обычные поля объявляются (компилятору сообщается имя и тип поля) и определяются (компилятор выделяет память для хранения поля) при помощи одного оператора. Объявление статического поля находится внутри определенного класса, а определение поля располагается вне класса и представляет собой определение глобальной переменной. Это делается потому, что если бы определение находилось внутри класса, то это нарушало бы принцип, в соответствии с которым определение класса не должно быть связано с выделением памяти. Поместив определение статического поля вне класса, мы обеспечили однократное выделение памяти под это поле до того, как программа будет запущена на выполнение, и статическое поле в этом случае станет доступным всему классу. Каждый объект класса уже не будет обладать своим экземпляром поля, как это должно быть с автоматическими полями (листинг 5.4). Листинг 5.4 // статические данные класса #include <iostream> using namespace std; ////////////////////////////////////////////////////// class foo { private: static int count; // общее поле для всех объектов // (в смысле "объЯвлениЯ") public: foo() // инкрементирование при создании объекта { count++; } 45
int getcount() // возвращает значение count { return count; } }; //‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐ int foo::count = 0; // *определение* count ////////////////////////////////////////////////////// int main() { foo f1, f2, f3; // создание трех объектов // каждый объект видит одно и то же значение cout << "Число объектов: " << f1.getcount() << endl; cout << "Число объектов: " << f2.getcount() << endl; cout << "Число объектов: " << f3.getcount() << endl; return 0; } Результат: Число объектов: 3 Число объектов: 3 Число объектов: 3 Константные методы, их аргументы и константные объекты. Константные методы отличаются тем, что не изменяют значений полей своего класса. Для того чтобы сделать функцию константной, необходимо указать ключевое слово const после прототипа, но до начала тела функции. Если объявление и определение разделены, то const указывается дважды. Константными следует делать методы, у которых нет необходимости изменять значение полей объектов класса. Если нужно передать аргумент в функцию по ссылке и в то же время защитить его от изменения функцией, необходимо сделать этот аргумент константным при объявлении и определении метода. Кроме того, модификатор const применим и для объектов классов. Если объект класса объявлен с модификатором const?, он становится недоступным для изменения. Это означает, что для такого объекта можно вызвать только константные методы, поскольку они гарантируют, что объект не будет изменен (листинг 5.5). Листинг 5.5 #include <iostream> using namespace std; 46
class MyClass { public: static int n; static int addn(int arg) { return n+arg; } }; // инициализация статического члена данных int MyClass::n=30; int main() { // обращение к статическим членам // через имя класса MyClass::n=3; cout<<MyClass::addn(2)<<endl; // обращение к статическим членам // через объект MyClass cls; cls.n=5; cout<<cls.addn(2)<<endl; system("pause"); return 0; } Результат: 5 7 47
ЛЕКЦИЯ 6. МАССИВЫ И СТРОКИ Объявление одномерного и многомерного массива. Общая форма объявления одномерного массива имеет следующий вид: <класс> тип имя [размер], где класс — необязательный элемент, определяющий класс памяти (extern, static, register); тип — базовый тип элемента массива; имя — идентификатор массива; размер — количество элементов в массиве. Доступ к элементу массива осуществляется с помощью имени массива и индекса. Индекс элемента массива помещается в квадратных скобках после имени. Нижнее значение индекса всегда нуль. Таким образом, элементами массива, состоящего из N элементов, являются переменные с индексами a[0],a[1],…,a[N–1]. Здесь квадратные скобки являются элементом синтаксиса, а не признаком необязательности конструкции. Объявление массива может иметь одну из двух синтаксических форм, указанных выше. Квадратные скобки, следующие за именем, — признак того, что переменная является массивом. Константное выражение, заключенное в квадратные скобки, определяет число элементов в массиве. Индексация элементов массива в языке C++ начинается с нуля. Таким образом, последний элемент массива имеет индекс на единицу меньше, чем число элементов массива. Во второй синтаксической форме константное выражение в квадратных скобках опущено. Эта форма может быть использована, если в объявлении массива присутствует инициализатор, либо массив объявляется как формальный параметр функции, либо данное объявление является ссылкой на объявление массива где-то в 48
другом месте программы. Однако для многомерного массива может быть опущена только первая размерность. Многомерный массив, или массив массивов, объявляется путем задания последовательности константных выражений в квадратных скобках, следующей за именем: <спецификация типа> <имя> [<константное выражение>] [<константное выражение>] ... ; Каждое константное выражение определяет количество элементов в данном измерении массива, поэтому объявление двумерного массива содержит два константных выражение, три трехмерных и т. д. Члены массива называют элементами. Массив в памяти располагается по строкам, поэтому доступ к элементам двумерного (многомерного массива) осуществляется таким образом: ID[0][0],…,ID[0][N2–1], … ID[N1–[0],…,ID[N1–1][N2–1], где ID — имя массива. Массив может состоять из элементов любого типа, кроме типа void и функций, т. е. элементы массива могут иметь базовый, перечислимый, структурный тип, быть объединением, указателем или массивом. Примеры объявления массивов int x[10]; // Одномерный массив из 10 целых чисел. Индексы меняются от 0 до 9. double y[2][10]; // Двумерный массив вещественных чисел из 2 строк и 10 столбцов. Инициализация одномерного и многомерного массива. Как и простые переменные, массивы могут быть инициализированы при объявлении. Инициализатор для объектов составных типов (каким является массив) состоит из списка инициализаторов, разделенных запятыми и заключенных в фигурные скобки. Каждый инициализатор в списке представляет собой либо константу соответствующего типа, либо, в свою очередь, список инициализаторов. Эта конструкция используется для инициализации многомерных массивов. 49
Наличие списка инициализаторов в объявлении массива позволяет не указывать число элементов по его первой размерности. В этом случае количество элементов в списке инициализаторов и определяет число элементов по первой размерности массива. Тем самым определяется размер памяти, необходимой для хранения массива. Число элементов по остальным размерностям массива, кроме первой, указывать обязательно. Если в списке инициализаторов меньше элементов, чем в массиве, то оставшиеся элементы неявно инициализируются нулевыми значениями. Если же число инициализаторов больше, чем требуется, то выдается сообщение об ошибке. Примеры инициализации массивов int a[3] = {0, 1, 2}; // Число инициализаторов равно числу элементов double b[5] = {0.1, 0.2, 0.3}; // Число инициализато‐ ров меньше числа элементов int c[] = {1, 2, 4, 8, 16}; // Число элементов массива определяется по числу инициализаторов int d[2][3] = {{0, 1, 2}, {3, 4, 5}}; // Инициализация двумерного массива. Массив состоит из двух строк, в каждой из которых по 3 элемента. Элементы первой стро‐ ки получают значения 0, 1 и 2, а второй — значения 3, 4 и 5. int e[3] = {0, 1, 2, 3}; // Ошибка — число инициализа‐ торов больше числа элементов Обратите внимание, что не существует присваивания массиву, соответствующего описанному выше способу инициализации. int a[3] = {0, 1, 2}; // Объявление и инициализация a = {0, 1, 2}; // Ошибка Передача массивов в функцию. Когда массив используется в качестве аргумента функции, передается только адрес массива, а не копия всего массива. При вызове функции с именем массива в функцию передается указатель на первый элемент массива (надо помнить, что в С имена массивов без индекса — это указатели на первый элемент массива.) Параметр должен иметь тип, совместимый с указателем. Имеется три способа объявления параметра, предназначенного для получения указателя на массив. Во-первых, он может быть объявлен как массив, как показано ниже: 50
void display(int num[10]) { int i; for (i=0; i<10; i++) cout << num[i]; } Хотя параметр num объявляется как целочисленный массив из десяти элементов, С автоматически преобразует его к целочисленному указателю, поскольку не существует параметра, который мог бы на самом деле принять весь массив. Передается только указатель на массив, поэтому должен быть параметр, способный принять его. Следующий способ состоит в объявлении параметра для указания на безразмерный массив, как показано ниже: void display(int num[]) { int i; for (i=0; i<10; i++) cout << num[i]; } где num объявлен как целочисленный массив неизвестного размера. Последний способ, которым может быть объявлен num, — через указатель, как показано ниже: void display(int *num) { int i; for (i=0; i<10; i++) cout << num[i]; } Двумерный массив. В объявлениях функции массивыаргументы представлены типом данных и размером. Вот объявление функции display(); void display(double[DISTRICTS][MONTHS]); Нa самом деле здесь есть один необязательный элемент. Следующее объявление работает так же: void display(double[][MONTHS]); Почему функции не нужно значение первой размерности? Для этого следует вспомнить, что двумерный массив — это массив массивов. Функция сначала рассматривает аргумент как массив отделов. Ей не важно знать, сколько отделов, но нужно знать, на51
сколько велики элементы, представляющие собой отделы, так как тогда она сможет вычислить, где находится каждый элемент (умножая количество байтов, приходящееся на один элемент, на индекс нужного элемента). Отсюда следует, что если мы объявили функцию с одномерным массивом в качестве аргумента, то нам не нужно указывать размер массива: void somefunc(int elem[]); Строки на основе char массива и класса string. В языке С++ есть два вида строк. Во-первых, в нем предусмотрены строки, завершающиеся нулевым байтом (null-terminated string), представляющие собой массивы символов, последним элементом которых является нулевой байт. Это единственный вид строки, предусмотренный в языке С. В языке С++ определен также класс string, который реализует объектно-ориентированный подход к обработке строк. Объявляя массив символов, следует зарезервировать одну ячейку для нулевого байта, т. е. указать размер, на единицу больше, чем длина наибольшей предполагаемой строки. Например, чтобы объявить массив str, предназначенный для хранения строки, состоящей из 10 символов, следует выполнить следующий оператор: char str[11]; В этом объявлении предусмотрена ячейка, в которой будет записан нулевой байт. Строка символов, заключенных в двойные кавычки, например, "Здравствуйте, я ваша тетя!", называется строковой константой (string constant). В языке C/C++ предусмотрен богатый выбор функций для работы со строками (табл. 6.1). Самыми распространенными среди них являются следующие. Таблица 6.1 Имя 1 Предназначение strcpy (si, s2) 2 Копирует строку s2 в строку si. strcat (si, s2) Приписывает строку s2 в конец строки si. 52
Окончание табл. 6.1 1 2 strlen(si) Вычисляет длину строки s2. strcmp(si,s2) Возвращает 0, если строки si и s2 совпадают, отрицательное значение, если sl<s2, и положительное значение, если sl>s2. strchr(si,ch) Возвращает указатель на позицию первого вхождения символа ch в строку si. strstr(si,s2) Возвращает указатель на позицию первого вхождения строки s2 в строку si. Эти функции объявлены в стандартном заголовочном файле string.h. В программах на языке С++ используется также заголовочный файл <cstring>. He следует путать нуль (о) и нулевой байт (\о), или нулевой символ. Признаком конца строки является именно нулевой байт. Если не указать обратную косую черту, нуль будет считаться обычным символом. Массивы строк используются довольно часто. Например, сервер базы данных может сравнивать команды, введенные пользователем, с массивом допустимых команд. Для того чтобы создать массив строк, завершающихся нулевым байтом, необходим двухмерный массив символов. Максимальное значение левого индекса задает количество строк, а правого индекса — максимальную длину каждой строки. Ниже в фрагменте объявлен массив, состоящий из 30 строк, каждая из которых может содержать 9 до 79 символов и последний байт, содержащий 0: char str_array[30] [80]; Пример массива строк: char star[DAYS][MAX] = { "Понедельник", "Вторник", "Среда", "Четверг", "Пятница", "Суббота", "Воскресенье" }; 53
Шаблонный класс string. В действительности, класс string представляет собой специализацию более общего шаблонного класса basic_string. Существует две специализации типа basic_string: • тип string, который поддерживает 8-битовые символьные строки; • тип wstring, который поддерживает строки, образованные двухбайтовыми символами. Существует три причины для включения в C++ стандартного класса string: • непротиворечивость данных (строка определяется самостоятельным типом данных); • удобство (программист может использовать стандартные С++ операторы); • безопасность (границы массивов отныне не будут нарушаться). Для использования строковых классов C++ необходимо включить в программу заголовок <string>. Прототипы трех самых распространенных конструкторов класса string имеют следующий вид: string(); string(const char *str); string(const string &str); Первая форма конструктора создает пустой объект класса string. Вторая форма создает string-объект из строки с завершающим нулем, адресуемой параметром str. Эта форма конструктора обеспечивает преобразование из строки с завершающим нулем в объект типа string. Третья создает string-объект из другого string-объекта. Для объектов класса string определены следующие операторы (табл. 6.2). Таблица 6.2 Оператор Описание 1 54 2 = Присваивание + Конкатенация
Окончание табл. 6.2 1 2 += Присваивание с конкатенацией == Равенство != Неравенство < Меньше <= Меньше или равно > Больше >= Больше или равно [] Индексация << Вывод >> Ввод Эти операторы позволяют использовать объекты типа string в обычных выражениях и позволяют избежать вызова таких функций, как strcpy() или strcat(). В общем случае, в выражениях можно смешивать string-объекты и строки с завершающим нулем. Оператор "+" можно использовать для конкатенации одного string-объекта с другим или string-объекта со строкой, созданной в C-стиле. Ввод и вывод осуществляются путем, схожим с применяемым для строкового типа. Операции << и >> перегружены для использования с объектами класса string, метод getline() принимает ввод, который может содержать пробелы или несколько строк. Доступ к отдельным символам объектов класса string можно получить разными способами. В следующем примере показан доступ с использованием метода at(). Можно также использовать перегруженную операцию [], которая позволяет рассматривать объект класса string как массив. Однако операция [] не предупредит вас, если вы попытаетесь получить доступ к символу, лежащему за пределами массива (например, после конца строки): string str; str (“Мы изучаем С++!”); cout << str.at(5); 55
Некоторые методы класса string представлены в табл. 6.3. Таблица 6.3 begin () Метод Возвращаемое значение Итератор, указывающий на первый символ в строке cbegin () Итератор const, указывающий на первый символ в строке (С++11) end О Итератор, указывающий на элемент, следующий за последним cendO Итератор const, указывающий на элемент, следующий за последним (С++11) rbegin () Обратный итератор, указывающий на элемент, следующий за последним crbegin () Обратный итератор const, указывающий на элемент, следующий за последним (С++11) rend() Обратный итератор, указывающий на первый символ crend () Обратный итератор const, указывающий на первый символ (С++11) size () Количество элементов в строке, равное расстоянию от begin о до end () length {) Тоже, что и size() capacity () Выделенное количество элементов в строке. Может быть больше действительного количества символов. Значение capacity () — size() представляет количество символов, которые могут быть присоединены к строке до того, как возникает необходимость в выделении большого количества памяти. max_size () Максимально допустимый размер строки data () Указатель типа const chart*, который указывает на первый элемент массива, чьи первые size () элементов равны соответствующим элементам в строке, управляемой *this. Указатель не должен считаться действительным после того, как был модифицирован сам объект string с_str() Указатель типа const charT*, который указывает на первый элемент массива, чьи первые size () элементов равны соответствующим элементам в строке, управляемой *this, и чей следующий элемент является символом charT (0) (маркер окончания строки) для типа charT. Указатель не должен считаться действительным после того, как был модифицирован сам объект string Getjallocator() Копия объекта allocator, который используется для распределения памяти для объекта string 56
ЛЕКЦИЯ 7. ПЕРЕГРУЗКА ОПЕРАЦИЙ И ПРЕОБРАЗОВАНИЕ ТИПОВ Перегрузка операций. Существует два основных способа перегрузки операторов: глобальные функции, дружественные для класса, или подставляемые функции самого класса. В большинстве случаев, операторы (кроме условных) возвращают объект, или ссылку на тип, к которому относятся его аргументы (если типы разные, нужно выбрать как интерпретировать результат вычисления оператора в конкретном случае). Синтаксис перегрузки операторов очень похож на определение функции с именем operator∆, где ∆ — это идентификатор оператора (+, -, <<, >>). Операторы "a->", "[]", "()", "=" и "(type)" можно переопределить только как методы класса. При перегрузке операторов ">>" и "<<", для ввода и вывода через потоки нужно подключить заголовочный файл iostream. Следующие операции не могут быть перегружены: операция доступа к членам структуры или класса (.), операция разрешения (::) и операция условия (?:). А также операция (->), с которой мы еще не сталкивались. Кроме того, нельзя создавать новые операции (например, нельзя определить новую операцию возведения в степень **, которая есть в некоторых языках) и пытаться их перегрузить; перегружать можно только существующие операции. Различные способы передачи аргументов в функции и возвращения значений операторов: • если аргумент не изменяется оператором, в случае, например, арифметических операторов, его следует передавать как ссылку на константу; • тип возвращаемого значения зависит от сути оператора. Если оператор должен возвращать новое значение, то необходимо 57
создавать новый объект (как в случае бинарного плюса). Чтобы запретить изменение объекта как l-value, нужно возвращать его константным; • для операторов присваивания необходимо возвращать ссылку на измененный элемент. Также, чтобы использовать оператор присваивания в конструкциях вида (x=y).func(), где функция func() вызывается для переменной x, после присваивания ей y, то вместо не возвращения ссылки на константу, нужно возвращать просто ссылку; • логические операторы должны возвращать в худшем случае int, а в лучшем bool. Перегрузка унарных операций. Унарный оператор — это оператор от одного параметра. Если он объявлен внутри класса, то этим параметром (неявным) является this. Перегрузка унарных операторов имеет следующий синтаксис (листинг 7.1): Листинг 7.1 тип‐имя_класса operator∆ (тип_параметра операнд) { // операции } Так выглядит переопределение префиксных и постфиксных операторов (листинг 7.2). Листинг 7.2 имя_класса operator ‐‐();//Префиксный имя_класса operator ‐‐( int);//Постфиксный Единственное отличие префиксной операции от постфиксной — ключевое слово int в списке аргументов. Но int — это не аргумент! Это слово говорит, что перегружается постфиксная операция. Пример перегрузки префиксной и постфиксной операции ++ представлен в листинге 7.3. 58
Листинг 7.3 // префиксная и постфиксная операции ++ для класса Counter #include <iostream> using namespace std; class Counter { private: unsigned int count; public: Counter() : count(0) { } Counter(int c) : count(c) { } unsigned int get_count() { return count; } Counter operator++() { return Counter(++count);} // создан временный безымян‐ ный объект, для его использования требуется конструк‐ тор с одним параметром Counter operator++(int) {return Counter(count++); } }; int main() { Counter c1, c2; // определяем переменные cout << "c1 = " << c1.get_count() << endl; cout << "c2 = " << c2.get_count() << endl; c2 = ++c1; // т.к. перегруженная операция префиксного инкремента возвращает объект типа Counter, ее можно использовать и как r‐value cout << "\nc1 = " << c1.get_count(); // снова показываем значения cout << "\nc2 = " << c2.get_count(); c2 = c1++; //т.к. перегруженная операция постфиксного инкремента возвращает объект типа Counter, ее тоже можно использовать и как r‐value cout << "\nc1 = " << c1.get_count(); // и снова cout << "\nc2 = " << c2.get_count() << endl; return 0; } 59
Перегрузка бинарных операций. Бинарный оператор — это функция от двух параметров, параметрами которой являются левый и правый операнды оператора. Перегрузка операций с двумя аргументами очень похожа на перегрузку унарных операций (листинг 7.4). Листинг 7.4 тип‐имя_класса operator∆(список аргументов) { // операции } В перегруженных бинарных операциях всегда вызывается метод левого операнда. Объект, стоящий справа от знака операции, должен быть передан в функцию в качестве аргумента. Пример перегрузки операций +, =+, < представлен ниже (листинг 7.5). Листинг 7.5 //перегрузка операций +, =+, < для переменных типа Rasst #include <iostream> using namespace std; class Rasst // класс русских мер длины 18 века { private: int arshin; int vershok; public: Rasst() : arshin(0), vershok(0) { } // конструктор без параметров Rasst(int ar, int ver) {// конструктор с двумя параметрами if(ver >= 16) {ver ‐= 16; ar++;} arshin = ar; vershok = ver; } void getras() {// получение информации от пользователя 60
cout << "\nВведите аршины: "; cin >> arshin; cout << "Введите вершки: "; cin >> vershok; } void showras() {// показ информации cout << arshin << "~" << vershok << endl; } Rasst operator+(Rasst) const;// сложение длин bool operator<(Rasst d2) const; //сравнение двух длин void operator+=(Rasst d2); //сложениие с присваивани‐ ем }; Rasst Rasst::operator+(Rasst d2) const { // сложение двух длин int a = arshin + d2.arshin; // складываем футы int v = vershok + d2.vershok; // складываем дюймы return Rasst(a, v); // создаем и возвращаем временную переменную } bool Rasst::operator<(Rasst d2) const {// сравнение двух длин float t1 = arshin + vershok / 16; float t2 = d2.arshin + d2.vershok / 16; return (t1 < t2) ? true : false; } void Rasst::operator+=(Rasst d2) { arshin += d2.arshin; vershok += d2.vershok; } int main() { Rasst ras1, ras3, ras4; // определяем переменные ras1.getras(); // получаем информацию Rasst ras2(11, 12); // переменная с конкретным значением ras3 = ras1 + ras2; // складываем две переменные ras4 = ras1 + ras2 + ras3; // складываем несколько переменных 61
cout << "ras1 результат cout << "ras2 cout << "ras3 cout << "ras4 return 0; } = "; ras1.showras();// показываем = "; ras2.showras(); = "; ras3.showras(); = "; ras4.showras(); Перегрузка операций индексации массива. Оператор индексирования [] всегда определяется как член класса и, так как под индексируемым объектом предполагается массив, ему следует возвращать ссылку. Способ создания безопасного массива, с встроенной проверкой индекса, за счет перегрузки операции [] представлен в листинге 7.6. Листинг 7.6 #include <iostream> using namespace std; const int LIMIT = 10; // размер массива class safearray { private: int arr[LIMIT]; public: int& operator[](int n) { // функция возвращает ссылку! if(n < 0 || n >= LIMIT) { cout << "\nОшибочный ин‐ декс!; return ‐1;} return arr[n]; } }; int main() { safearay sa1; for(int j = 0; j < LIMIT; j++) sa1[j] = j * 10; for(j = 0; j < LIMIT; j++) { int temp = sa1[j]; 62
cout << "Элемент " << j << " равен " << temp << endl; } return 0; } Преобразование типов. Преобразование типов близко к перегрузке операций. Некоторые преобразования происходят между основными типами и типами, определенными пользователем. В табл. 7.1 отражены возможные варианты преобразований. Таблица 7.1 Вид преобразования типа Основной → Основной Процедура Процедура в классе назначения в исходном классе Встроенные операции преобразования Основной → Класс Конструктор Класс → Основной Класс → Класс Операция преобразования Конструктор Операция преобразования Преобразование типов от основного к пользовательскому. Для перехода от основного типа к определенному пользователем типу используется конструктор с одним аргументом. Его иногда называют конструктором преобразования. Пример такого конструктора представлен в листинге 7.7. Листинг 7.7 #include <iostream> using namespace std; class Rasst // класс русских мер длины { private: const float artm = 0.7112F; //коэффициент перевода метров в аршины int arshin; int vershok; public: 63
Rasst() : arshin(0), vershok(0) { } // конструктор без параметров Rasst(float meters) {// конструктор с одним пара‐ метром, переводящий метры в аршины и вершки (конструк‐ тор преобразования) float f_arshin = meters / artm; // переводим в аршины arshin = int(f_arshin); // берем число полных аршинов vershok = 16 * (f_arshin — arshin); // остаток — это вершки } Rasst(int ar, int ver) {// конструктор с двумя параметрами if(ver >= 16) {ver ‐= 16; ar++;} arshin = ar; vershok = ver; } void getras() {// получение информации от пользователя cout << "\nВведите аршины: "; cin >> arshin; cout << "Введите вершки: "; cin >> vershok; } void showras() { cout << arshin << "~" << vershok << endl; } operator float() const// оператор для перевода аршинов в метры { float f_arshin = vershok / 16; // вершки в аршины f_arshin += static_cast<float>(arshin); // + целые аршины return f_arshin * artm; // переводим в метры } }; int main() { float mtrs; 64
Rasst ras1 = 2.35F; // используется конструктор, переводящий метры в аршины и вершки cout << "\nras1 = "; ras1.showras(); mtrs = static_cast<float>(ras1); // используется опе‐ ратор перевода в метры cout << "\nras1 = " << mtrs << " meters\n"; Rasst ras2(5, 19); // используеется конструктор с двумя параметрами mtrs = ras2; // используется неявный перевод типа cout << "\nras2 = " << mtrs << " meters\n"; // ras2 = mtrs; // а вот это ошибка — так делать нельзя return 0; } Преобразование типов от пользовательского к основному. Для преобразования от определенного пользователем типа к основному создается операция преобразования (листинг 7.8). Листинг 7.8 operator тип_в_который_преобразуем() { необходимые преобразования; return переменная_нового_типа; } Операция может быть вызвана явно (листинг 7.9). Листинг 7.9 переменная = static_cast<тип_в_который_преобразуем> (объект); или с помощью простого присваивания переменная = объект; В обоих случаях происходит преобразование объекта в его эквивалентное значение основного типа. Например, преобразование строк в объекты класса string и наоборот, как это представлено в листинге 7.10: 65
Листинг 7.10 // перевод обычных строк в класс String #include <iostream> using namespace std; #include <string.h> // для функций str* class String { private: enum { SZ = 80 }; // размер массива char str[SZ]; // массив для хранения строки public: String() { str[0] = '\x0'; } // конструктор без параметров String(char s[]) // конструктор с одним параметром { strcpy(str, s); } // сохраняем строку в массиве void display() const { cout << str; } // показ строки operator char*(){ return str; }// перевод строки к обычному типу }; int main() { String s1; // вызов конструктора без параметров char xstr[] = "Ура, товарищи! "; s1 = xstr; // неявный вызов конструктора с одним па‐ раметром s1.display(); String s2 = "Свобода!"; // снова вызов конструктор с параметром cout << static_cast<char*>(s2); //вызов оператора пе‐ ревода типа cout << endl; return 0; } Преобразование типов от пользовательского к пользовательскому. Для преобразования между объектами различных, оп66
ределенных пользователем классов применяются те же два метода преобразования, что и для преобразований между основными типами и объектами определенных пользователем классов. То есть можно использовать конструктор с одним аргументом или операцию преобразования. Выбор зависит от того, нужно ли записать функцию преобразования в классе для исходного объекта или для объекта назначения. Функция в исходном объекте. В листинге 7.11 функция преобразования расположена в исходном классе. В данном случае она реализована в виде операции преобразования. Листинг 7.11 #include <iostream> using namespace std; class RasstA // класс русских мер длины 18 века { private: int arshin; int vershok; public: RasstA() : arshin(0), vershok(0) { } // конструк‐ тор без параметров RasstA(int ar, int ver) {// конструктор с двумя параметрами if(ver >= 16) {ver ‐= 16; ar++; } arshin = ar; vershok = ver; } void getras() {// получение информации от пользо‐ вателя cout << "\nВведите аршины: "; cin >> arshin; cout << "Введите вершки: "; cin >> vershok; } void showras() { cout<<arshin<<"аршинов и "<< ver‐ shok<<" вершков\n;} }; class RasstB // класс русских мер длины 18 века { private: 67
int verst; int sajen; public: RasstB() : verst(0), sajen(0) { } // конструктор без параметров RasstB(int vers, int saj) {// конструктор с двумя параметрами if(saj >= 500) {saj ‐= 500; vers++; } verst = vers; sajen = saj; } void getras() {// получение информации от пользо‐ вателя cout << "\nВведите версты: "; cin >> verst; cout << "Введите сажни: "; cin >> sajen; } void showras() { cout << verst << "верст и " << sajen << " сажней "; } operator RasstA() const { // операция преоб‐ разования типа int saj = sajen + 500*verst; return RasstA(3*saj, 0); } }; int main() { RasstA ras1(5, 10); //используется конструктор с двумя параметрами RasstB ras2 (1, 2); //используется конструктор с двумя параметрами RasstA ras3 = ras2; //перевод систем единиц изме‐ рения __ras1.showras(); ras2.showras(); cout << " == "; ras3.showras(); return 0; } 68
Функция в объекте назначения. В листинге 7.7 это же преобразование выполняется функцией преобразования, которая находится в классе назначения. В этой ситуации используется конструктор с одним аргументом. Так как конструктор класса назначения должен иметь доступ к данным исходного класса для выполнения преобразования, нужно создать методы доступа к полям класса (листинг 7.12). Это методы getVer() и getSaj (). Листинг 7.12 #include <iostream> using namespace std; class RasstB // класс русских мер длины { private: int verst; int sajen; public: RasstB() : verst(0), sajen(0) { } // конструктор без параметров RasstB(int vers, int saj) {// конструктор с двумя па‐ раметрами if(saj >= 500) {saj ‐= 500; vers++; } verst = vers; sajen = saj; } void getras() {// получение информации от пользователя cout << "\nВведите версты: "; cin >> verst; cout << "Введите сажни: "; cin >> sajen; } void showras() { cout << verst <<"верст и "<< sajen <<" сажней "; } int getVer() { return verst;} // получить версты int getSaj() { return sajen;} // получить сажни }; class RasstA // класс русских мер длины { 69
private: int arshin; int vershok; public: RasstA() : arshin(0), vershok(0) { } // конструктор без параметров RasstA(RasstB); // конструктор с одним аргументом RasstA(int ar, int ver) {// конструктор с двумя пара‐ метрами if(ver >= 16) {ver ‐= 16; ar++; } arshin = ar; vershok = ver; } void getras() {// получение информации от пользователя cout << "\nВведите аршины: "; cin >> arshin; cout << "Введите вершки: "; cin >> vershok; } void showras() { cout << arshin << "аршинов и " << vershok <<" вершков " << endl; } }; RasstA::RasstA(RasstB ras) { //преобразовать RasstB в RasstA int ars = ( ras.getVer() * 500 + ras.getSaj() ) *3; arshin = ars; vershok = 0; } int main() { RasstA ras1(5, 10); // используется конструктор с двумя параметрами RasstB ras2 (1, 2); RasstA ras3 = ras2; ras1.showras(); ras2.showras(); cout << " == "; ras3.showras(); return 0; } 70
Предотвращение преобразования типа от основного к пользовательскому с помощью конструктора. В стандарт языка C++ включены ключевые слова explicit и mutable. Ключевое слово explicit относится к преобразованию типов, a mutable предназначено для изменения полей констант. Ключевое слово еxplicit используется для предотвращения неявного преобразования типа. Оно помещается перед объявлением конструктора с одним аргументом. Конструктор, объявленный с ключевым словом explicit, не может быть использован в ситуации неявного преобразования данных. В листинге 7.13 показано, как это выглядит. Листинг 7.13 #include <iostream> using namespace std; class Rasst // класс русских мер длины { private: const float artm = 0.7112F; //коэффициент перевода метров в аршины int arshin; int vershok; public: Rasst() : arshin(0), vershok(0) { } // конструктор без параметров explicit Rasst(float meters) {// конструктор преобра‐ зования float f_arshin = meters / artm; // переводим в аршины arshin = int(f_arshin); // берем число полных аршинов vershok = 16 * (f_arshin — arshin); // остаток — это вершки } Rasst(int ar, int ver) {// конструктор с двумя пара‐ метрами if(ver >= 16) {ver ‐= 16; ar++;} 71
arshin = ar; vershok = ver; } void getras() {// получение информации от пользовате‐ ля cout << "\nВведите аршины: "; cin >> arshin; cout << "Введите вершки: "; cin >> vershok; } void showras() { cout << arshin << "~" << vershok << endl; } }; int main() { void showInf(Rasst); // объявление Rasst ras1(2.35F); // конструктор с 1 аргументом // преобра‐ зовывает метры в Расстояние // Rasst dist1 = 2.35F; // ОШИБКА, если конструктор является явным(EXPLICIT) cout << "\nras1 = "; ras1.showras(); float mtrs = 3.0F; cout << "\nras1 "; //showInf(mtrs); // ОШИБКА, если конструк‐ тор является явным(EXPLICIT) return 0; } void showInf(Rasst r) { cout << "(в аршинах и вершках) = "; r.showras(); } Для создания объекта-константы, имеющего определенное поле, которое нужно будет изменять, несмотря на то, что сам объект 72
является константой, используется ключевое слово mutable. Данные, объявленные с этим ключевым словом, могут быть изменены, даже если их объект объявлен как const. Пример использования ключевого слова mutable представлен в листинге 7.14 Листинг 7.14 #include <iostream> #include <string> using namespace std; class scrollbar { private: int size; // релевантный для константы mutable string owner; // не релевантный для константы public: scrollbar(int sz, string own) : size(sz), owner(own) { } void setSize(int sz) // изменения размера { size = sz; } void setOwner(string own) const // изменения владелеца { owner = own; } int getSize() const { return size; } string getOwner() const { return owner; } }; // возвраты размера // возвраты владелеца int main() { const scrollbar sbar(60, "#1"); // sbar.setSize(100); // не может быть изменен в объекте‐константе sbar.setOwner("#2"); // может быть изменен даже если объект—константа cout << sbar.getSize() << ", " << sbar.getOwner() << endl; return 0; } 73
ЛЕКЦИЯ 8. НАСЛЕДОВАНИЕ Базовый и производный классы. Наследование — процесс создания новых классов, называемых наследниками или производными, из уже существующих, базовых классов. Производный класс получает все возможности базового класса, но может быть усовершенствован. Есть возможность использовать существующий код несколько раз. Рассмотрим основную форму наследования: сlass имя_производного_класса : режим_доступа имя_базового_класса Существуют три режима доступа: public (общий), private (приватный), protected (защищенный). Последний позволяет членам быть доступными методам своего и произвольного класса; при этом ограничивает доступ из функций, не принадлежащих этим классам. Если режим доступа отсутствует, то у производного класса class подразумевается private. Проиллюстрируем схемой возможность использования спецификаторов в различных ситуациях: к элементу в базовом private protected Public private protected public private protected public public protected private к элементу в производном классе Недоступен protected public Недоступен protected protected Недоступен private private Для объектов производного класса могут быть использованы методы базового класса. Это называется правами доступа. Если в 74
производном классе не определен конструктор, то будет использоваться подходящий конструктор базового класса. Кроме того, есть возможность использовать доступный метод взамен отсутствующего. Заметим, что наследование не работает в обратном направлении и базовому классу не доступны производные классы. Рассмотрим листинг 8.1, в котором показаны два класса Counter и CountDn. Листинг 8.1 #include <iostream> using namespace std; ////////////////////////////////////////////////////// class Counter //базовый класс { protected: unsigned int count; //счетчик public: Counter ( ) : count ( 0 ) //конструктор без аргументов { } Counter ( int c ) : count ( c ) { } unsigned int get_count ( ) const { return count; } // возвращает значение счетчика Counter operator++ ( ) //увеличивает значение //счетчика (префикс) { return Counter ( ++count ); } }; ////////////////////////////////////////////////////// class CountDn : public Counter//производный класс { public: Counter operator‐‐ ( ) //уменьшает значение счетчика { return Counter ( ‐‐count ); } }; ////////////////////////////////////////////////////// //// int main ( ) { CountDn c1; // объект с1 75
cout << "\n c1=" << c1.get_count ( ); //вывод на пе‐ чать ++c1; ++c1; ++c1; //увеличиваем c1 три раза cout << "\n c1=" << c1.get_count ( ); //вывод на пе‐ чать ‐‐c1; ‐‐c1; //уменьшаем c1 два раза cout << "\n c1=" << c1.get_count ( ); //вывод на пе‐ чать cout << endl; return 0; } Конструкторы производного класса. Для инициализации объекта производного класса нельзя воспользоваться конструктором базового класса (компилятор будет использовать только конструктор базового класса без аргументов). Нужно написать новый конструктор для производного класса. Рассмотрим листинг 8.2 Листинг 8.2 // counten2.cpp // конструкторы в производных классах #include <iostream> using namespace std; ////////////////////////////////////////////////////// class Counter { protected: unsigned int count; // счетчик public: Counter ( ) : count (0) // конструктор без параметров { } Counter ( int c ) : count ( c ) // конструктор с одним параметром { } unsigned int get_count ( ) const // получение значениЯ { return count; } Counter operator++ ( ) // оператор увеличениЯ { return Counter ( ++count ); } }; 76
////////////////////////////////////////////////////// class CountDn : public Counter //режим доступа при наследовании public: { public: CountDn ( ) : Counter ( ) // конструктор без парамет‐ ров { } CountDn ( int c ) : Counter ( c )// конструктор с од‐ ним параметром { } CountDn operator‐‐ ( ) // оператор уменьшениЯ { return CountDn ( ‐‐count ); } }; ////////////////////////////////////////////////////// int main ( ) { CountDn c1; // переменные класса CountDn CountDn c2 ( 100 ); cout << "\nc1 = " << c1.get_count ( ); // выводим значениЯ на экран cout << "\nc2 = " << c2.get_count ( ); ++c1; ++c1; ++c1; // увеличиваем c1 cout << "\nc1 = " << c1.get_count ( ); // показываем результат ‐‐c2; ‐‐c2; // уменьшаем c2 cout << "\nc2 = " << c2.get_count ( ); // показываем результат CountDn c3 = ‐‐c2; // создаем переменную c3 на основе c2 cout << "\nc3 = " << c3.get_count ( ); // показываем значение cout << endl; return 0; } В конструкторе CountDn() : Counter() {} появилась новая возможность: имя функции, следующее за двоеточием. Оно используется конструктором CountDn() для вызова конструктора Counter() 77
базового класса. При создании объекта производного класса вызовется конструктор CountDn() для его инициализации, который в свою очередь вызовет конструктор Counter() (кроме того, он может выполнять и свои операции). В строке: CountDn c2 ( 100 ); Функция main() использует конструктор класса CountDn с одним аргументом: CountDn ( int c ) : Counter ( c ) Такая конструкция означает, что аргумент будет передан от конструктора CountDn() в Counter(), где будет использован для инициализации объекта. Базовые методы класса. Для производного класса можно определять методы, имеющие такие же имена, как и у методов базового класса. В этом случае имеет место перегрузка функций. В листинге 8.3 рассмотрим пример с перегрузкой метода базового класса. Листинг 8.3 // перегрузка функций базового класса #include <iostream> using namespace std; #include <process.h> ////////////////////////////////////////////////////// class Stack { protected: enum { MAX = 3 }; // размер стека int st[ MAX ]; // данные, хранЯщиесЯ в стеке int top; // индекс последнего элемента в стеке public: Stack ( ) // конструктор { top = ‐1; } void push ( int var ) // помещение числа в стек { st[ ++top ] = var; } int pop ( ) // извлечение числа из стека { return st[ top‐‐ ]; } }; 78
////////////////////////////////////////////////////// class Stack2 : public Stack { public: void push ( int var ) // помещение числа в стек { if ( top >= MAX — 1 ) // если стек полон, то ошибка { cout << "\nЋшибка: стек полон"; exit ( 1 ); } Stack::push ( var ); // вызов функции push класса Stack } int pop ( ) // извлечение числа из стека { if ( top < 0 ) // если стек пуст, то ошибка { cout << "\nЋшибка: стек пуст\n"; exit ( 1 ); } return Stack::pop ( ); // вызов функции pop класса Stack } }; ////////////////////////////////////////////////////// int main ( ) { Stack2 s1; s1.push ( 11 ); s1.push ( 22 ); s1.push ( 33 ); cout << endl << cout << endl << cout << endl << cout << endl << нет cout << endl; return 0; } // поместим в стек несколько чисел s1.pop s1.pop s1.pop s1.pop ( ( ( ( ); // заберем числа из стека ); ); ); // ой, а данных‐то больше В программе стоит обратить внимание на два метода push() и pop(). Эти методы имеют те же иена, аргументы и возвращаемые 79
значения, что и методы базового класса. В случае, если один метод существует и в базовом, и в производном классах, то будет выполнен метод производного класса — метод производного класса перегружает метод базового. Методы производного класса получают доступ к методам базового класса, используя операцию разрешения (::). Эта операция позволяет точно определить, к какому классу относится метод. Иерархия классов. Рассмотрим в качестве примера базу данных служащих некоторой компании. В ней существует три категории служащих: менеджеры, ученые, рабочие. В базе данных хранятся имена и номера всех служащих. А кроме того, должности и взносы в гольф-клуб и публикации ученых. Таким образом, программа начинается с описания базового класса employee. Этот класс содержит фамилии и номера служащих. Он порождает три новых класса: laborer, manager, scientist. Последние два содержат добавочнчую информацию (листинг 8.4). Работник Имя Номер Менеджер Ученый Должность Публикаци Публикаци и Рабочий Рис. 8.1. Пример иерархии классов Листинг 8.4 // пример написаниЯ базы данных сотрудников с использованием наследованиЯ #include <iostream> using namespace std; const int LEN = 80; // максимальнаЯ длина имени ////////////////////////////////////////////////////// 80
class employee // некий сотрудник { private: char name[ LEN ]; // имЯ сотрудника unsigned long number; // номер сотрудника public: void getdata ( ) { cout << "\n Введите фамилию: "; cin >> name; cout << " Введите номер: "; cin >> number; } void putdata ( ) const { cout << "\n ”амилиЯ: " << name; cout << "\n Ќомер: " << number; }}; ////////////////////////////////////////////////////// class manager : public employee // менеджер { private: char title[ LEN ]; // должность, например вице‐президент double dues; // сумма взносов в гольф‐клуб public: void getdata ( ){ employee::getdata ( ); cout << " ‚ведите должность: "; cin >> title; cout << " Введите сумму взносов в гольф‐клуб: "; cin >> dues; } void putdata ( ) const { employee::putdata ( ); cout << "\n Должность: " << title; cout << "\n Сумма взносов в гольф‐ клуб: " << dues; }}; ////////////////////////////////////////////////////// 81
class scientist : public employee // ученый { private: int pubs; // количество публикаций public: void getdata ( ) { employee::getdata ( ); cout << " ‚ведите количество публикаций: "; cin >> pubs; } void putdata ( ) const { employee::putdata ( ); cout << "\n Љоличество публикаций: " << pubs; }}; ////////////////////////////////////////////////////// class laborer : public employee // рабочий{}; ////////////////////////////////////////////////////// int main ( ) { manager m1, m2; scientist s1; laborer l1; // введем информацию о нескольких сотрудниках cout << endl; cout << "\n‚вод информации о первом менеджере"; m1.getdata ( ); cout << "\n‚вод информации о втором менеджере"; m2.getdata ( ); cout << "\n‚вод информации о первом ученом"; s1.getdata ( ); cout << "\n‚вод информации о первом рабочем"; l1.getdata ( ); // выведем полученную информацию на экран cout << "\n€нформациЯ о первом менеджере"; m1.putdata ( ); cout << "\n€нформациЯ о втором менеджере"; m2.putdata ( ); cout << "\n€нформациЯ о первом ученом"; s1.putdata ( ); 82
cout << "\n€нформациЯ l1.putdata ( ); cout << endl; return 0; о первом рабочем"; } Заметим, что мы не определяли объекты класса employee. Мы использовали его как общий класс, единственной целью которого было стать базовым для производных классов. Ни в базовом, ни в производном классах нет конструкторов, поэтому компилятор, наталкиваясь на определение типа manager m1, m2; использует конструктор, установленный по умолчанию для класса manager — вызывающий конструктор класса employee. Общее и частное наследование. Один из способов точного регулирования доступа к членам класса — объявление производного класса. Мы рассматривали пример, в котором использовалось объявление типа: class manager : public employee. Ключевое слово public определет, что объект производного класса может иметь доступ к методам базового класса, объявленным как public. Альтернативой является ключевое слово private. При его использовании для объектов производного класса нет доступа к методам базового класса, объявленным как public. Поскольку для объектов нет доступа к членам базового класса, объявленным как private или protected, то результатом будет то, что для объектов производных классов не будет доступа ни к одному из членов базового класса. Изучим пример (листинг 8.5). Листинг 8.5 #include <iostream> using namespace std; ////////////////////////////////////////////////////// class A // базовый класс { private: // тип доступа к данным совпадает с типом int privdataA; // доступа к функциЯм protected: int protdataA; 83
public: int pubdataA; }; ////////////////////////////////////////////////////// class B : public A // public наследование { public: void funct ( ) { int a; a = privdataA; // ошибка, нет доступа a = protdataA; // так можно a = pubdataA; // так можно }}; ////////////////////////////////////////////////////// class C : private A // private наследование { public: void funct ( ) { int a; a = privdataA; // ошибка, нет доступа a = protdataA; // так можно a = pubdataA; // так можно } }; ////////////////////////////////////////////////////// int main ( ) { int a; B objB; a = objB.privdataA; // ошибка, нет доступа a = objB.protdataA; // ошибка, нет доступа a = objB.pubdataA; // так можно C objC; a = objC.privdataA; // ошибка, нет доступа a = objC.protdataA; // ошибка, нет доступа a = objC.pubdataA; // ошибка, нет доступа return 0; } 84
В программе описан класс А, имеющий данные со спецификаторами доступа private, public, protected. Классы B и С являются производными классами. Класс B — общий наследник от класса А, класс С — частный. Объекты общего наследника класса B имеют доступ к членам класса А, объявленным как public или protected, а объекты частного наследника класса C имеют доступ только к членам, объявленным как public (рис. 8.2). private protected public class B class C private public A class A A private ObjA Запрещено private protected protected public public B ObjB C ObjC Рис. 8.2. Частное и общее наследование Уровни наследования. Производные классы могут являться базовыми классами для других производных классов: class A {}; class B : class A {}; class C : class B {}; 85
Здесь класс B является производным класса A, a класс С является производным класса B. Результат иерархии классов обобщение схожих характеристик. Чем более общим является класс, тем выше он находится в схеме. Множественное наследование. Класс может быть производным не только от одного базового класса, а от многих. Этот случай называется множественным наследованием. Форма наследования в этом случае следующая: class имя_производного_класса: список базовых классов{ //…. }; Список базовых классов содержит перечисленные через запятую базовые классы с соответствующими режимами доступа к каждому из базовых классов (рис. 8.3). A B C Рис. 8.3. Диаграмма классов при множественном наследовании Пока конструкторы базовых классов не имеют аргументов, то производный класс может не иметь функцию-конструктор. Если же конструтор базового класса имеет один или несколько аргументов, каждый производный класс обязан иметь конструктор. Чтобы передать аргументы в базовый класс, нужно определить их после объявления конструктора базового класса: конструктор_производного_класса(список аргументов): базовый класс1(список аргументов){} … базовый классN(список аргументов){} 86
Здесь базовый класс1….базовый классN — это имена конструкторов базовых классов, которые наследуются производным классом. Двоеточие отделяет имя конструктора производного класса от списка аргументов базового класса. Список аргументов, ассоциированный с базовым классом, может состоять из констант, глобальных параметров. Так как объект инициализируется во время выполнения программы, можно в качестве параметров использовать переменные. Неопределенность при множественном наследовании. В определенных ситуациях могут возникнуть некоторые проблемы, связанные со множественным наследованием. Допустим, что в обоих классах существуют методы с одинаковыми именами, а в производном классе метода с таким именем нет. Как в этом случае объект производного класса определит, какой из методов базовых классов выбрать? Проблема решается путем использования оператора разрешения, определющего класс, в котором находится метод. Процесс направления к версии метода конкретного класса называется устранением неоднозначности. Рассмотрим листинг 8.6. Листинг 8.6 #include <iostream> using namespace std; ////////////////////////////////////////////////////// class A { public: void show ( ) { cout << "Љласс A\n"; } }; class B { public: void show ( ) { cout << "Љласс B\n"; } }; class C : public A, public B { }; ////////////////////////////////////////////////////// 87
int main ( ) { C objC; // объект // objC.show ( ); не скомпилируетсЯ objC.A::show ( ); objC.B::show ( ); return 0; } класса C // так делать нельзЯ — программа // так можно // так можно Другой вид неопределенности возникает, если мы создаем производный класс от двух базовых классов, которые, в свою очередь, являются производными одного класса. Это создает дерево наследования в форме ромба (листинг 8.7). Листинг 8.7 #include <iostream> using namespace std; ////////////////////////////////////////////////////// class A { public: void func ( ){cout << "A";}; }; class B : public A { }; class C : public A { }; class D : public B, public C { }; ////////////////////////////////////////////////////// int main ( ) { D objD; objD.func ( ); // неоднозначность: программа не ском‐ пилируетсЯ objD.B::func ( ); //так можно return 0; } 88
Классы В и С являются производными класса А, а класс D — производный классов В и С. Трудности начинаются, когда объект класса D пытается воспользоваться методом класса А. В этом примере объект D использует метод func(). Однако классы В и С содержат копии метода func(), унаследованные от класса А. Компилятор не может решить, какой из методов использовать, и сообщает об ошибке. Включение: классы в классах. Механизмы включения и наследования являются формами взаимоотношений между классами. Поэтому полезно их сравнить. Если класс B является производным класса А, то мы говорим, что класс B имеет все характеристики класса А и, кроме того, свои собственные. Точно также мы можем сказать, что скворец это птица, так как имеет признаки, характерные для птиц, но при этом и свои собственные отличительные признаки. Поэтому наследование часто называют взаимоотношением. Включение называют взаимоотношением типа «имеет». Мы говорим, что библиотека имеет книги, накладная имеет строки. Это взаимоотношение типа «часть целого»: книга — часть библиотеки. В ООП включение появляется, когда один объект является атрибутом другого объекта. Рассмотрим случай , когда А — атрибут класса В: class A{}; class В{ A objA;}; В диаграммах UML включение считается специальным видом объединения. Если класс А содержит объект класса В и превосходит класс В по организации, то это и есть включение. Например, коллекция марок имеет включение марок. В листинге 8.8 использовано включение вместо наследования: Листинг 8.8 #include <iostream> #include <string> using namespace std; ////////////////////////////////////////////////////// 89
class student { private: string school; string degree; public: void getedu ( ) { cout << " ‚ведите название учебного заведениЯ: "; cin >> school; cout << " ‚ведите уровень образованиЯ\n"; cout << " (неполное высшее, бакалавр, магистр, кандидат наук): "; cin >> degree; } void putedu ( ) const { cout << "\n “чебное заведение: " << school; cout << "\n ‘тепень: " << degree; } }; ////////////////////////////////////////////////////// class employee { private: string name; unsigned long number; public: void getdata ( ) { cout << "\n ‚ведите фамилию: "; cin >> name; cout << " ‚ведите номер: "; cin >> number; } void putdata ( ) const { cout << "\n ”амилиЯ: " << name; cout << "\n Ќомер: " << number; } }; ////////////////////////////////////////////////////// 90
class manager { private: string title; double dues; employee emp; student stu; public: void getdata ( ) { emp.getdata ( ); cout << " ‚ведите должность: "; cin >> title; cout << " ‚ведите сумму взносов в гольф‐клуб: "; cin >> dues; stu.getedu ( ); } void putdata ( ) const { emp.putdata ( ); cout << "\n „олжность: " << title; cout << "\n ‘умма взносов в гольф‐клуб: " << dues; stu.putedu ( ); } }; ////////////////////////////////////////////////////// class scientist { private: int pubs; employee emp; student stu; public: void getdata ( ) { emp.getdata ( ); cout << " ‚ведите количество публикаций: "; cin >> pubs; stu.getedu ( ); } 91
void putdata ( ) const { emp.putdata ( ); cout << "\n Љоличество публикаций: " << pubs; stu.putedu ( ); } }; ////////////////////////////////////////////////////// class laborer { private: employee emp; public: void getdata ( ) { emp.getdata ( ); } void putdata ( ) const { emp.putdata ( ); } }; ////////////////////////////////////////////////////// int main ( ) { manager m1; scientist s1, s2; laborer l1; // введем информацию о нескольких сотрудниках cout << endl; cout << "\n‚вод информации о первом менеджере"; m1.getdata ( ); cout << "\n‚вод информации о первом ученом"; s1.getdata ( ); cout << "\n‚вод информации о втором ученом"; s2.getdata ( ); cout << "\n‚вод информации о первом рабочем"; l1.getdata ( ); // выведем полученную информацию на экран 92
cout << "\n€нформациЯ о первом менеджере"; m1.putdata ( ); cout << "\n€нформациЯ о первом ученом"; s1.putdata ( ); cout << "\n€нформациЯ о втором ученом"; s2.putdata ( ); cout << "\n€нформациЯ о первом рабочем"; l1.putdata ( ); cout << endl; return 0; } Более сложная форма объединения — композиция. Она обладает всеми его свойствами, но ее часть может принадлежать только одному целому и время жизни части то же, что и целого. Например, машина имеет двери (помимо других деталей). Двери не могут принадлежать другой машине, они являются ее неотъемлемой частью. Если включение — это взаимоотношение типа «имеет», то композиция — «состоит из». 93
9. УКАЗАТЕЛИ Адреса и указатели. Указатель — это переменная, значением которой является адрес некоторого объекта (обычно другой переменной) в памяти компьютера. Подобно тому, как переменная типа char имеет в качестве значения символ, а переменная типа int — целочисленное значение, переменная типа указателя имеет в качестве значения адрес ячейки оперативной памяти. Допустимые значения для переменной-указателя — множество адресов оперативной памяти компьютера. Понятие указателя тесно связано с понятием адреса переменной. В С++ существует специальная операция, позволяющая получить адрес переменной. &p — получение адреса, где p — идентификатор переменной. Результатом операции является адрес переменной p. Понятие переменной типа «указатель» также связано с операцией косвенной адресации*, называемой еще операцией разыменования, которая имеет структуру: *р — разыменование, где р — идентификатор переменной-указателя. Эта запись означает, что в ячейку с адресом, записанным в переменную р, помещено значение некоторой величины: n=32; p=&n; /* p–адрес ячейки, куда записано n */ v=*p; В результате выполнения этих действий в переменную v будет помещено число 32. Указатели и массивы. В языке C массивы и указатели тесно связаны друг с другом. Например, когда объявляется массив в виде int a[25]; то при этом не только выделяется память для 25 элементов массива, но и формируется указатель с именем a, значение которого равно адресу первого по счету (нулевого) элемента массива. Дос94
туп к элементам массива может осуществляться через указатель с именем a. С точки зрения синтаксиса языка указатель a является константой, значение которой можно использовать в выражениях, но изменить это значение нельзя. Поскольку имя массива является указателем-константой, допустимо, например, такое присваивание: int a[25]; int *ptr; ptr=a; Также справедливы следующие соотношения: например, имеется массив a[N], тогда истинными будут следующие сравнения: a==&a[0]; *a==a[0]. Указатели можно увеличивать или уменьшать на целое число: ptr=a+1; Теперь указатель ptr будет указывать на второй элемент массива a, что эквивалентно &a[1]. К указателям типа void арифметические операции применять нельзя, так как им не ставится в соответствие размер области памяти. Таким образом, в языке C++ для доступа к элементам массива существует два различных способа. Первый способ связан с использованием обычных индексных выражений в квадратных скобках, например, a[7] = 3 или a[i+2] = 5. Второй способ доступа к элементам массива связан с использованием адресных выражений и операции косвенной адресации в форме *(a+3) = 10 или *(a+i+2) = 5. При реализации на компьютере первый способ приводится ко второму. Для приведенных примеров обращение к элементу массива a[3] преобразуется в *(a+3). Указатели и функции. Использование указателей в качестве формальных параметров функции дает возможность передавать из вызывающей подпрограммы в функцию не само значение или массив значений, а адрес переменной (адреса фактических аргументов). 95
В частности, при работе с массивами не требуется использовать в качестве формальных аргументов массив. Это может быть указатель того же типа, что и тип элемента массива. В подпрограмму в этом случае можно передавать адрес начального элемента массива (листинг 9.1). Листинг 9.1 # include <iostream> # define N 3 int max(int k,int* b) //b — указательна целое, //k — количество элементов в массиве { int i,m1; m1=*b; //*b — значение 1‐го эл‐та массива (с индексом 0) for(i=1;i<k;i++) { b++;//Переход к следующему элементу массива if (m1<*b)m1=*b; //*b — значение текущего эл‐та массива } return(m1); } int main() { static int A[N]={1,7,3}; cout << endl << “max=” << max(N,&A[0]; return 0; } Управление памятью (new, delete). Основные отличия между статическим и динамическим выделением памяти следующие: • статические объекты обозначаются именованными переменными, и действия над этими объектами производятся напрямую, с использованием их имен. Динамические объекты не имеют собственных имен, и действия над ними производятся косвенно, с помощью указателей; • выделение и освобождение памяти под статические объекты производится компилятором автоматически. Программисту не нужно самому заботиться об этом. Выделение и освобождение 96
памяти под динамические объекты целиком и полностью возлагается на программиста. Это достаточно сложная задача, при решении которой легко наделать ошибок. Для манипуляции динамически выделяемой памятью служат операторы new и delete. Оператор new имеет две формы. Первая форма выделяет память под единичный объект определенного типа: int *pint = new int(1024); Здесь оператор new выделяет память под безымянный объект типа int, инициализирует его значением 1024 и возвращает адрес созданного объекта. Этот адрес используется для инициализации указателя pint. Вторая форма оператора new выделяет память под массив заданного размера, состоящий из элементов определенного типа: int *pia = new int[4]; В этом примере память выделяется под массив из четырех элементов типа int. Данная форма оператора new не позволяет инициализировать элементы массива. Обе формы оператора new возвращают одинаковый указатель, в нашем примере это указатель на целое. И pint, и pia объявлены совершенно одинаково, однако pint указывает на единственный объект типа int, а pia — на первый элемент массива из четырех объектов типа int. Когда динамический объект больше не нужен, мы должны явным образом освободить отведенную под него память. Это делается с помощью оператора delete, имеющего, как и new, две формы — для единичного объекта, и для массива: // освобождение единичного объекта delete pint; // освобождение массива delete[] pia; Что случится, если мы забудем освободить выделенную память? Память будет расходоваться впустую, она окажется неиспользуемой, однако возвратить ее системе нельзя, поскольку на 97
нее нет указателя. Такое явление получило специальное название утечка памяти. Указатели и строки. Строка — это серия символов, сохраненная в расположенных последовательно байтах памяти. В С++ доступны два способа работы со строками: строки в стиле С (унаследован от С); строки, основан‐ ные на библиотечном классе string. Рассмотрим только первый способ. Серия символов, сохраняемая в последовательных байтах, предполагает хранение строки с массиве типа char, где каждый элемент содержится в отдельном элементе массива. Главная особенность строк в стиле С: последним в каждой строке является нулевой символ. Этот символ, записываемый как \0, представляет собой символ ANCII-кодом 0, который служит меткой конца строки: сhar dog[8] = {‘b’, ‘e’, ‘a’, ‘u’, ‘x’, ‘ ’, ‘I’, ‘I’}; // это не строка!!! char cat[8] = {‘f’, ‘a’, ‘t’, ‘e’, ‘s’, ‘s’, ‘a’, ‘\0’}; // а это — строка Существует более простой способ инициализации массива. Для этого нужно использовать строку в двойных кавычках, которая называется строковой константой или строковым литералом. char bird[11] = “Mr. Cheeps”; // наличие символа \0 подразумевается char fish[ ] = “Bubbles”; // позволяет компилятору подсчитать количество элементов Инициализация символьного массива строковой константой — это один из тех случаев, когда безопаснее поручить компилятору подсчет количества элементов в массиве. Если сделать массив больше строки, никаких проблем не возникнет — только непроизводительный расход пространства. Стоит обратить внимание, что строковая константа (в двойных кавычках) не взаимозаменяема с символьной константой (в одинарных кавычках). Символьная константа, такая как ‘S’, представляет собой сокращенное обозначение для кода символа. В системе ANCII константа ‘S’ — это просто другой способ записи кода 83. 98
Поэтому следующий оператор присваивает значение 83 переменной shirt_size: char shirt_size = ‘S’; // нормально “S” — это строка, состоящая из двух символов — S и \0. Кроме того, “S” представляет адрес памяти, по которому перемещается строка. А значит, ниже приведенный пример неправильный: char shirt_size = “S”; // не допускает по причине не‐ соответствия типов Специальное соотношение между указателями и строками расширяют строки в стиле С. Рассмотрим следующий код: char flower[10] = “rose”; cout << flower << “s are red\n”; Имя массива — это адрес его первого элемента, потому flower в операторе cout представляет адрес элемента char, содержащего символ r. Главное здесь то, что flower трактуется как адрес значения char. Это предполагает, что можно использовать переменную — указатель на char в качестве аргумента cout, потому что она тоже содержит адрес cout. “s are red\n” — тоже адрес первого элемента строки “s are red\n”. Изначально передается первый адрес первого элемента строки, после чего cout начинает выводить все остальное, пока не попадется символ \0. В листинге 9.2 иллюстрируются различные формы строк. Листинг 9.2 #include <iostream> #include <cstring> // объявление strlen(), strcpy() using namespace std; int main () { char animal[20] = “bear”; // animal содержит bear const char * bird = “wren”; // bird содержит адрес строки char *ps; // не инициализированно cout << animal << “ and”;// отображение bear cout << bird << “\n”;// отображение wren 99
cout << “Enter a kind of animal: ”;// cin >> animal;// нормально, если вводится меньше 20 символов ps = animal; // установка ps в указатель на строку cout << ps << “!\n”;// нормально; то же, что и приме‐ нение animal cout << “before using strcpy():\n”; cout << animal << “ at” << (int*)animal << endl; cout << ps << “at” << (int*)ps << endl; ps = new char [strlen(animal)+1];// получение нового хранилища strcpy(ps, animal);// копирование строки в новое хра‐ нилище cout << “After using strcpy():\n”; cout << animal << “ at” << (int*)animal << endl; cout << ps << “ at” << (int*)ps << endl; delete [] ps; return 0; } Указатели на объекты. В языке С можно получить доступ к структуре непосредственно или с использованием указателей на эту структуру. Аналогичным образом в С++ можно ссылаться на объект. Для доступа к членам объекта через сам объект используется оператор «точка» (.). Если же применяется указатель на объект, тогда необходимо использовать оператор «стрелка» (—>). Использование операторов «точка» и «стрелка» аналогично их использованию для структур и объединений. Указатель на объект объявляется с использованием того же синтаксиса, что и указатели на данные других типов. В следующей программе создается простой класс с именем P_example и определяется объект этого класса ob, а также указатель р на объект P_example. Ниже проиллюстрировано, как получить доступ к объекту ob непосредственно и опосредованно с использованием указателя (листинг 9.3). Листинг 9.3 #include <iostream> using namespace std; 100
class P_example { int num; public: void set_num(int val) {num = val; } void show_num(); }; void P_example::show_num() { cout << num << " \n"; } int main() { P_example ob, *p; // объявление объекта и указателя на него ob.set_num(1); // прямой доступ к ob ob.show_num(); р = &ob; // присвоение р адреса ob p‐>show_num(); // доступ к ob с помощью указателя return 0; } Связный список представляет собой более гибкую систему для хранения данных, в которой вовсе не используются массивы. Вместо них мы получаем для каждого элемента память, применяя операцию new, а затем соединяем (или связываем) элементы данных между собой, с помощью указателей. Элементы списка не обязаны храниться в смежных участках памяти, как это происходит с массивами; они могут быть разбросаны по всему пространству памяти (рис. 9.1). Рис. 9.1. Связанный список 101
Листинг 9.4 // linklist.cpp // список #include <iostream> using namespace std; ////////////////////////////////////////////////////// struct link // один элемент списка { int data; // некоторые данные link* next; // указатель на следующую структуру }; ////////////////////////////////////////////////////// class linklist // список { private: link* first; public: linklist() // конструктор без параметров { first = NULL; } // первого элемента пока нет void additem(int d); // добавление элемента void display(); // показ данных }; ////////////////////////////////////////////////////// void linklist::additem(int d) // добавление элемента { link* newlink = new link; // выделяем память newlink‐>data = d; // запоминаем данные newlink‐>next = first; // запоминаем значение first first = newlink; // first теперь указывает на новый элемент } ////////////////////////////////////////////////////// void linklist::display() { link* current = first; // на‐ чинаем с первого элемента while(current) // пока есть данные 102
{ cout << current‐>data << endl; // печатаем данные current = current‐>next; // двигаемся к следующему элементу } } ////////////////////////////////////////////////////// int main() { linklist li; // создаем переменную‐список li.additem(25); // добавляем туда несколько чисел li.additem(36); li.additem(49); li.additem(64); li.display(); // показываем список return 0; } Класс linklist имеет только одно поле: указатель на начало списка. При создании списка конструктор инициализирует этот указатель, именованный как first, значением NULL, которое аналогично значению 0. Это значение является признаком того, что указатель указывает на адрес, который точно не может содержать полезной информации. В нашей программе элемент, указатель которого на следующий элемент имеет значение NULL, является конечным элементом списка. Добавление новых элементов в список. Функция additem() позволяет добавить новый элемент в связный список. Новый элемент помещается в начало списка (новый элемент может быть и в конце списка, но это будет более сложным примером). Рассмотрим последовательно действия при вставке нового элемента. Сначала мы создаем новый элемент типа link в строке link* newlink = new link; 103
Таким образом, выделяем память для нового элемента с помощью операции new и сохраняем указатель на него в переменной newlink. Затем заполняем элемент нужным нам значением. При этом действия со структурой похожи на действия с классами; к элементам структуры можно получить доступ, используя операцию ->. В следующих двух строках программы присваиваем переменной data значение, переданное как аргумент функции additem(), а указателю на следующий элемент присваиваем значение, хранящееся в указателе first. Этот указатель содержит адрес начала списка: newlink‐>data = d; newlink‐>next = first; И в заключение присваиваем указателю first значение указателя на новый элемент списка: first = newlink; Смыслом всех этих действий является замена адреса, содержащегося в указателе first, на адрес нового элемента, при этом старый адрес указателя first превратится в адрес второго элемента списка. Этот процесс иллюстрирует рис. 9.2. Рис. 9.2. Вставка нового элемента в связный список 104
Результатом работы будет последовательность чисел: 64; 49; 36; 25. Указатели на указатели. В случае обычных указателей, указатель содержит адрес некоторого участка памяти, содержащего некоторое значение. В случае указателя на указатель, первый указатель содержит адрес второго, который, в свою очередь, содержит адрес участка памяти, содержащего некоторое значение. Многочисленное перенаправление может и дальше расширяться. Но существует немного случаев, когда необходимо что-то более мощное, чем указатель на указатель. Излишнее перенаправленние приводит к концептуальным ошибкам, которые очень трудно исправлять. Переменная, являющаяся указателем на указателе, должна быть описана следующим образом. Две звездочки помещаются перед именем. Например, следующее объявление сообщается компилятору — newbalance — это указатель на указатель типа float: float **newbalance; Важно понимать, что newbalance — это не указатель на число с плавающей точкой, а указатель на указатель на вещественное число. Для получения доступа к целевому значению, косвенно указываемому указателем на указатель, следует применить оператор * два раза, как показано в листинге 9.5. Листинг 9.5 #include <iostream> int main() { int x, *p, **q; x = 10; p = &x; q = &p; cout << **q << endl; /* вывод значения x */ return 0; } 105
После объявления указателя и до первого присвоения ему значения, указатель может содержать неизвестное значение. Если попытаться использовать указатель до сообщения ему значения, можно нарушить работу не только программы, но и всей операционной системы. По существующим соглашениям неиспользуемый указатель должен содержать нулевое значение; то, что указатель имеет нулевое значение, не обеспечивает безопасности. Если использовать нулевой указатель слева от оператора присваивания, это может привести к «зависанию» программы. 106
ЛЕКЦИЯ 10. ВИРТУАЛЬНЫЕ И ДРУЖЕСТВЕННЫЕ ФУНКЦИИ Полиморфизм — термин, используемый для описания процесса, при котором различные реализации функций могут быть доступны с использованием одного имени. По этой причине полиморфизм характеризуют одной фразой — «один интерфейс, много методов». Это означает, что основной класс операций может быть оформлен в одном стиле, хотя конкретные действия могут быть различны. В С++ полиморфизм поддерживается и во время компиляции — перегрузка функций и операторов, и во время выполнения программы — использование указателей на базовые классы и виртуальных функций. Виртуальные функции — это функции, которые объявляются с использованием ключевого слова virtual в базовом классе и переопределяются в одном или нескольких производных классах. При этом прототипы функций в разных классах одинаковы. Если типы функций различны, то механизм виртуальности для них не включается. Если функции, объявленные виртуальными, отличаются только типом возвращаемого значения, это является ошибкой. Особенность использования виртуальных функций состоит в том, что при вызове функции, объявленной виртуальной, через указатель на базовый тип, во время выполнения программы определяется, какая виртуальная функция будет вызвана, в зависимости от того, на объект какого класса будет указывать указатель. Таким образом, когда указателю базового класса присвоены адреса объектов различных производных классов, выполняются различные версии виртуальных функций. Вследствие запретов и различий между перегрузкой обычных функций и перегрузкой виртуальных функций для описания переопределения последних используется термин «замещение». 107
Если в производном классе функция не замещает виртуальную, так как она имеет другой прототип или не объявлена, то вызывается функция базового класса. Вызов виртуальной функции обычно реализуется не как прямой вызов по таблице виртуальных функций класса. Эта таблица создается компилятором во время компиляции, а связывание происходит во время выполнения. Такой подход называется поздним или динамическим связыванием. Выбор функций в обычном порядке, во время компиляции, называется ранним или статическим связыванием. Абстрактные классы и чистые виртуальные функции. Базовый класс, объекты которого никогда не будут реализованы, называется абстрактным классом. Такой класс может существовать с единственной целью — быть родительским по отношению к производным классам, объекты которых будут реализованы. Также он может служить звеном для создания иерархической структуры классов. Чистая виртуальная функция — это функция, после объявления которой добавлено выражение = 0. Введя в класс такую функцию, мы программно защищаем объекты базового класса от реализации. Знак равенства не имеет ничего общего с операцией присваивания. Конструкция = 0 сообщает компилятору, что функция будет чисто виртуальной (листинг 10.1). Листинг 10.1 #include <iostream> using namespace std; class Base //базовый класс { public: virtual void show() = 0; //чистаЯ виртуальнаЯ //функциЯ }; ////////////////////////////////////////////////// class Derv1 : public Base //порожденный класс 1 { public: 108
void show() { cout << "Derv1\n"; } }; ////////////////////////////////////////////////// class Derv2 : public Base //порожденный класс 2 { public: void show() { cout << "Derv2\n"; } }; ///////////////////////////////////////////////// int main() { // Base bad; //невозможно создать объект //из абстрактного класса Base* arr[2]; //массив указателей на //базовый класс //Base bv; // Ћшибка Derv1 dv1; //Ћбъект производного класса 1 Derv2 dv2; //Ћбъект производного класса 2 arr[0] = &dv1; arr[1] = &dv2; //‡анести адрес dv1 в массив //‡анести адрес dv2 в массив arr[0]‐>show(); arr[1]‐>show(); return 0; } Результат: Derv1 Derv2 //‚ыполнить функцию show() //над обоими объектами Виртуальные деструкторы. Деструкторы базового класса обязательно должны быть виртуальными. Допустим, чтобы удалить объект порожденного класса, выполнили delete над указателем базового класса, указывающим на порожденный класс. Тогда delete, будучи обычным методом, вызовет деструктор для базового класса вместо того, чтобы запустить деструктор для порожденного класса. Это приведет к тому, что будет удалена только та часть объекта, которая относится к базовому классу (листинг 10.2). 109
Листинг 10.2 #include <iostream> using namespace std; ////////////////////////////////////////////////////// class Base { public: virtual ~Base() //виртуальный деструктор { cout << "Base удален\n"; } }; ////////////////////////////////////////////////////// class Derv : public Base { public: ~Derv() { cout << "Derv удален\n"; } }; ////////////////////////////////////////////////////// int main() { Base* pBase = new Derv; delete pBase; return 0; } Результат: Derv удален Base удален Виртуальные базовые классы. Рассмотрим ситуацию, когда каждый из порожденных классов Child1 и Child2 наследует свою копию базового класса Parent. Эта копия называется подобъектом. Каждый объект класса Grandchild будет иметь два подобъекта класса Parent. Чтобы избежать неоднозначности при обращении к членам базового объекта Parentб, можно объявить этот базовый класс виртуальным. Для этого используется то же зарезервированное имя virtual, что и при объявлении виртуальных функций (рис. 10.1). 110
class Child1 : virtual public Parent{…}; class Child2 : virtual public Parent{…}; class Grandchild : public Child1, public Child2{…}; Parent Child1 Child2 Grandchild Рис. 10.1. Наследование классов Дружественные функции. Принцип инкапсуляции и ограничения доступа к данным запрещает функциям, не являющимся методами соответствующего класса, доступ к скрытым (private) или защищенным данным объекта. Политика этих принципов такова, что, если функция не является членом объекта, она не может пользоваться определенным рядом данных. Тем не менее, есть ситуации, когда такая жесткая дискриминация приводит к значительным неудобствам. Допустим, что необходимо, чтобы функция работала с объектами двух разных классов. Например, функция будет рассматривать объекты двух классов как аргументы и обрабатывать их скрытые данные. В такой ситуации спасет лишь friend-функция. Рассмотрим листинг 10.3: Листинг 10.3 #include <iostream> using namespace std; ////////////////////////////////////////////////////// class beta; //нужно длЯ объЯвлениЯ frifunc class alpha { private: int data; 111
public: alpha() : data(3) { } без friend int frifunc(alpha, beta); //конструктор //аргументов //дружественнаЯ //функциЯ }; ////////////////////////////////////////////////////// class beta { private: int data; public: beta() : data(7) { } //конструктор без //аргументов friend int frifunc(alpha, beta); //дружественнаЯ //функциЯ }; ////////////////////////////////////////////////////// int frifunc(alpha a, beta b) //определение функции { return( a.data + b.data ); } //‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐ ‐‐‐‐‐ int main() { alpha aa; beta bb; cout << frifunc(aa, bb) << endl; return 0; } //вызов функции В этой программе два класса — alfa и beta. Конструкторы этих классов задают их единственные элементы данных в виде фиксированных значений (3 и 7 соответственно). Необходимо, чтобы функция frifunc() имела доступ и к тем, и к другим скрытым дан112
ным, поэтому мы делаем ее дружественной функцией. Этой цели в объявлениях внутри каждого класса служит ключевое слово friend: friend int frifunc(alpha, beta) Это объявление может быть расположено где угодно внутри класса (нет разницы в public или private). Объект каждого класса передается как параметр функции frifunc(), и функция имеет доступ к срытым данным обоих классов посредством этих аргументов. Кроме того, к классу нельзя обращаться до того, как он объявлен в прграмме. Важное отличие дружественных функций состоит в том, что для них не используется указатель this. При объявлении дружественной функции-оператора должны передаваться два аргумента для бинарных операций и один для унарных. Есть ситуации, в которых использование дружественных функций обязательно. Перегрузку операции умножения объекта класса на целое можно записать в виде функции-члена и в виде дружественной функции. В то время, как операцию умножения целое на объект класса можно определить только через дружественную функцию. Методы могут быть превращены в дружественные функции одновременно с определением всего класса как дружественного (листинг 10.4). Листинг 10.4 #include <iostream> using namespace std; ////////////////////////////////////////////////////// class alpha { private: int data1; public: alpha() : data1(99) { } //конструктор friend class beta; //beta Р дружественный класс }; ////////////////////////////////////////////////////// class beta 113
{ //все методы имеют доступ public: //к скрытым данным alpha void func1(alpha a) { cout << "\ndata1=" << a.data1;} void func2(alpha a) { cout << "\ndata1=" << a.data1;} }; ////////////////////////////////////////////////////// int main() { alpha a; beta b; b.func1(a); b.func2(a); cout << endl; return 0; } Статические функции. Некоторые члены класса могут быть объявлены с модификатором класса памяти static. Это статические члены класса. Статические члены-данные класса являются общими для всех объектов данного класса. Изменив значение статического члена класса в одном объекте, мы получим изменившееся значение во всех других объектах класса. Статические членыданные класса можно использовать для подсчета количества созданных объектов класса или существующих в данный момент объектов класса. Функции-члены класса также могут быть объявлены статическими. Статические функции-члены класса не получают указатель this, соответственно, эти функции не могут обращаться к нестатическим членам класса. К статическим членам класса статические функции-члены класса обращаются посредством операции точка или ->. Статическая функция-член класса не может быть виртуальной. К статическим функциям-членам класса можно обращаться, даже если не создано ни одного объекта данного класса, нужно только использовать полное имя члена класса (листинг 10.5). Если функция func1() является статической функцией-членом класса A, то ее можно вызвать: A::func1(); 114
Листинг 10.5 #include <iostream> using namespace std; ////////////////////////////////////////////////////// class gamma { private: static int total; //всего объектов класса //(только объЯвление) int id; //ID текущего объекта public: gamma() //конструктор без аргументов { total++; //добавить объект id = total; //id равен текущему значению total } ~gamma() //деструктор { total‐‐; cout << "“даление ID " << id << endl; } static void showtotal() // статическаЯ функциЯ !!!!!!! { cout << "‚сего: " << total << endl; } void showid() // ЌестатическаЯ функциЯ { cout << "ID: " << id << endl; } }; //‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐‐ int gamma::total = 0; // определение total ////////////////////////////////////////////////////// int main() { gamma g1; gamma::showtotal(); //!!!!!!!!!!!!!! //без static обращение к методу только через объект 115
gamma g2, g3; gamma::showtotal(); g1.showid(); g2.showid(); g3.showid(); cout << "‐‐‐‐‐‐‐‐‐‐конец программы‐‐‐‐‐‐‐‐‐‐\n"; return 0; } Инициализация копирования и присваивания. Перегрузка оператора присваивания. Мы постоянно пользуемся оператором присваивания, может быть, даже не задумываясь о том, что за ним стоит с точки зрения компилятора. Пусть а1 и а2 некоторые объекты. Несмотря на то, компилятору: «присвоить а2 значение а1», выражние а2=а1; заставит компилятор копировать данные из а1 элемент за элементом в а2. Таковы действия по умолчанию для операторов присваивания. Инициализация одного объекта другим вызывает подобные действия alpha a2(a1); компилятор создает новый объект а2, затем элемент за элементом копирует данные. Это то, что по умолчанию делает компилятор. Если же нужно заставить присваивание или инициализацию выполнять какие-либо более интеллектуальные действия, то придется разобраться, как обойти стандартную реакцию на эти операторы. Рассмотрим в листинге 10.6 пример перегрузки оператора присваивания. Листинг 10.6 #include <iostream> using namespace std; ////////////////////////////////////////////////////// class alpha { private: int data; public: alpha() //конструктор без аргументов { } 116
alpha(int d) //конструктор с одним аргументом { data = d; } void display() //вывести данные { cout << data; } alpha operator = (alpha& a) //перегружаемый = { data = a.data; //не выполнЯетсЯ автоматически cout << "\nЗапущен оператор присваиваниЯ"; return alpha(data); //возвращает копию alpha } }; ////////////////////////////////////////////////////// int main() { alpha a1(37); alpha a2; a2 = a1; //запуск перегружаемого = cout << "\na2="; a2.display(); //вывести a2 alpha a3 = a2; cout << "\na3="; a3.display(); cout << endl; return 0; } Результат: Запущен оператор присваивания а2=37 а3=37 // запускаетсЯ = //вывести a3 Класс alpha очень прост, в нем содержится только один элемент данных. Конструкторы инициализируют данные, а методы выводят их значения на экран. В функции operator = () перегружен оператор =. В main() определяем переменную а1 и присваиваем ей значение 37, определяем переменную a2. А значение присваиваем в выражении a2=a1. Тем самым запускаем нашу перегружаемую функцию operator = (). В выражении alpha a3=a2; происходит инициализация, а не копирование, это равносильно выражению alpha a3(a2); Стоит отметить, что аргумент для функции operator=() пе117
редается по ссылке. Когда значение аргументов передаются по ссылке, копии не создаются, что помогает экономить память. Кроме того удается ликвидировать прецеденты ложного создания объектов. Функция может возвращать результат в вызывающую программу по значению или по ссылке. В первом случае происходит передача результата, а значит, создается копия объекта, что и возвращается в программу. Во втором случае новый объект не создается. Ссылка на исходный объект — вот и все, что возвращается в качестве результата. Функция operator = () возвращает результат путем создания временного объекта и его инициализации с помощью одноаргументного конструктора в выражении return alpha(data). Оператор присваивания уникален тем, что он не может наследоваться. Перегрузив присваивание в базовом классе, нельзя использовать ту же функцию в порожденных классах. Перегрузка оператора присваивания. Определить объект и инициализировать его другим можно разными способами: alpha a3(a2); alpha a3 = a2; Оба стиля включают конструктор копирования, то есть конструктор, создающий новый объект и копирующий в него свои аргументы. По умолчанию этот конструктор производит поэлементное копирование. Это похоже на то, что делает оператор присваивания, с той лишь разницей, что компьютер создает новый объект. Рассмотрим листинг 10.7 Листинг 10.7 // xofxref.cpp // перегрузка конструктора копированиЯ: X(X&) #include <iostream> using namespace std; ////////////////////////////////////////////////////// class alpha { private: 118
int data; public: alpha() //конструктор без аргументов { } alpha(int d) //конструктор с одним аргументом { data = d; } alpha(alpha& a) //конструктор копированиЯ { data = a.data; cout << "\n‡апущен конструктор копированиЯ"; } void display() //display { cout << data; } void operator = (alpha& a) //overloaded = operator { data = a.data; cout << "\n‡апущен оператор присваиваниЯ"; } }; ////////////////////////////////////////////////////// int main() { alpha a1(37); alpha a2; a2 = a1; //запуск перегружаемого = cout << "\na2="; a2.display(); //вывести a2 alpha a3(a1); //запуск конструктора копированиЯ // alpha a3 = a1; //эквивалентное определение a3 cout << "\na3="; a3.display(); //вывести a3 cout << endl; return 0; } В этой программе перегружается и оператор присваивания, и конструктор копирования. Конструктор копирования может быть запущен во время определения объекта. Также он запускается при передаче по значению аргумента функции: создает копию объекта, 119
с которой функция работает. Конструктор копирования не запускается при передаче аргумента по ссылке или при передаче указателя на объект. В этих случаях функция работает с исходной переменной. Конструктор копирования также создает временный объект, когда значение возвращается из функции в основную программу. Важно, что в конструкторе копирования аргумент должен передаваться по ссылке, что не приведет к созданию копий объекта. И, вообще, работая с объектами, которым требуется нечто большее, нежели простое поэлементное копирование, нужно передавать и возвращать данные по ссылкам везде, где это возможно. Иногда бывает нужно запретить копирование. Сделать это можно, используя те же инструменты. Необходимо перегрузить присваивание и конструктор копирования, делая их скрытыми членами класса. И как только вы попытаетесь скопировать информацию, компилятор сообщит, что функция недоступна. Указатель this. Методы каждого класса имеют доступ к указателю под названием this, который ссылается на сам объект. Таким образом, когда вызывается какой-либо метод, значением указателя this становится адрес объекта, для которого этот метод вызван, то есть его можно использовать для получения доступа к данным объекта, на который он ссылается. Более практичным применением указателя this является возврат значений из методов и перегружаемых операций в вызывающую программу. Многие встречались с проблемой возврата объекта из функции по ссылке, потому что объект был локальный по отношению к возвращающей функции и, следовательно, уничтожался во время выхода из функции. Необходим более долговечный объект. Объект, чьим методом является данная функция, прочнее, нежели его собственные методы. Методы объекта создаются и уничтожаются при каждом вызове, в то время как сам объект может быть удален только извне (например, delete). Таким образом, будет лучше, если в объект включен метод, потому что в таком случае этот объект и возвращается в результате работы своего метода в вызывающую программу. Новый подход легко осуществляется с помощью указателя this (листинг 10.8). 120
Листинг 10.8 #include <iostream> using namespace std; ////////////////////////////////////////////////////// class alpha { private: int data; public: alpha() // конструктор без аргументов { } alpha(int d) // конструктор с одним аргументом { data = d; } void display() // вывести данные { cout << data; } alpha& operator = (alpha& a) // перегружаемаЯ операциЯ = { data = a.data; // не делаетсЯ автоматически cout << "\n‡апущен оператор присваиваниЯ"; return *this; // вернуть копию this alpha } }; ////////////////////////////////////////////////////// int main() { alpha a1(37); alpha a2, a3; a3 = a2 = a1; // перегружаемый =, дважды cout << "\na2="; a2.display(); // вывести a2 cout << "\na3="; a3.display(); // вывести a3 cout << endl; return 0; } Так как this является указателем на объект, чей метод выполняется, то *this — это и есть сам объект, и выражение возвращает его по ссылке. Следует отметить, что указатель this нельзя использовать в статических методах, так как они не ассоциированы с конкретным объектом. 121
ЛЕКЦИЯ 11. ПОТОКИ И ФАЙЛЫ Программа на С++ воспринимает ввод и вывод как потоки байтов. При вводе программа извлекает байты из входного потока, а при выводе помещает байты в выходной поток. Байты входного потока могут поступать с клавиатуры, но так же из устройств хранения, вроде жесткого диска или другой программы. Аналогично, байты выходного потока могут передаваться на дисплей, на принтер, на устройство хранения, или же отправляться другой программе. Потоки служат посредниками между программой и источником или местом назначения потока. Такой подход позволяет программам на С++ трактовать ввод с клавиатуры так же, как и ввод из файла; программа просто просматривает поток байтов, не нуждаясь в информации о том, откуда эти байты поступают. Точно так же, используя потоки, программа на С++ может обрабатывать вывод независимо от того, куда направляются байты. Потоковые классы. Библиотека потоковых классов С++ построена на основе двух базовых классов: ios и streambuf. Класс streambuf обеспечивает организацию и взаимосвязь буферов ввода-вывода, размещаемых в памяти, с физическими устройствами ввода-вывода. Методы и данные класса streambuf программист явно обычно не использует. Этот класс нужен другим классам библиотеки ввода-вывода. Он доступен и программисту для создания новых классов на основе уже существующих (рис. 11.1). Рис. 11.1. Класс streambuf 122
Класс ios содержит средства форматированного ввода-вывода и проверки ошибок (рис. 11.2). Рис. 11.2. Класс ios Стандартные потоки (istream, ostream, iostream) служат для работы с терминалом. Строковые потоки (istrstream, ostrstream, strstream) служат для ввода-вывода из строковых буферов, размещенных в памяти. Файловые потоки (ifstream, ofstream, fstream) служат для работы с файлами: • ios базовый потоковый класс; • streamtbuf буферизация потоков; • istream потоки ввода; • ostream потоки вывода; • iostream двунаправленные потоки; • iostream_withassign поток с переопределенной операцией присваивания; • istrstream строковые потоки ввода; • ostrstream строковые потоки вывода; • strstream двунаправленные строковые потоки; • ifstream файловые потоки ввода; • ofstream файловые потоки вывода; • fstream двунаправленные файловые потоки. Следующие объекты-потоки заранее определены и открыты в программе перед вызовом функции main: 123
extern istream cin; // Стандартный поток ввода с клавиатуры extern ostream cout; // Стандартный поток вывода на экран extern ostream cerr; // Стандартный поток вывода сообщений об ошибках (экран) extern ostream clog; // Стандартный буферизованный поток вывода // сообщений об ошибках (экран) Потоковые классы, их методы и данные становятся доступными в программе, если в неё включен нужный заголовочный файл: iostream — для ios, ostream, istream; strstream — для strstream, istrstream, ostrstream; fstream — для fstream, ifstream, ofstream. Потоковый ввод/вывод файлов. Файловый ввод-вывод ничем не отличается от консольного. За единственным исключением — если данные читаются из файла, то в любой момент можно вернуться к началу файла и считать все заново. Для того чтобы в C++ работать с файлами, необходимо подключить заголовочный файл fstream: #include <fstream> После этого можно объявлять объекты, привязанные к файлам. Для чтения данных из файла используются объекты типа ifstream (аббревиатура от input file stream, для записи данных в файл используются объекты типа ofstream (output file stream). Например: ifstream in; // Поток in будем использовать для чтения ofstream out; // Поток out будем использовать для записи Чтобы привязать тот или иной поток к файлу (открыть файл для чтения или для записи) используется метод open, которому необходимо передать параметр — текстовую строку, содержащую имя открываемого файла: in.open("input.txt"); out.open("output.txt"); После открытия файлов и привязки их к файловым потокам, работать с файлами можно так же, как со стандартными потоками 124
ввода-вывода cin и cout. Например, чтобы вывести значение переменной x в поток out, используется следующая операция: out<<x; А чтобы считать значение переменной из потока in: in>>x; Для закрытия ранее открытого файла используется метод close() без аргументов: in.close(); out.close(); Закрытый файловый поток можно переоткрыть заново при помощи метода open, привязав его к тому же или другому файлу. При считывании данных из файла может произойти достижение конца файла (end of file, сокращенно EOF). После достижения конца файла никакое чтение из файла невозможно. Для того чтобы проверить состояние файла, необходимо вызвать метод eof(). Данный метод возвращает true, если достигнут конец файла или false, если не достигнут. Кроме того, состояние файлового потока можно проверить, если просто использовать идентификатор потока в качестве логического условия: if (in) { } Также можно использовать в качестве условия результат возвращаемой операцией считывания. Если считывание было удачным, то результат считается истиной, а если неудачным — ложью. Например, организовать считывание последовательности целых чисел можно так: int d; while(in>>d) { } А организовать считывание файла построчно (считая, что строка заканчивается символом перехода на новую строку) так: string S; while ( getline(in,S)) { } 125
Проверить, удалось ли нам открыть файл, можно следующим образом: if ( ! outfile ) // false, если файл не открыт cerr << "Ошибка открытия файла.\n" Так же открывается файл и для ввода, только он имеет тип ifstream: ifstream infile("name‐of‐file"); if ( ! infile ) // false, если файл не открыт cerr << "Ошибка открытия файла.\n" Ниже приводится текст простой программы (листинг 11.1), которая читает файл с именем in_file и выводит все прочитанные из этого файла слова, разделяя их пробелом, в другой файл, названный out_file. Листинг 11.1 #include <iostream> #include <fstream> #include <string> int main() { ifstream infile("in_file"); ofstream outfile("out_file"); if( ! infile ){ cerr << "Ошибка открытия входного файла.\n" return ‐1; } if (!outfile){ cerr << "Ошибка открытия выходного файла.\n" return ‐2; } string word; while ( infile >> word ) outfile << word << ' '; return 0; } 126
Файловый ввод/вывод с помощью методов. Функции put и get. Один из способов чтения и записи бесформатных файлов основан на применении функций put() и get(). Эти функции оперируют символами. Точнее говоря, функция get() считывает символ, а функция put() — записывает его. Разумеется, если файл открыт в бинарном режиме, то при считывании символа (не расширенного символа), эти функции считывают и записывают байты. Функция get() имеет несколько форм, однако чаще всего используется ее следующая версия. "Класс: оstream: функциячлен:put: iostream &get (char ch); ostream &put (char ch); Функция get() считывает отдельный символ из потока и записывает его в переменную ch. Кроме того, функция get() возвращает ссылку на поток. Функция put() записывает переменную ch в поток и возвращает ссылку на поток. Функции read и write. Блоки бинарных данных можно считывать с помощью функций read() и write(). Их прототипы выглядят следующим образом: istream bread(char *buf, streamsize num); istream &write(const char*buf, streamsize num); Функция read считывает пит символов из потока и записывает их в буфер, на который ссылается указатель buf. Функция write записывает пит символов в поток, считывая их из буфера, на который ссылается указатель buf. Как разъяснялось в предыдущей лекции, тип streamsize определен в библиотеке как разновидность типа int. Он позволяет хранить максимальное количество символов, которые могут преобразовываться при выполнении операций ввода-вывода (листинг 11.2). Следующая программа записывает структуру на диск, а затем считывает ее обратно. Листинг 11.2 #include <iostream> #include <fstream> 127
#include <cstring> using namespace std; struct status { char name[80]; double balance; unsigned long account_num; }; int main() struct status acc; strcpy(acc.name,"Ральф Трантор"); acc.balance = 1123.23; асе.account_num = 34235678; // Записываем данные ofstream outbal("balance",ios::out| ios::binary); if('outbal) { cout << "Невозможно открыть файл.\n"; return 1; } outbal.write((char *) &acc, sizeof(struct status)); outbal.close(); // Считываем данные вновь ifstream inbal ("balance" , ios:: in | ios: rbinary) ; if(!inbal) { cout << "Невозможно открыть файл. \n"; return 1; } inbal.read((char *) &acc, sizeof(struct status)); cout << acc.name << endl; cout << "Счет # " << acc.account_num; cout.precision (2) ; cout.setf (ios::fixed); cout << endl << "Баланс: $" << acc.balance; inbal. close () ; return 0 ; } Как видно, для считывания или записи целой структуры достаточно одного вызова функции read() или write(). Отдельное поле структуры невозможно считать или записать отдельно. Кроме то128
го, этот пример показывает, что буфером может служить объект любого типа. Функции peek() и putback(). Можно считать следующий символ из потока, не извлекая его оттуда. Для этого предназначена функция peek( ), имеющая прототип, приведенный ниже: int_type peek(); Эта функция возвращает следующий символ из потока ввода или признак конца файла. Тип int_type определен как разновидность типа int. Символ, считанный из потока последним, можно вернуть обратно с помощью функции putback(). Ее прототип имеет следующий вид: istream &putback(char с); Здесь параметр с означает символ, считанный последним. Функция flush)). При выводе данные не сразу передаются физическому устройству, связанному с потоком. Вместо этого они накапливаются во внутреннем буфере, пока он не заполнится. Однако существует способ принудительно записать информацию из буфера на диск, не дожидаясь его заполнения. Для этого предназначена функция flush(). Ее прототип имеет следующий вид: ostream &flush(); Функцию flush следует вызывать, когда программа выполняется в неблагоприятных условиях (например, если часто происходят сбои питания). Закрытие файла или прекращение работы программы также очищает все буферы. Перегрузка операций извлечения и вставки. Вывод в поток выполняется с помощью операции вставки (в поток), которая является перегруженной операцией сдвига влево << . Левым ее операндом является объект потока вывода. Правым операндом может являться любая переменная, для которой определен вывод в поток (то есть переменная любого встроенного типа или любого определенного пользователем типа, для которого она перегружена). Например, оператор cout << "Hello!\n"; приводит к выводу в предопределенный поток cout строки "Hello!". 129
Операция << возвращает ссылку на объект типа ostream, для которого она вызвана. Это позволяет строить цепочки вызовов операции вставки в поток, которые выполняются слева направо: int i = 5; double d = 2.08; cout << "i = " << i << ", d = " << d << '\n'; Эти операторы приведут к выводу на экран следующей строки: i = 5, d = 2.08 Операция вставки в поток поддерживает следующие встроенные типы данных: char, short, int, long, char* (рассматриваемый как строка), float, double, long double, void*: ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& ostream& operator<< (short n); operator<< (unsigned short n); operator<< (int n); operator<< (unsigned int n); operator<< (long n); operator<< (unsigned long n) ; operator<< (float f); operator<< (double f); opera to r<< (long double f) ; operator<< (const void *p); Целочисленные типы форматируются в соответствии с правилами, принятыми по умолчанию, если они не изменены путем установки различных флагов форматирования. Тип void* используется для отображения адреса: int i; // Отобразить адрес в 16‐ричной форме: cout << &i; Отметим, что перегрузка не изменяет нормального приоритета выполнения операции <<, поэтому можно записать cout << "sum =" << x+y << "\n"; без круглых скобок. Однако, в случае cout << (x & y) << "\n"; круглые скобки нужны. Для ввода информации из потока используется операция извлечения, которой является перегруженная операция сдвига вправо >>. Левым операндом операции >> является объект класса istream, который также является и результатом операции. Это по130
зволяет строить цепочки операций извлечения из потока, выполняемых слева направо. Правым операндом может быть любой тип данных, для которого определен поток ввода: istream& operator>>(short& n); istream& operator>>(unsigned short& n); istream& operator>>(int& n); istream& operator>>(unsigned int& n); istream& operator>>(long& n); istream& operator>>(unsigned long& n); istream& operator>>(float& f); istream& operator>>(double& f); istreaai& operator>>(long double& f); istream& operator>>(void*& p); По умолчанию операция >> пропускает символы-заполнители (по умолчанию — пробельные символы), затем считывает символы, соответствующие типу заданной переменной. Пропуск ведущих символов-заполнителей устанавливается специально для этого предназначенным флагом форматирования. Рассмотрим следующий пример: int i; double d; cin >> i >> d; Последний оператор приводит к тому, что программа пропускает ведущие символы-заполнители и считывает целое число в переменную i. Затем она игнорирует любые символы-заполнители, следующие за целым числом, и считывает переменную с плавающей точкой d. Для переменной типа char* (рассматриваемой как строка) оператор >> пропускает символы-заполнители и сохраняет следующие за ними символы, пока не появится следующий символзаполнитель. Затем в указанную переменную добавляется нульсимвол '\n'. Одним из главных преимуществ потоков ввода-вывода является их расширяемость для новых типов данных. Можно реализовать операции извлечения и вставки для своих собственных типов данных. Чтобы избежать неожиданностей, ввод-вывод для определенных пользователем типов данных должен следовать тем же согла131
шениям, которые используются операциями извлечения и вставки для встроенных типов данных. Рассмотрим пример перегрузки операций извлечения и вставки в поток для определенного пользователем типа данных, которым является следующий класс даты: class Date { public: Date(int d, int m, int y); Date(const tm & t); Date(); private: tm tm_date; }; Этот класс содержит член типа tm, который представляет собой структуру для хранения даты и времени, определенную в заголовочном файле time.h. Чтобы осуществить ввод-вывод пользовательского типа данных, какими являются объекты класса Date, нужно перегрузить операции извлечения и вставки в поток для этого класса. Приведем соответствующее объявление класса Date: class Date { tin tm_date; friend istream& friend ostream& dat); public: Date(int d, int Date(tm t); Date(); tin tm_date; friend istream& friend ostream& dat); }; operator>> (istreamfi is, Date dat); operator<< (ostream& os, const Date& m, int y); operator>> (istreamfi is, Date dat); operator<< (ostream& os, const Date& Реализуем операции извлечения и вставки для объектов класса Date. Возвращаемым значением для операции извлечения (и вставки) должна являться ссылка на поток, чтобы несколько операций могли быть выполнены в одном выражении. Первым параметром должен быть поток, из которого будут извлекаться данные, вторым 132
параметром — ссылка или указатель на объект определенного пользователем типа. Чтобы разрешить доступ к закрытым данным класса, операция извлечения должна быть объявлена как дружественная функция класса. Ниже приведена операция извлечения из потока для класса Date: istream.& operator>>(istream& is, Date& dat){ is >> dat.tm_date.tm_mday; is >> dat.tm_date.tm_mon; is >> dat.tm_date.tm_year; return is; } Те же самые замечания верны и для операции вставки. Она может быть построена аналогично. Единственное отличие заключается в том, что в нее нужно передать константную ссылку на объект типа Date, поскольку операция вставки не должна модифицировать выводимые объекты. Ниже приведена ее реализация для класса Date: osiream& operator<<(ostream& os, const Date& dat){ os << dat.tm_date.tm_mday << '/'; os << dat.tm_date.tin_mon << '/'; os << dat.tm_date.tm_year; return os; } Следуя соглашениям о вводе-выводе для потоков, теперь можно осуществлять извлечение и вставку объектов класса Date следующим образом: Date birthday(24,10,1985); cout << birthday << '\n'; или Date date; cout << "Пожалуйста, введите дату (день, месяц, год)\n"; cin >> date; cout << date << '\n'; Аргументы командной строки. Программы, обрабатывающие файлы, часто используют аргументы командной строки для идентификации файлов. Аргументы командной строки — это парамет133
ры, вводимые в командной строке после команды. Например, чтобы подсчитать количество слов в некоторых файлах в системе Unix или Linux, в приглашении командной строки понадобится ввести следующую команду: wc reportl report2 report3 Здесь wc — имя программы, а reportl, report2 и report3 — имена файлов, переданные программе в качестве аргументов командной строки. В С++ имеется механизм, который позволяет программам, запущенным из среды командной строки, получать доступ к аргументам командной строки. Можно использовать следующий альтернативный заголовок функции main (): int main(int argc, char *argv[]) Аргумент argc представляет количество аргументов в командной строке. Счетчик включает имя самой команды. Переменная argv — это указатель на указатель на char. Это звучит несколько абстрактно, но argv можно трактовать как массив указателей на аргументы командной строки, причем argv[0] указывает на первый символ строки, содержащей имя самой команды, argv [1] — указатель на первый символ строки, содержащей первый аргумент командной строки, и т. д. То есть argv [0] — первая строка команды и т. д. Например, предположим, что имеется следующая командная строка: wc reportl report2 report3 В этом случае argc будет равно 4, argv[0] — wc, argv[l] — reportl и т. д. Следующий цикл будет выводить каждый аргумент командной строки в отдельной строке экрана: for (int i = 1; i < argc; i++) cout « argv[i] « endl; Если начать с i = 1, то будут выведены только аргументы командной строки, а если начать с i = 0, будет выведено и имя команды. Конечно, аргументы командной строки тесно взаимосвязаны с операционными системами, ориентированными на командную строку, такими как режим командной строки Windows, Unix и Linux. Другие среды также могут допускать использование аргументов командной строки. 134
Код в листинге 11.3 сочетает технологию командной строки с технологиями файловых потоков для подсчета количества символов в файлах, перечисленных в командной строке. Листинг 11.3 include <iostream> include <fstream> include <cstdlib> // для exit () using namespace std; int main(int argc, char * argv[] ) { if (argc == 1)// выход при отсутствии аргументов { cerr « "Usage: " « argv [0 ] « " filename [s] \n"; exit(EXIT_FAILURE); } ifstream fin; // открытие потока long count; long total = 0; char ch; for (int file = 1; file < argc; file++) { fin.open (argv[file] ) ;// подключение потока к argv [file] if (!fin.is_open()) { cerr « "Could not open " « argv [file] « endl;// не удается открыть файл fin.clear(); continue; } count = 0; while (fin.get(ch)) count++; cout « count « " characters in " « argv [file] << endl; // количество символов в файле total += count; fin.clear(); // требуется для некоторых реализаций 135
fin.close(); // отключение от файла } cout « total « " characters in all files\n"; // коли‐ чество символов во всех файлах return 0; } Некоторые реализации С++ требуют вызова fin.clear() в конце программы, а другие — нет. Это зависит от того, сбрасывается ли состояние потока автоматически при ассоциировании нового файла с объектом типа ifstream. Использование fin.clear () не повредит, даже если в этом нет необходимости. Вывод на печатающее устройство. Не составляет никаких проблем использовать консольные программы для того, чтобы посылать данные на принтер. Операционная система найдет ряд специальных имен файлов, которые обозначают различные устройства. Тем самым делается возможной работа с устройствами как с файлами. Табл. 11.1 содержит все зарезервированные под устройства имена файлов. Таблица 11.1 Имя con Устройство Консоль (клавиатура и монитор) aux или com1 Первый последовательный порт com2 Второй последовательный порт prn или lpt1 Первый параллельный порт lpt2 Второй параллельный порт lpt3 Третий параллельный порт nul Фиктивное (несуществующее устройство) В большинстве систем принтер подключен к первому параллельному порту, поэтому имя принтера — prn или lptl (понятно, что в случае, если система настроена иначе, надо использовать другое имя). Следующая программа посылает строку и число на принтер, используя форматированный вывод (оператор вставки) (листинг 11.4). 136
Листинг 11.4 #include <fstream> // для файловых потоков using namespace std; int main() { char* s1 = “\nСегодня ваше счастливое число ‐‐ ”; int n1 = 17982; ofstream outfile; // создать выходной файл outfile.open(“PRN”); //открыть принтеру доступ к нему outfile << s1 << n1 << endl; // послать данные на принтер outfile << ‘\x0C’; // прогнать лист до конца return 0; } Таким способом на принтер можно послать сколько угодно строк. Служебный символ ‘\x0C’ осуществляет прогон страницы. Следующая программа распечатает содержимое дискового файла на принтере. В ней используются посимвольный подход к передаче данных (листинг 11.5). Листинг 11.5 #include <fstream> //для файловых функций #include <iostream> using namespace std; #include <process> // для exit() int main (int argc, char* argv) { if (argc != 2) { cerr << “\nФормат команды: oprint имя_файла\n”; exit(‐1); } char ch; // символ для считывания ifstream infile; //создать входной файл infile.open (argv[1]); //открыть файл if (!infile) // проверять на наличие ошибок 137
{ cerr << “\nНевозможно открыть ” << argv[1] << endl; exit(‐1); } ofstream outfile; // создать файл outfile.open(“PRN”); //открыть доступ принтера к нему while(infile.get(ch) != 0) //считать символ outfile.put(ch);// отправить символ на печать outfile.put(‘\x0C’); // прогон страницы return 0; } Эта программа может быть использована для печати любых текстовых файлов, например исходных текстов программ .срр. Она очень похожа на команду print из операционной системы MS DOS. Программа проверяет корректность количества аргументов и правильность открытия файла. 138
ЛЕКЦИЯ 12. ШАБЛОНЫ И ИСКЛЮЧЕНИЯ Функция-шаблон определяет общий набор операций, который будет применен к различным типам данных. Шаблоны позволяют применять общие алгоритмы к широкому кругу данных (например, алгоритмы сортировки и поиска). Определение функций-шаблонов. Функции-шаблоны создаются с использованием ключевого слова template (шаблон). Шаблон используется для создания каркаса функции, оставляя компилятору реализацию подробностей. Общая форма функции-шаблона имеет следующий вид (листинг 12.1). Листинг 12.1 template <class пользовательский_тип> возвращаемый_тип имя_функции(список параметров) { // тело функции } Пользовательский тип является «держателем места» (placeholder) для имени типа данных, которое используется функцией. Он может быть применен в определении функции и будет автоматически заменен компилятором на фактический тип данных во время создания конкретной версии функции. Можно определить несколько типов-шаблонов данных в инструкции template, используя список с запятыми в качестве разделителя (листинг 12.1). Например, следующая программа создает функцию-шаблон, имеющую два типа-шаблона: Листинг 12.1 #include <iostream> template <class type1, class type2> void myfunc(type1 x, type2 y) 139
{ std::cout << X << ' ' << у << std::endl; } int main() { myfunc(10, "hi"); myfunc(0.23, 10L); return 0; } В этом примере типы-шаблоны type1 и type2 заменяются компилятором на типы int, char*, double и long, а компилятор создает два различных экземпляра функции myfunc() в функции main(). Явная перегрузка функций-шаблонов. Хотя функция-шаблон перегружает себя по мере необходимости, также можно перегрузить ее явным образом. Если перегружается функция-шаблон, то перегруженная функция переопределяет функцию-шаблон для того конкретного набора типов параметров, для которого создается перегруженная функция. Примером служит переопределение функции swap() в листинге 12.3. Листинг 12.3 // переопределение функции‐шаблона #include <iostream> template <class X> void swap(X &a, X &b) { X temp; temp = a; a = b; b = temp; } void swap(int &af int &b) {// обобщенная версия swap() int temp; temp = a; a = b; b = temp; cout << "Inside overloaded swap(int &, int &).\n"; } 140
int main() { int i=10, j=20; float x=10 .1, y=23.3; char a='x', b='z'; cout << "Original i, j : " << i << ' ' << j << endl << "Original x, y : " << x << ' ' << у << endl << "Original a, b : " << a << ' ' << b << endl; swap(i, j); // вызов явно перегруженной swap () swap(x, у); // обмен вещественных значений swap(a, b); // обмен символов cout << "Swapped i, j : " << i << ' ' << j << endl; cout << "Swapped x, y: " << x << ' ' << у << endl; cout << "Swapped a, b: " << a << ' ' << b << endl; return 0; } При вызове функции swap (i, j) вызывается перегруженная явным образом версия swap(). Поэтому компилятор не создает данной версии swap() функции, поскольку функция-шаблон переопределена с помощью явной перегрузки. Ограничения на функции-шаблоны. Функции-шаблоны сходны с перегруженными функциями, за исключением того, что они более ограничивающие. Для перегруженных функций можно выполнять различные действия в теле каждой функции. В отличие от этого, для функции-шаблона необходимо выполнять одни и те же общие действия, и только тип данных может быть различным. Другим ограничением на функции-шаблоны является то, что виртуальная функция не может быть функцией-шаблоном. Классы-шаблоны. Кроме функций-шаблонов можно также определить классы-шаблоны. Классы-шаблоны полезны тогда, когда класс содержит логику, допускающую значительные обобщения. Используя классы-шаблоны, можно создавать классы, поддерживающие очереди, связанные списки и т. д. для произвольных типов данных. Компилятор автоматически создаст корректный код, основываясь на типе данных, указанном перед компиляцией. Общая форма объявления класса-шаблона показана ниже (листинг 12.4) 141
Листинг 12.4 template < class пользовательский_тип> class имя_класса { ... } Здесь пользовательский тип является параметром-типом, который будет указан при создании экземпляра класса. При необходимости можно определить несколько типов-шаблонов, используя список и запятую в качестве разделителя. После создания классашаблона можно создать конкретный экземпляр этого класса, используя следующую общую форму: имя_класса <тип> объект; Тип является именем типа данных, с которыми будет оперировать данный класс. Функции-члены класса-шаблона являются автоматически шаблонами. Нет необходимости особым образом указывать на то, что они являются шаблонами с использованием ключевого слова template (листинг 12.5). В следующем примере создается класс-шаблон stack, реализующий стандартный стек «последним вошел — первым вышел». Он может использоваться для реализации стека с произвольным типом данных. Листинг 12.5 // демонстрация класса‐шаблона stack #include <iostream> const int SIZE = 100; template <class SType> class stack {// создание клас‐ са‐шаблона stack SType stck[SIZE]; int tos; public: stack(); ~stack(); 142
void push(SType i); SType pop(); }; template <class SType> stack<SType>::stack() {// функция‐конструктор stack tos = 0; cout << "Stack Initialized\n"; } template <class SType> stack<SType>::~stack() {// функция‐деструктор stack cout << "Stack Destroyed\n"; } template <class SType> void stack<SType>::push(SType i) {// помещение объекта в стек if (tos==SIZE) { cout << "Stack is full. \n"; return; } stck[tos] = i; tos++; } template <class SType> SType stack<SType>::pop() {// извлечение объекта из стека if(tos==0) { cout << "Stack underflow.\n"; return 0; } tos ‐‐; return stck[tos]; } int main() { stack<int> a; // создание целочисленного стека stack<double> b; // создание вещественного стека stack<char> с; //создание символьного стека int i; 143
// использование целого и вещественного стеков a.push (1); b.push (99.3); a.push(2); b.push(‐12.23); cout << a.pop() << " "; cout << a.pop() << " "; cout << b.pop() << " "; cout << b.pop() << "\n"; // демонстрация символьного стека for (i=0; i<10; i++) с.push ( (char) 'A'+i); for (i=0; i<10; i+ + ) cout << c.pop(); cout << "\n"; return 0; } Нужный тип данных подставляется в угловые скобки. При изменении типа данных, указываемого при создании объектов класса stack, одновременно изменяется тип данных, хранящихся в стеке. Например, можно создать другой стек, хранящий указатели на символы: stack<char *> chrptrstck; Также можно использовать класс stack для создания стека, в котором хранятся данные пользовательского типа, включая объекты классов (в том числе классовшаблонов) и структур. Обработка исключений (exception handling) позволяет упорядочить обработку ошибок времени исполнения. Используя обработку исключений С++, программа может автоматически вызвать функцию-обработчик ошибок тогда, когда такая ошибка возникает. Обработка исключений позволяет автоматизировать большую часть кода для обработки ошибок. Обработка исключений в С++ использует три ключевых слова: try, catch и throw. Те инструкции программы, в которых ожидается возможность появления исключительных ситуаций, содержатся в блоке try. Если в блоке try возникает исключение, т. е. ошибка, то генерируется исключение. Исключение перехватывается, используя catch, и обрабатывается. 144
Инструкция, генерирующая исключение, должна исполняться внутри блока try. Вызванные из блока try функции также могут генерировать исключения. Всякое исключение должно быть перехвачено инструкцией catch, которая следует за инструкцией try. Общая форма блоков try и catch показана ниже (листинг 12.6). Листинг 12.6 try {// блок try catch (тип1 аргумент) {// блок catch} catch (тип2 аргумент) {// блок catch} ... catch (типN аргумент) {// блок catch} Когда исключение сгенерировано, оно перехватывается соответствующей инструкцией catch, обрабатывающей это исключение. Одному блоку try может отвечать несколько инструкций catch. Какая именно инструкция catch исполняется, зависит от типа исключения. Перехваченным может быть любой тип данных, включая созданные программистом классы. Если никакого исключения не сгенерировано, то есть никакой ошибки не возникло в блоке try, то инструкции catch выполняться не будут. Исключение может также быть сгенерировано из функции, вызванной изнутри блока try. Инструкция throw должна выполняться либо внутри блока try, либо в функции, вызванной из блока try. Общая форма записи инструкции throw имеет вид: throw исключение; Если генерируется исключение, для которого отсутствует подходящая инструкция catch, может произойти аварийное завершение программы. При генерации необработанного исключения вызывается функция terminate(). По умолчанию terminate() вызывает функцию abort(), завершающую выполнение программы. Однако можно задать свою собственную обработку, используя функцию set_terminate(). 145
Обычно код в инструкции catch пытается исправить ошибку путем выполнения подходящих действий. Если ошибку удалось исправить, то выполнение продолжается с инструкции, непосредственно следующей за catch. Однако иногда не удается справиться с ошибкой, и блок catch завершает программу путем вызова функции exit() или функции abort(). В следующем примере (листинг 12.7) тип инструкции catch не совпадает с throw, исключение не будет перехвачено и произойдет аварийное завершение программы. Листинг 12.7 // данный пример не будет работать #include <iostream> int main() { cout << "Start\n"; try { // начало блока try cout << "Inside try block\n"; throw 100; // генерация ошибки cout << "This will not execute"; } catch (double i) { // не будет работать для целочис‐ ленного исключения cout << "Caught an exception ‐‐ value is: "; cout << i << "\n"; } cout << "End"; return 0; } Эта программа выдаст следующий результат, поскольку исключение целого типа не будет перехвачено инструкцией catch (double i): Start Inside try block Abnormal program termination Использование нескольких инструкций catch. C одним блоком try может быть связано несколько инструкций catch. Но разные инструкции catch должны перехватывать разные типы ис146
ключений. В общем случае, инструкции catch проверяются на соответствие типа в порядке их расположения в программе. Каждая инструкция catch отвечает только на свой тип исключений. Все остальные блоки catch игнорируются. Следующая программа обрабатывает исключения целого и строкового типа (листинг 12.8). Листинг 12.8 #include <iostream> void Xhandler(int test) { try { if (test) throw test; else throw "Value is zero"; } catch (int i) { cout << "Caught Exception #: " << i << '\n'; } catch(char *str) { cout << "Caught a string: "; cout << str << '\n'; } } int main() { cout << "Start\n"; Xhandler(1); Xhandler(2); Xhandler(0); Xhandler(3); cout << "End"; return 0; } Эта программа выдаст следующий результат на экран: Start Caught Caught Caught Caught End Exception Exception a strung: Exception #: 1 #: 2 Value is zero #: 3 147
Перехват всех исключений. В определенных обстоятельствах может потребоваться перехватывать все исключения, а не какойто конкретный тип. Для этого достаточно использовать следующую форму инструкции catch: catch (...) { // обработка всех исключений } Здесь многоточие соответствует любому типу данных (листинг 12.9). Следующая программа иллюстрирует использование catch (...): Листинг 12.9 #include <iostream> void Xhandler(int test) { try{ if(test==0) throw test; // генерация int if(test==1) throw 'a'; // генерация char if(test==2) throw 123.23; // генерация double } catch (...) { cout << "Caught One!\n"; } // перехват всех исключений } int main() { cout << "Start\n"; Xhandler(0); Xhandler(1); Xhandler(2); cout << "End"; return 0; } Программа выведет на экран следующий текст: Start Caught One! Caught One! 148
Caught One! End Три инструкции throw были перехвачены с использованием одной инструкции catch. Задание ограничений на исключения. Когда функция вызывается из блока try, можно ограничить тип исключений, которые эта функция может сгенерировать. Кроме того, можно вообще запретить ей генерировать исключения. Для задания этих ограничений необходимо добавить к определению функции ключевое слово throw следующим образом (листинг 12.10). Листинг 12.10 возвращаемый_тип имя_функции (список аргументов) throw (список типов) { //... } Здесь только типы данных, содержащиеся в списке типов, могут быть сгенерированы данной функцией. Эти типы следуют в списке, разделенном запятыми. Если будет сгенерировано исключение какого-либо другого типа, то произойдет аварийное завершение программы. Если необходимо, чтобы функция не могла сгенерировать никакого исключения, то следует оставить список пустым. Попытка генерации исключения, не поддерживающегося функцией, будет иметь своим результатом вызов функции unexpected(). Обычно эта функция в свою очередь вызывает функцию terminate(). Можно задать свой собственный порядок обработки непредвиденных исключений с помощью функции set_expected(). Следующая программа показывает, как ограничить набор типов исключений, которые могут быть сгенерированы функцией (листинг 12.11). 149
Листинг 12.11 // ограничение на генерируемые функцией типы #include <iostream.h> // данная функция и double void Xhandler(int if(test==0) throw if(test==1) throw if(test==2) throw } может сгенерировать только int, char test) throw (int, char, double) { test; // генерация int 'a'; // генерация char 123.23; // генерация double int main() { cout << "start\n"; try { Xhandler(0); // попытка передать 1 и 2 в Xhandler() } catch (int i) { cout << "Caught an integer\n"; } catch (char c) { cout << "Caught char\n"; } catch(double d) { cout << "Caught double\n"; } cout << "end"; return 0; } Повторная генерация исключений. Если возникает необходимость снова сгенерировать исключения из блока, который обрабатывает исключения, можно сделать это путем вызова throw без указания исключения. В результате текущее исключение будет передано во внешнюю последовательность try/catch обработки исключений. Следующая программа иллюстрирует повторную генерацию исключения типа char* (листинг 12.12–12.13) Листинг 12.12 // пример повторной генерации исключения #include <iostream.h> 150
void Xhandler() { try { throw "hello"; // генерация char * } Листинг 12.13 catch (char *) { // перехват char * cout << "Caught char * inside Xhandler\n"; throw; // повторная генерация char * извне функции } } int main() { cout << "Start\n"; try{ Xhandler(); } catch(char *) { cout << "Caught char * inside main\n"; } cout << "End"; return 0; } Эта программа выдаст на экран следующий текст: Start Caught char * inside Xhandler Caught char * inside main End 151
ЛЕКЦИЯ 13. СТАНДАРТНАЯ БИБЛИОТЕКА ШАБЛОНОВ Сущности STL. Стандартная библиотека шаблонов (STL — Standard Template Library) содержит более сотни различных шаблонов и алгоритмов. Это часть Стандартной библиотеки классов C++, которая может использоваться для хранения и обработки данных. В STL содержится несколько основных сущностей. Три наиболее важных — это контейнеры (способ организации хранения данных), алгоритмы и итераторы. Кроме них STL поддерживает распределители памяти, предикаты, функции сравнения. Контейнеры STL подразделяются на две категории: последовательные и ассоциативные. Наследниками последовательных контейнеров являются специализированные контейнеры: стек, очередь и приоритетная очередь. Последовательные контейнеры. В последовательных контейнерах каждый элемент связывается с другими посредством номера своей позиции в ряду, все элементы, кроме конечных, имеют по одному соседу с каждой стороны. Данные контейнеры предназначены для обеспечения последовательного или произвольного доступа к своим членам (или элементам). Примером последовательного контейнера является обычный массив. Также к этому виду контейнеров относятся векторы, списки, деки (очередь с двусторонним доступом), строки типа string. Для использования контейнера STL нужно подключить соответствующий заголовочный файл. В STL не нужно специфицировать размеры контейнеров. Передача информации о том, какие типы объектов будут храниться, происходит через передачу параметра в шаблон, например: vector<int> aVect; //создать вектор целых чисел (типа int) list<airtime> departurejist; //создать список типа airtime В табл. 13.1 сведены характеристики последовательных контейнеров STL. 152
Таблица 13.1 Контейнер Простой массив Характеристика Плюсы/минусы Постоянный размер + Быстрый случайный доступ (по индексу) - Медленная вставка или изъятие данных из середины - Размер не может быть изменен во время работы программы Вектор Расширяемый массив + Быстрый случайный доступ (по индексу) + Быстрая вставка или изъятие данных из хвоста - Медленная вставка или изъятие данных из середины Список Аналогичен связному списку + Быстрая вставка или изъятие данных из любого места + Быстрый доступ к обоим концам - Медленный случайный доступ Дек Вектор, с двухсторонним доступом + Быстрый случайный доступ + Быстрая вставка и изъятие данных из хвоста или головы - Медленная вставка или изъятие данных из середины Демонстрация обычного копирования контейнера deque представлена в листинге 13.1. Листинг 13.1 #include <iostream> #include <deque> #include <algorithm> using namespace std; int main() { int arr1[] = { 1, 3, 5, 7, 9 }; int arr2[] = { 2, 4, 6, 8, 10 }; deque<int> d1; deque<int> d2; for(int j = 0; j < 5; j++) {// перенос из массивов в очереди d1.push_back(arr1[j]); 153
d2.push_back(arr2[j]); } copy(d1.begin(), d1.end(), d2.begin()); // копирование из d1 в d2 for(int k = 0; k < d2.size(); k++) cout << d2[k] << ' ' << endl; return 0; } Ассоциативные контейнеры. Данные в ассоциативных контейнерах расположены непоследовательно, а доступ к ним производится через ключи. Среди ассоциативных контейнеров выделяют множества, мультимножества, карту и мулътикарту. Они хранят данные в виде дерева, то есть поддерживают быструю вставку, удаление и поиск данных. Особенность мультиконтейнеров — возможность хранения нескольких ключей для одного элемента. В табл. 13.2 представлена характеристика ассоциативных контейнеров. Таблица 13.2 Контейнер Множество (set) Характеристики Хранит только ключевые объекты. Каждому значению сопоставлен один уникальный ключ Мультимножество (multiset) Хранит только ключевые объекты. Одному значению может быть сопоставлено несколько неуникальных ключей Карта (mар) Ассоциирует ключевой объект с объектом, хранящим значение (целевым). Одному значению сопоставлен один уникальный ключ. Иногда карты называют ассоциативными списками или словарями Мультиокарта (multimар) Ассоциирует ключевой объект с объектом, хранящим значение (целевым). Один элемент может иметь несколько ключей (не обязательно уникальных) Ниже приведены некоторые специальные функции ассоциативных контейнеров: • begin() — итератор на первый элемент; • end() — итератор на элемент за последним; 154
• • • • empty() — true, если контейнер пуст; count(key) — количество элементов с данным ключом; find(key) — итератор на элемент с указанным ключом; erase(it), erase(start,end) — удаляет элемент с заданным итератором или между заданными; • size() — число элементов; • clear() — полная очистка контейнера. Способ создания ассоциативного контейнера подобен предыдущему: set<int> intSet; //создает множество значений int multiset<employee> machinists; /* мультимножество объ‐ ектов класса employee*/ Пример использования показан в листинге 13.2. Листинг 13.2 /* подсчет частоты встречи слов в тексте в процентах*/ #include <iostream> #include <string> #include <map> #include <fstream> using namespace std; int main() { map <string,int> words; ifstream in; in.open("in.txt"); // файл с текстом string word; while (in>>word) { words[word]++; } ofstream output_file; output_file.open("output_file.txt"); int count=0; map <string,int>::iterator curr_word; output_file<<"Words count:"<<endl; for (curr_word=words.begin();curr_word!=words.end();curr_w ord++) { 155
out‐ put_file<<(*curr_word).first<<":"<<(*curr_word).second <<endl; count+=(*curr_word).second; } output_file<<"Words percent :"<<endl; for (curr_word=words.begin();curr_word!=words.end();curr_w ord++) { float statistic = (*curr_word).second/count; statistic *= 100; output_file<<(*curr_word).first<<": "<< statistic <<"%"<<endl; } return 0; } Итераторы. В STL итератор представляет собой объект класса iterator. Их применяют вместо указателей для получения доступа к элементам контейнера (табл. 13.3). Итераторы можно инкрементировать с помощью оператора ++, после выполнения которого итератор станет ссылаться на следующий элемент. Получить значение элемента, на который ссылается итератор, можно с помощью его разыменовывания (оператор *). Объявление итератора имеет следующий синтаксис: Имя_контейнера <тип данных> :: iterator имя_итератора. Например: vector <float>::iterator begin; string::iterator end,cur; Для разных типов контейнеров используются свои итераторы. Для обращения к данным элемента в последовательных контейнерах достаточно перед именем итератора поставить *, но в ассоциативных контейнерах данный способ невозможен, так как в каждом элементе хранится несколько значений (ключ и данные). Для доступа к элементу используется следующий метод: 156
(*iter).first //для обращения к ключу (*iter).second //для обращения к данным //iter — итератор элемента Пример использования итератора для вывода элементов списка представлен в листинге 13.3. Листинг 13.3 // итератор и цикл for для вывода данных # include <iostream> #include <list> #include <algorithm> using namespace std: int main() { int arr[] = { 2, 4. 6, 8 }; list<int> theList; for(int k = 0; k < 4; k++) //заполнить список элемен‐ тами theList.push_back( arr[k]); // массива list<int>::iterator iter; //итератор для целочисленно‐ го списка for(iter = theList.begin(); iter != theList.end(); iter++) cout << *iter << ' '; // вывести список cout << endl; return 0; } Таблица 13.3 Тип итератора Запись/Чтение Хранение значения Направление Доступ С произвольным доступом Запись и чтение Возможно Оба направления Случайный Двунаправленный Запись и чтение Возможно Оба направления Линейный Прямой Запись и чтение Возможно Только прямое Линейный Выходной Только запись Невозможно Только прямое Линейный Входной Только чтение Невозможно Только прямое Линейный 157
Кроме обычных итераторов, в контейнере определены обратные (или реверсивные) итераторы, которые имеют тип reverse_iterator. Такие итераторы используются для прохода последовательного контейнера в обратном направлении, то есть от последнего элемента к первому. Инкремент обратного итератора приводит к тому, что он начинает указывать на предыдущий элемент, а декремент — к тому, что он указывает на следующий элемент. Демонстрация обратного итератора показана в листинге 13.4. Листинг 13.4 #include <iostream> #include <list> using namespace std; int main() { int arr[] = { 2, 4, 6, 8, 10 }; // массив типа int list<int> theList; for(int j =0; j<5; j++) theList.push_back(arr[j]); // перенести содержимое массива в список list<int>::reverse_iterator revit; // обратный итера‐ тор revit = theList.rbegin(); // реверсная итерация while( revit != theList.rend() ) // по списку cout << *revit++ << ' '; // с выводом на экран cout << endl; return 0; } Алгоритмы. В STL алгоритмы — это независимые шаблонные функции. Их можно использовать при работе как с обычными массивами C++, так и с контейнерами (предполагается, что в класс включены базовые функции). STL предоставляет около 60 универсальных алгоритмов, которые выполняют рутинные операции над данными, характерными для контейнеров (поиск, сортировка, обмен) и т. д. Все они определены в заголовочном файле <algorithm> в пространстве имен std. Все алгоритмы поиска возвращают итератор на элемент, а не сам элемент. Ниже приведены некоторые из них. 158
Алгоритмы поиска: find(begin,end,what) — ищет первый элемент со значенимем what в промежутке begin — end, где begin и end — итераторы соответствующего контейнера; adjacent_find(start,end) — ищет два последовательных совпадающих элемента между start и end и возвращает итератор на него; search(start,end,sbegin,send) — ищет между start и end последовательность sbegin-send. Пример использования функции find() показан в листинге 13.5. Листинг 13.5 #include <iostream> #include <algorithm> #include <list> using namespace std; int main() { list<int> theList(5); // пустой список для 5 значений list<int>:: iterator iter; // итератор int data = 0; // заполнение списка данными for(iter=theList.begin(); iter!=theList.end(); iter++) *iter = data += 2; // 2, 4, 6, 8, 10 // поиск числа 8 iter = find(theList.begin(), theList.end(), 8); if(iter != theList.end()) cout << "\nНайдено число 8.\n"; else cout << "\nЧисло 8 не найдено.\n"; return 0; } Алгоритмы сортировки: reverse(start,end) — инвертирует элементы последовательности start-end (сортировка в обратном порядке); random_shuffle(start,end) — сортировка элементов между start и end в случайном порядке; 159
sort(start,end) — сортировка элементов от start до end в порядке возрастания. Удаления элементов: remove(begin,end,what) — в промежутке begin-end удаляет все элементы, равные what; unique(begin,end) — удаляет все дубликаты. Другие функции: swap_ranges(start,end,start2) — меняет местами элементы от start до end с элементами от start2 до end2, end2 вычисляется автоматически, чтобы расстояние start-end совпал с start2-end2; replace(s,e,d1,d2) — в промежутке s-e элементы d1 меняет на d2; fill(begin,end,data) — заполнить промежуток begin-end значением data; copy(start,end,new_start) — копирует промежуток от start до end в new_start (все параметры — итераторы); count(sbegin,send,d) — подсчет количества элементов со значением d в промежутке sbegin — send; equal(start,end,start2) — возвращает true, если start-end = start2 — end2 (количество элементов start-end равно количеству элементов start2-end2); Пример использования алгоритма copy() представлен в листинге 13.6. Листинг 13.6 #include <iostream> #include <vector> #include <algorithm> using namespace std: int main() { int beginRange, endRange; int arr[] = { 11, 13,15, 17, 19, 21. 23, 25, 27, 29 }; vector<int> v1(arr, arr+10); // инициализированный вектор vector<int> v2(10); //неинициализированный вектор cout << "Введите диапазон копирования (пример: 2 5):"; 160
cin >> beginRange >> endRange: vector<int>::iterator iter1 = v1.begin() + beginRange; vector<int>::iterator iter2 = v1.begin() + endRange; vector<int>::iterator iter3; // копировать диапазон из vl в v2 iter3 = copy(iter1, iter2, v2.begin()); // (it3 ‐> последний скопированный элемент) Iter1 = v2.begin(); while(iter1 != iter3) cout << *iter1++ << ‘ ‘; cout << endl; return 0; } Функциональный объект — это объект шаблонного класса, в котором имеется единственный метод: перегружаемая операция(). Эти объекты используются в качестве параметра некоторых алгоритмов. Для использования функциональных объектов, определенных в библиотеке STL, необходимо подключить к программе заголовочный файл <functional>. Некоторые объекты-функции, включенные в STL, и их назначения: negate<type>() — операция !; plus<type>() — сложение; minus<type>() — вычитание; multiplies<type>() — умножение; divides<type>() — деление; modulus <type>() — вычисление остатка; equal_to<type>() — равенство; not_equal_to<type>() — не равенство; less<type>() — меньше; greater<type>() — больше; less_equal<type>() — меньше либо равно; greater_equal<type>() — больше либо равно; logical_not<type>() — не; logical_and<type>() — и (&&); logical_or<type> () — или (||). 161
Например, поэлементно сложить два вектора a и b, содержащие значение типа double, и поместить результат в a, можно следующим образом: transform(a.begin(), a.end(), b.begin(), a.begin(), plus<double>()); Пример сортировки массива типа double по убыванию с использованием функционального объекта greater<>() (листинг 13.7). Листинг 13.7 #include <iostream> #include <algorithm> //для sort() #include <functional> //для greater<> using namespace std; // массив double double fdata[] = { 19.2, 87.4, 33.6, 55.0, 11.5, 42.2 }; int main() { // сортировка значений double sort(fdata, fdata + 6, greater<double>()); // вывести отсортированный массив for(int j=0; j < 6; j++) cout << fdata[j] << ‘ ‘ ; cout << endl; return 0; } 162
БИБЛИОГРАФИЧЕСКИЙ СПИСОК Лафоре Р. Объектно-ориентированное программирование в C++ Object-Oriented Programming in C++ Серия: Классика Computer Science Издательство: Питер, 2012г. — 928 стр. Прата С. Язык программирования С++. Лекции и упражнения. Вильямс. — 2007. Шилдт Г. С++ базовый курс. Вильямс. — 2008. Шилдт Г. Полный справочник поС++. Вильямс. — 2007. Уэйт М., Прата С., Мартин Д. Язык Си. Руководство для начинающих — М. : «Мир», 1988. — 512 с. 163
Учебное издание Шурыгин Владимир Николаевич Объектно-ориентированное программирование Конспект лекций для студентов, обучающихся по направлению 230400 — Информационные системы и технологии Редактор Е.Б. Казакова Компьютерная верстка Е.А. Бариновой Подписано в печать 01.09.14. Формат 60×84/16. Бумага офсетная. Печать на ризографе. Усл. печ. л. 9,53. Тираж 200 экз. (1-й завод 50 экз.) Заказ № 13. Московский государственный университет печати имени Ивана Федорова. 127550, Москва, ул. Прянишникова, д. 2А. Отпечатано в Издательстве МГУП имени Ивана Федорова. 164