Text
                    В. М. Бондарев
Программирование на
Cw

В. М. Бондарев Программирование на C++ Рекомендовано Министерством образования и науки Украины как учебное пособие Харьков «Компания СМИТ» 2005
УДК 004 Б-81 Рецензенты: Жолткевич Г* Н. — д.т.н., проф., Харьковский национальный университет им. В. Н. Каразина; Руденко О. Г. — д.т.н., проф., зав. каф. ЭВМ, Харьковский национальный университет ра- диоэлектроники . Бондарев В. М. Б—81 Программирование на C++. 2-е изд. — Харьков: «Компания СМИТ», 2005.— 284 с. ISBN 966-8530-50-0 Книга содержит компактное изложение основ алгоритмичес- кого языка C++ и состоит из трех частей. В первой части дано вве- дение в программирование на C++, основной темой второй части является объектно-ориентированное программирование, третья часть вводит читателя в обобщенное программирование и знако- мит со стандартной библиотекой шаблонов. Для тех, кто только пробует писать программы, в книге есть приложение, с которого им стоит начать. Для студентов вузов специальностей «Программное обеспече- ние автоматизированных систем», «Информатика», «Прикладная математика» и всех, кто хочет научиться программировать на C++. ISBN 966-8530-50-0 © Бондарев В. М., 2005 © «Компания СМИТ», 2005
Предисловие Сегодня существует много книг, посвященных программиро- ванию на C++ — важной составной части образования програм- миста. Тем не менее, эта книга имеет право на существование по нескольким причинам. Во-первых, ее содержание согласовано с учебными программами таких дисциплин, как «Основы про- граммирования» и «Объектно-ориентированное программирова- ние». Во-вторых, автор имеет опыт преподавания этих дисциплин, что позволило ему изложить некоторые традиционно трудные для понимания вопросы просто. В-третьих, несмотря на полноту ох- вата материала, объем книги не велик, и это делает ее доступной подавляющему большинству студентов. Даже если не упоминать о других особенностях книги: строгое следование стандарту язы- ка C++, наличие задач в конце каждого раздела, множество ко- ротких примеров программного кода, поясняющих материал, перечисленного достаточно, чтобы сделать пособие полезным как студентам, так и преподавателям. Книга состоит из трех частей. В первой части дано введение в программирование на C++, рассмотрены управляющие конст- рукции языка, массивы, указатели, функции, работа со строка- ми, потоковый ввод и вывод. Основной темой второй части яв- ляется объектно-ориентированное программирование на C++. Изучается наследование, полиморфные классы, множественное наследование и другие особенности, присущие языку: обработ- ка исключений, перегрузка операторов, приведение типов, рас- пределение памяти. Третья часть вводит читателя в обобщенное программирование и знакомит со стандартной библиотекой шаб- лонов — неотъемлемой частью C++ и необходимым инструмен- том профессионального программирования.
4 1 ]редисловие В книге есть большое приложение, предназначенное для тех, кто только начал изучать программирование. Последнее, как из- вестно, отнюдь не сводится к изучению какого-то алгоритмическо- го языка. Наиболее ценным качеством программиста является его умение разработать хороший алгоритм для будущей программы, независимо от того, какой она будет — большой или маленькой. Поэтому в приложении рассматриваются основные понятия и на- чала технологии разработки алгоритмов. В качестве примеров при- ведены классические алгоритмы из области поиска и сортировки данных. Уделено внимание рекурсивным алгоритмам, без знания которых не может быть полноценного программиста. Первая часть пособия вместе с приложением дают материал для курса «Основы программирования», вторая и третья части бу- дут необходимы в курсе «Объектно-ориентированное программи- рование» . Задания, приведенные в конце разделов, могут быть ис- пользованы преподавателем при проведении практических занятий и лабораторных работ. Автор хотел бы выразить искреннюю благодарность всем, кто принял деятельное участие в подготовке этой книги к печати (их име- на вы можете увидеть в выходных данных) и сказать отдельное спа- сибо компании Validio Software, Ukraine (http: //www. validio.com.ua) за понимание и поддержку.
чисты Основы языка 1. Введение в C++ 1.1. Что такое C++ C++ — универсальный алгоритмический язык, применяемый для разработки системных и сложных прикладных программ. В на- стоящее время C++ — не только самый распространенный язык программирования, но и язык общения программистов, посколь- ку большинство публикуемых алгоритмов записывается на C++. Первую версию языка C++ предложил Бьярн Страуструп в 1981 году. С тех пор язык интенсивно развивается. В 1995 году автором языка совместно с организациями ANSI и ISO разработан стандарт языка (Standard C++), которым должны руководствовать- ся все разработчики компиляторов. C++ включает язык С как подмножество, имеет строгую ти- пизацию, модель объектно-ориентированного программирования и развитые средства расширения и адаптации языка к различным видам задач. 1.2. Простейшая программа Перед вами минимальная программа на C++. U_____________________________________ #include <iostream> using namespace std; void main() { cout « "Hello!";
6 Часть 1 Замечание. Вместо двух строк (стандарт) !г_______________________________________ #include <iostream> using namespace std; можно написать одну строку (старый стиль). U_________________________________________________________ | ^include <iostream.h> Все программы должны иметь функцию с именем main. Определение функции состоит из заголовка void main( ) и последо- вательности инструкций, заключенных в фигурные скобки. Слово void в заголовке говорит о том, что функция не должна вырабаты- вать и возвращать значение. Программа состоит из единственной инструкции, которая вы- водит в стандартный выводной поток cout (console output) стро- ковую константу. Директива компилятора #include вставляет в текст програм- мы содержимое текстового файла iostream. Это заголовочный файл, в котором находится объявление средств ввода-вывода в потоки. В частности, там определены объект cout и оператор помещения в поток «. Инструкция using namespace std; расширяет пространство имен, доступных в нашей программе. Подробнее об этом будет ска- зано позже. Большие и маленькие буквы в языках С и C++ различаются. Замечание. Если вы работаете в среде MS Visual C++, запус- кайте программу на трансляцию и выполнение командой меню Build/Execute (клавиша Ctrl+F5). 1.3. Консольные программы в Visual C++ Программы Win32, имитирующие текстовый режим работы компьютера и не имеющие графического интерфейса, называются консольными. Они больше всего подходят для начального освое- ния C++, т.к. не добавляют к сложности языка сложность програм- мирования оконного интерфейса.
Основы языка 7 Чтобы создать консольное приложение в системе программи- рования Visual C++, которая входит в состав MS Visual Studio 6, необходимо: а) выбрать пункт меню File /New (запустится утилита AppWizard); б) в диалоговом окне на вкладке Projects выбрать вид проекта Win32 Console Application; , в) там же в поле Location ввести путь к каталогу проекта, на- пример, D:\MyPrograms; г) там же в поле Project name ввести имя проекта, например, nHello* и нажать кнопку Ofe; д) в появившемся окне выбрать разновидность проекта A simple application и нажать кнопку Finish; е) в следующем окне подтвердить выбранные опции, нажав кноп- ку Ok. Для работы с проектом в Visual C++ имеются следующие окна: Workspace — показывает на двух вкладках состав классов и со- став файлов проекта; Output Window — показывает сообщения компилятора, сбор- щика и отладчика; CodeEditor — показывает содержание файлов. В результате получится несколько файлов, основной из них Hello.cpp — ваша программа. #include "stdafx.h" int main(int argc, char* argvl]) { return 0; } Переделайте ее так, как хотите, но не выбрасывайте первую строку ttinclude "stdafx.h" ! Выполните полную компиляцию проекта (меню Build/ Rbuild All). Запустите программу на выполнение (меню Build/Execute Hello.exe или Ctrl+F5). Результаты вы увидите в окне консольного приложения. После внесения изменений в код программы только нажмите Ctrl+F5. Пример. Ввести два целых числа, сложить их и результат вы- вести.
8 Часть 1 #include <iostream> using namespace std; void main() { int x, y; cin » x >> y; cout << "Сумма равна " << x * * у « endl; } Строка int x, у; определяет две целые переменные. Она означа- ет то же, что int х> int у; или int х; int у;. Определение говорит транс- лятору, что для переменных хну надо выделить память. Другие числовые типы в C++: • char — целое 1 байт; • short — целое 2 байта; • long — целое 4 байта; • float — вещественное 4 байта; • double — вещественное 8 байт. Замечание. Стандарт языка не устанавливает объем памяти, отводимой под переменные встроенных типов. Этот объем оп- ределяется реализацией компилятора. Выше приведены зна- чения для Visual C++. Имя cin (console input) обозначает стандартный входной по- ток, а знак «»» — операцию извлечения из потока. Каждая операция в C++ вырабатывает значение. Значением опе- рации cin » х является cin. Это позволяет вместо двух инструкций cin » х; cin » у; записать одну cin >> х >> у; которая понимается как (cin » х) » у;. Как и прочие бинарные операции, операция » левоассоциа- тивна. Последняя строка программы cout << “Сумма равна " « х + у << endl; выводит в выходной поток слова "Сумма равна ”, затем значение суммы, после чего перемещает курсор в начало следующей строки. Перемещение курсора происходит из-за вывода манипулятора endl.
Основы языка 9 Тело функции представляет собой составную инструкцию. Составная инструкция строится по схеме: {инструкция инструкция ... инструкция}. Такие части программы как: int х,у cin » х >> у cout « "Сумма " << х 4- у << endl являются выражениями. Любое выражение становится инструк- цией, если после него поставить точку с запятой. 1.4. Организация повторений Формат инструкции цикла следующий: while (выражение) инструкция. Логические значения в языке C++ могут передаваться число- выми значениями: О — ложь, не 0 — истина. Пример. Вводить с клавиатуры целые числа и суммировать, пока не встретится число 0. Сумму вывести на экран. и ________________________________________________________ float sum - 0, х; cin » х; while (х != 0) { sum ~ sum + х; cin » х; } cout « sum « "\n"; Тот же код можно подправить, и он будет работать чуть быстрее. и________________________________________________________ float sum - 0, х; cin » х; while (х) { sum += х; cin >> х; } cout << sum « "\n";
10 Часть I Определение переменных может сопровождаться их инициа- лизацией, т.е. транслятор не только отведет память под перемен- ную, но и заполнит ее указанным значением. Глобальные перемен- ные по умолчанию инициализируются нулем. В языке C++ есть инструкция цикла с постусловием: do инструкция while (выражение);. В обоих циклах условием повторения является ненулевое зна- чение выражения в круглых скобках. Условия х, х/ = 0 и !(х === 0) эквивалентны. Операция присваивания А += В означает то же, что А = А + В, но выполняется быстрее, т.к. адресное выражение А вычисляется один раз, а не два. То же относится к операциям: *== /= % = +« - = «== »== &= |«=. Присваивание в C++ — не инструкция, как в Паскале, а право- ассоциативная операция (только присваивание правоассоциатив- но, остальные бинарные операции левоассоциативны). Ее значение равно значению правого операнда, поэтому выражения: а = b = с и а = (Ь — с) равносильны. Напомним, что точка с запятой после вы- ражения превращает его в инструкцию, поэтому: а = Ъ; и а = b = с; уже инструкции присваивания. Пример. Решить последнюю задачу, используя цикл с пост- условием. и_________________________________________________________ float sum - 0, х; do { cin >> х; sum += х; } while(х) cout « sum « "\n"; 1.5. Условная инструкция Схема условной инструкции такова: if (выражение) инструкция [else инструкция].
Основы языка П Пример. Вводить вещественные числа с клавиатуры. Отдель- но суммировать положительные и отдельно отрицательные числа. U_____________________________________________________ float pozsum = 0, negsum = 0; float х - 1; // это, чтобы войти в цикл наверняка while (х) { сin >> х; if (х > 0) pozsum += х; else negsum += x; } cout « "Положительная сумма “ « pozsum << endl; cout << "Отрицательная сумма *’ << negsum « endl; В языке C++ имеются следующие логические операции: && — «и», ||—«или», ! — «не». 1.6. Инструкция цикла for В языке C++ имеется еще одна инструкция цикла: for ( инструкция_инициализации [выражение!]; [выражение2] ) инструкция. Смысл инструкции for таков: и инструкция_инициализации while (выражение!) { инструкция выражение 2; }_____________________________________________________ Пример. Вводить с клавиатуры вещественные числа и сум- мировать, пока не встретится число 0. Вывести на экран среднее арифметическое. и______________________________________________________ [ float sum - 0, х - 1; |
12 Часть 1 for (int count - 0; x; count++) { cin >> x; sum +- x; } cout << suit. / count << endl; Переменная count определена прямо в инструкции for. В сре- де разработки Visual C++ ею можно пользоваться и после заверше- ния цикла, хотя стандарт языка это запрещает. 1.7. Задачи Задача 1 Введите 10 чисел с клавиатуры. После этого: а) выведите на экран наибольшее число; б) выведите два наибольших числа; в) выведите три наибольших числа. Задача 2 Найдите сумму ряда: а) 1 + 1/2 + 1/3+...+ 1/N; б) 1 + 2 + 3 + ... + N; в) sqr ( 2 + sqr ( 2 + sqr ( 2 + ... + sqr ( 2 )...))) — N раз. Задача 3 Дано десятичное целое число. а) сосчитайте сумму всех его цифр; б) сосчитайте знакопеременную сумму его цифр, но так, чтобы цифра в старшем разряде всегда имела знак «+», например, для 1953 сумма равна +1-9 + 5-3. Задача 4 Дано целое число. Напечатайте его в двоичной форме. Сделайте то же для вещественного числа. Задача 5 Дано уравнение 2 - cos(x) - х-х = 0. Известно, что на отрезке [0.5,2.5] имеется ровно один корень. Найдите его с точностью 0.0001.
Основы языка 13 2. Массивы 2.1. Одномерные массивы Пример. Ввести 10 чисел и сохранить их в памяти. U______________________________________________ float а[10]; for (int i = 0; i < 10; i++) cin >> a[i]; При определении массива количество элементов задается кон- стантным выражением. Если массив состоит из п элементов, эле- менты массива нумеруются целыми числами 0, 1, 2,..., п - 1. В определении float а[10] оператор [] можно рассматривать как операцию над базовым типом float и целым значением 10. Результатом операции является новый тип — массив из 10 вещест- венных чисел. Необычным является лишь то, что обозначение типа здесь не предшествует имени переменной, как в int п; или float* pf; а окружает его. Логичнее было бы float[10] а; но это противоречит принципу совместимости с языком С. С помощью объявления typedef новому типу можно дать соб- ственное имя и пользоваться им для определения переменных. V________________________________________________________ typedef float MyArray[10]; MyArray a; Инициализация массива в C++ выполняется при помощи списка значений. .Пример. Объявить, определить и инициализировать числа- ми 1, 2, 3 целый массив из 3 элементов. и______________________________________________________ | int гп[] - {1,2,3}; ] Все элементы массива располагаются в памяти плотно, один за другим. Чтобы обратиться к шестому элементу массива а, надо написать а[5]. Здесь [] адресная операция, которая из имени (т.е. адреса) массива и числа 5 вырабатывает адрес шестого по порядку элемента массива.
14 Часть 1 2.2. Двумерные массивы Двумерный массив — это одномерный массив из одномерных массивов. Для примера определим двумерный массив т размером 3x4. U_________________________________________________ [ int m [ 3 ] [4 ] ; [ Покажем, что такое определение логически вытекает из пред- ставления о двумерном массиве как об одномерном массиве из одно- мерных же массивов. Начнем с того, что сначала объявим промежуточный тип М — одномерный массив из 4-х целых чисел и_____________________________________________________ | typedef intМ [ 4] ; | Теперь определение М х; означает точно то же самое, что int х[4];, каким бы ни было выражение х. Определим одномерный массив из элементов типа М, т.е. мас- сив массивов. U ____________________________________________________ [ М ш[3] ; ~~] В роли х здесь выступает выражение пг[3]. Подставим его в int х[4]; и получим объявление двумерного массива без проме- жуточного типа М, как это обычно и делается. U_____________________________________________________ [ int m[3] [4] ; | Исходя из общего принципа размещения элементов массива в памяти (плотно и в порядке возрастания номеров), можно утвер- ждать, что элементы двумерного массива т[3][4] займут места в памяти в следующем порядке: т[0][0], т[0][1], т[0][2], т[0][3], m[l][OJ, т[1][1], т[1][2], т[1][3], т[2][0], т[2][1],т[2][2],т[2][3]. Пример. Ввести матрицу М размера 3x3. Поменять местами начальную и последнюю строки и вывести результат на экран, и_____________________________________________________ // Объявить, определить и инициализировать массив intM[3][3] = {{11,12,13}, {21,22,23}, {31,32,33}};
Основы языка 15 // поменять местами строки for (int j = 0; j < 3; j++) int R = M[0][j]; M[0][j] - M[l][j]; M[l] lj] = R; } { // вывести for (int i for (j : cout cout << } массив построчно = 0; i < 3; i++) - 0; j < 3; j++) { « M[i][j] « " end!; { f 2.3. Сортировка и поиск в массивах Для упорядочения небольших массивов можно использовать сортировку пузырьком, выбором или вставками, одним словом, алгоритмы с оценкой временной сложности О(п х п). Для упорядочения больших массивов применяют быструю сор- тировку Хора или сортировку слиянием, т.е. алгоритмы с оценкой временной сложности О(п log(rc)). Поиск числа в упорядоченном массиве ведут методом половин- ного деления. Временная сложность такого поиска оценивается как O(log(n)). Описание и оценку сложности всех перечисленных алгорит- мов можно найти в приложении. 2.4. Задачи Задача 1 Задан массив из 100 вещественных чисел. Найдите наиболь- шее число в массиве и место, на котором оно расположено. Задача 2 Задан массив чисел. Частично отсортируйте его, т.е. переставь- те числа так, чтобы все числа, меньше первого, предшествовали ему, а все числа, больше первого, следовали за ним.
16 Часть I Задача 3 Задан массив из 100 000 символов. Отсортируйте его как мож- но быстрее. Задача 4 Заполните массив из N элементов простыми числами в поряд- ке возрастания. Задача 5 Запрограммируйте двоичный поиск в массиве. Алгоритм дво- ичного поиска можно найти в приложении. 3. Указатели 3.1. Адресный тип данных Указатели применяются для работы с массивами, со свобод- ной памятью и в качестве параметров функций. Указатель — это тип данных, значением которого является адрес данных определенного типа. Бывают и бестиповые указате- ли, которые хранят просто адрес памяти, но в C++ они применяют- ся редко. Значение указателя можно получить: а) определив, где расположена в памяти некоторая переменная: б) выделив участок свободной памяти для хранения значений; в) при помощи арифметической операции над целым числом и другим указателем. Синтаксис определения указателя следующий: тип *имя; где тип — тип значений, на которые сможет указывать указатель; имя — имя переменного указателя. Примеры определений указателей. int* зп; // указатель на целое значение float *pf 1, *pf 2 ; // два указателя на вещественные
Основы языка 17 Новому типу можно дать собственное имя при помощи инст- рукции typedef. Пример объявления типа. И____________________________________________________ typeclef int* integer_p; integer p pn; Существует лишь одна адресная константа 0, которая озна- чает, что указатель никуда не указывает. Указателю нельзя присвоить, но можно добавить целое значение. U__________________________________________ рп - 0; // допустимо, 0 означает пустой указатель pn -- 1; // не допустимо рп += 3; // тоже допустимо, но не всегда корректно Смысл выражения рп + 3 в том, что к адресу, хранящемуся в рп, добавляется утроенный размер базового типа, в данном слу- чае типа int. 3.2. Операции new и delete Операция new, выполненная над некоторым типом, резерви- рует место в свободной памяти (в куче), необходимое для хранения значения этого типа. Результатом операции является адрес выде- ленной памяти или 0, если выделить память не удалось. Тип ре- зультата — «указатель на базовый тип». /Зк Замечание. Standard C++ выбрасывает исключение в случае СдЛ невозможности выделить память. Исключения будут обсуж- даться позже. Пример. Выделение памяти для вещественного числа. U___________________________________________________ float* pf; pf = new float; // или float* pf = new float;
18 Часть 1 После того, как свободная память выделена, ею можно пользо- ваться при помощи операции разыменования, U________________________________________________________ *pf = 3.14; float f - *pf + 1.414; Операция new не инициализирует выделяемую память, т.е. не заполняет ее никакими значениями. Если нужна инициализация, ее надо выполнять явно. 4_________________________________________________ | float* р£ = new float(0); If заполнение нулем | Когда надобность в выделенной памяти отпадет, ее надо осво- бодить при помощи операции delete. U_______________________________________________________ f delete pf;] 3.3. Разыменование и взятие адреса Основной операцией при работе с указателями является полу- чение доступа к значению, адрес которого хранится в указателе. Например, и___________________________________________ int *рп, п; *рп - 5; п •- *рп; Выражение *рп имеет такой же смысл, как имя целой пере- менной. Операция «*» называется разыменованием. Действие, обратное к разыменованию, позволяет получить адрес переменной по ее имени. Например, рп = &п;ч эта операция называется взятие адреса. 3.4. Связь между массивами и указателями Хотя указатель char* т и массив char т[ 100] — переменные разных типов, имя массива рассматривается транслятором как ука- затель на начальный элемент массива. Адресные выражения
Основы языка 19 m[n] и *(ш + п) эквивалентны. Замечание. Из эквивалентности адресных выражений сле- Ш дует: т[и] <=> *(т + п) <=> *(п + т) о п[т] т.е. обратиться к элементу массива т[п] можно и в форме п[т]. Пример. Скопировать содержимое массива А в массив В. и____________________________________________________ int А[]={1,2,3,4,5,6,7,8,9}, В[9]; int i; // 1-й способ, самый понятный for (i = 0; i < 9; i++) В [ i ] - A [ i ] ; // 2-й способ for (i = 0; 1 < 9; i + + ) * (B + i) ~ * (A + i) ; // 3-й способ int *a, *b; for (a = A, b = B, i ~ 0; i < 9; i++) *(b++) - *(a + +); Вычитание указателей имеет смысл, только если оба они ука- зывают на элементы одного массива. Связь между массивами и указателями распространяется и на двумерные массивы. т[i][j] означает то же самое, что *(*( m + i) + ]). Замечание. Пусть массив определен как int т[3][4].В соот- С&З ветствии с арифметикой указателей транслятор вычисляет адресное выражение *(*(т + i) + j) или, что то же самое, rn[i][j] так: (адрес начала массива m) + i * (размер одномерного массива из 4-х целых чисел) 4- j * (размер целого числа).
20 Часть 1 Отсюда следует, что для вычисления адресного выражения m[i][j] транслятору необходимо знать лишь вторую размерность массива, ио не первую. Выражение m[i] рассматривается транслятором как имя одномерного массива из 4-х целых чисел, поэтому все следующие выражения правильны и обозначают одно и то же, а именно, т[2][3]: *(т[2]+3), (*(т + 2))[3], 3[т[2]], 3[2[т]]. 3.5. Массивы в свободной памяти С помощью операции new можно размещать массивы в свобод- ной памяти. Следующий оператор выделяет намять под массив из 50 элементов типа long. и____________________________________________________ [ long *m - new long [ 50] ; ] Замечание. Оператор new не позволяет выделить память под dZb массив и сразу инициализировать ее, как это делается для простых типов данных. Занятая память высвобождается оператором U_______________________________________________ [ delete[] m; [ При выделении блока памяти размер его запоминается. Это позволяет освобождать память без указания размерности массива, но оператор индексации писать необходимо, т.к. в противном слу- чае освободится память, занятая только одним начальным элемен- том массива. Пример. Ввести 5 вещественных чисел и разместить их в сво- бодной памяти. и float *f;. f - new float[5]; for (int i ~ 0; i < 5; i ++-) {
Основы языка 21 cin » f[i]; } delete[] f; 3.6. Двумерные массивы в свободной памяти В свободной памяти можно разместить и двумерный массив. Для этого нужно вспомнить, что двумерный массив — это одномерный массив, элементами которого являются одномерные же массивы. Чтобы разместить в куче целый массив из 3 «строк» и 4 «столбцов»: У______________________________________________________ typedef int М[4] ; // М - промежуточный тип М* m - new М[3]; // создадим массив обычным способом То же самое можно сделать без промежуточного определения: V__________________________________________________ | int (*m)[4] = new int[3][4]; | Круглые скобки обязательны, т.к. без них объявление int *пг[4] означает, что т является массивом из 4-х указателей на целое, а не указателем на массив из 4-х целых чисел. Пользоваться таким массивом можно обычным образом: U I ГП[1] [2] = 20; I Удалит массив из памяти оператор U__________________________________________________ [delete[] m; [ 3.7. Задачи Задача 1 Создайте в свободной памяти целый массив размером 10 х 10, заполните его значениями от 0 до 99, а затем поверните содержи- мое массива на 90 градусов по часовой стрелке. Повернутый мас- сив распечатайте.
22 Часть 1 Задача 2 Двумерный массив размером 100 х 200 заполнен числами. Выясните, есть ли среди них одинаковые. Задача 3 Двумерный массив размером 100 х 200 заполнен числами. Сосчитайте, сколько среди них различных чисел. Задача 4 Двумерный массив заполнен единицами и нулями и символи- зирует лабиринт. Нули означают свободное пространство, а едини- цы — препятствия. Где-то посреди лабиринта находится путник. Ответьте, за какое наименьшее число шагов он может добраться до края лабиринта. Перемещаться можно вертикально или горизон- тально на соседнюю клетку, если она содержит нуль. Задача 5 Разместите в свободной памяти 5-мерный массив размером 2х2х2х2х2. Заполните его числами от 0 до 31. 4. Функции В C++, как и во всех других процедурных языках, функции нужны, чтобы разделить программу на относительно независимые части и кодировать каждую часть отдельно от других. 4.1. Параметры функции В C++ параметры передаются функции по значению. Это зна- чит, что если аргумент — переменная, то функция получает копию этой переменной, которая существует лишь пока выполняется тело функции. Пример. Определить функцию, которая складывает 2 числа и возвращает сумму в вызывающую программу. U___________________________________J_______________ float sum(float a, float b) {
Основы языка 23 return а + b; void main( ) { cout sum(3, 5); Возврат из функции выполняется инструкцией return. Ее не- обязательным элементом является выражение для возвращаемого значения. Инструкция return без выражения просто передает уп- равление вызывающей функции. Объявление функции можно отделить от ее определения. В этом случае объявление может не содержать имен параметров. | float sum(float, float); [ Определения функций не могут быть вложенными. Функция может не иметь параметров, может не возвращать значения. [ void &npty() {return;} | Вызов функции возможен только в составе выражения, но выражение может состоять из единственного вызова функции. | s = sum(x, у) ; | Если в программе нам необходимо действие, выполняемое фун- кцией, но не требуется возвращаемое значение, мы должны пре- вратить выражение в инструкцию, поставив после него точку с за- пятой. sum(x, у) ; 4.2. Выходные параметры функции Иногда требуется, чтобы функция изменяла значение пере- менных, передаваемых ей как параметры. Поскольку в C++ па- раметры передаются по значению, это невозможно, но можно
24 Часть 1 передать функции не сами переменные, а их адреса, т.е. указа- тели на них. Пример. Определить функцию, которая меняет местами зна- чения двух переменных. U________________________________________________________ void swap(int *az int* b) { int r; r = *a; *a ~ *b; *b - r; } Функция вызывается так: Я______________________ int х = 5, у ~ 3; swap(&х, ; Результатом операции взятия адреса &х является указатель на х9 операция &у дает нам указатель на у. Указатели передаются функции в качестве параметров и не меняются в результате выпол- нения функции, но меняются те значения, на которые направлены указатели. 4.3. Ссылки Как видно из предыдущего примера, для работы внутри функ- ции с выходным параметром необходимо: а) в вызывающей программе получить адреса переменных опе- рацией взятия адреса «<£»; б) в теле функции обращаться к этим переменным с помощью операции разыменования « *». Чтобы исключить эту рутинную работу, в C++ введена специ- альная разновидность указателей, называемая ссылками. Ссылки — это указатели, над которыми операция разыменования выпол- няется неявно. Пример. Решение задачи об обмене переменных с использо- ванием ссылок. V____________________________________________________ |~~void swap (int &а, int &b)
Основы языка 25 { int: г; г - а; а ~ b; b - г; } Функция вызывается так: и______________________ int х = 5, у - 3; swap(х, у); Замечание. В программировании такой способ обмена дайны- ми с функцией называется передачей параметров по ссылке. Большие массивы для экономии времени и памяти следует передавать по ссылке, даже если их значение не изменяется функ- цией. Чтобы предотвратить их случайное изменение, передают не просто ссылку, а ссылку на константу. и_____________________________________________________ [ void f(const large &a, const~large *b}; ^2 Используйте спецификатор const, чтобы обезопасить програм- му от случайных изменений параметров. Ссылки редко используют за пределами функций, но, вообще говоря, это возможно. При этом нужно помнить, что ссылка — это разновидность указателя, который инициализируется только од- нажды, в момент своего определения. Синтаксис определения ссылки следующий: тип& имя = инициализирующее выражение;. Инициализирующее выражение для ссылки должно быть lvalue, т.е. таким, что может стоять в левой части оператора при- сваивания. Для ссылки на константу такого ограничения нет. Если ини- циализирующее выражение не lvalue, создается временный объект, хранящий значение инициализирующего выражения. Объект су- ществует, пока не закончится время жизни ссылки. V_____________________________________________________ int п = 5; // ссылка rl - синоним п ant &rl = п; // ссылка г2 - временная переменная
26 Часть I | const int. &r2 = n + 1; | После инициализации ссылки-синонима все операции над ней фактически выполняются над переменной, которой она проиници- ализирована. rl — 6; rl*-+; // равносильно п = б // равносильно п+^- 4.4. Возврат ссылки Функция может не только принимать параметры-ссылки, но и возвращать ссылку в программу. Пример. Определение функции, которая возвращает ссыл- ку на больший из двух аргументов. int& refmax(int &а, int &b) { if (a >= b) return a; else return b; Значением данной функции является адресное выражение, ко- торое можно использовать и в левой части операции присваивания. U_________________________________________________________ int х = 5, у = 8; refmax(x, у) = 10; Возврат значения функцией происходит по правилам иници- ализации, а не присваивания. Это значит, что если возвращается ссылка на неадресное выражение (локальную переменную, ариф- метическое выражение), создается временный объект (см. выше об инициализации ссылок). Разумное применение таким функциям найти трудно, т.к. они ничем не отличаются от функций, возвра- щающих значение, а не ссылку.
Основы языка 27 4.5. Одномерные массивы как параметры Чтобы передать массив в функцию, ей надо сообщить адрес начала массива и количество его элементов. Это можно сделать при помощи двух параметров. Пример. Определить функцию, которая возвращает сумму всех элементов массива. float sum (float а[], int n) { float s ~ 0; for (int 1 ~ 0; i < n; i + + ) s + -~ a [i] ; return s; } Задать указатель на начальный элемент можно по-разному. Следующие два заголовка равносильны заголовку функции из пре- дыдущего примера: U ________________________________ float sum(float *а, int n); float sum(f'Loat a[1000], int n) ; Замечание. Поскольку C++ не предусматривает проверку СодЬ выхода за границу массива во время выполнения программы, компилятор просто игнорирует константу в квадратных скоб- ках в параметре-массиве. Рассмотренный способ передавать массив в функцию не един- ственный. Можно, например, сделать это при помощи двух указа- телей — на первый и на последний элемент массива. и________________________________________________________ [float sum(float *begin/ float *end); ' | Впрочем, для программиста удобнее, если второй параметр указывает не на последний элемент массива, а на «элемент», сле- дующий за последним.
28 Часть 1 u float sum(float *begin, float *end) { float s = 0; while (begin != end) s += *(begin++); return s; } float m[100] = {1.2}; cout « sum(m, m + 100) << endl; 4.6. Двумерные массивы как параметры Когда параметром является двумерный массив, его вторая раз- мерность обязательно указывается в объявлении функции float а[ 14]. Объявление параметра как float а[ ][ ] недопустимо. Нет ничего странного в том, что компилятору требуется вто- рая размерность, ведь именно она определяет размер тех одномер- ных массивов, которые составляют двумерный. Пример. Определить функцию, которая распечатывает дву- мерный массив а[3][4] в виде матрицы. V_______________________________ void print (float а[][4], int sizel) { for (int i - 0; i < sizel; i++) { for (int j = 0; j<4; j++) cout << a[i] [ j ] «. " cout << endl; } } Заголовок функции в предыдущем примере мог быть и таким: U_________________________________________________ | void print (float (*а) [4],' int sizel). j
Основы языка 29 Параметр а здесь является указателем на одномерный массив из четырех вещественных чисел. Если вам не нравится передавать в функцию двумерные мас- сивы, можно передать двумерный массив как одномерный, и в теле функции вычислять местоположение элемента. U__________________________________________________ void printl(float а[], int sizel, int size2) { for (int i = 0; i < sizel; i++) { for (int j ~ 0; j < size2; j++) cout << a[i * size2 + j] << " "; cout« endl; } } Чтобы правильно вызвать эту функцию, надо передать ей в пер- вом параметре адрес не двумерного массива тп, а одномерного иными словами не float**, а просто float*. float m[][4] - {{11, 12, 13, 14} , {21, 22 , 23, 24} , {31 . 32, 33, 34}}; printl(*m, 3, 4); Этого же можно добиться явным приведением типа. U_________________________________________ [printl((f loat* )in~ 3, 4) ; 4.7. Задачи Задача 1 Определите функцию, которая получает трехмерный массив вещественных чисел и возвращает его сумму. Задача 2 Определите функцию, которая возвращает ссылку на седло- вой элемент матрицы или NULL, если таковой отсутствует. Седло- вым элементом называется элемент, который одновременно явля- ется наибольшим в своей строке и наименьшим в своем столбце (или наоборот, наименьшим в строке и наибольшим в столбце).
30 Часть 1 Задача 3 Определите функцию для сложения длинных целых положи- тельных десятичных чисел. Каждое число задается массивом из 100 символов, один символ — одна цифра. Задача 4 Определите функцию для умножения длинных целых поло- жительных десятичных чисел. Задача 5 Определите функцию, которая делит нацело длинные целые положительные десятичные числа. 5. Строки 5.1. Встроенный тип char Значениями типа char являются целые числа со знаком (signed char) или без знака (unsigned char), которые помещаются в один байт. От других целых типов его отличает наличие симво- лических констант вида: "А” — для изображаемых символов; ”\ооо” и ”\xhhh" — для всех символов без исключения, где ооо — 8-ичные, a hhh — 16-ичные цифры. Несколько символов имеют собственные имена: \п — новая строка; \t — горизонтальная табуляция; \и — вертикальная табуляция; \Ь — возврат назад; \г — возврат каретки; \а — звонок (attention); \\ — обратная косая черта; \’ — одинарная кавычка; \" — двойная кавычка. Замечание. Значения типа char, выводимые в выходной по- ток cout, выглядят как символы, а не как числа благодаря определению операции помещения в поток.
Основы языка 31 5.2. Строки символов Строка языка С представляет собой массив символов, который завершается символом с кодом 0. Например, строка "QWERTY" имеет тип char[7]> пустая строка "" — тип char[ 1 ]. Замечание. В стандартной библиотеке C++, кроме С-строк, CiiZhiiJ определены строки типа string. Это средство более высокого уровня, и речь о нем пойдет позже. Строковая константа — это последовательность символов, заключенная в двойные кавычки. В числе символов строки могут находиться любые символьные константы, например, "Звонок в конце сообщения\007\п". Соседние строковые константы транслятором «склеиваются». Например, "АБВ""ГДЕ" означает то же, что "АБВГДЕ". При этом неважно, сколько разделителей (пробелов, знаков табуляции, пере- водов строк) стоит между константами. Строковые константы можно использовать для инициализа- ции символьных массивов. Пример. Определить массив из 7 символов и инициализиро- вать его. и_________________________________________________________ char s[7]= ”ABCDEF"; // вариант 1 char s[] - "ABCDEF"; // вариант 2, то же, что 1 char *s = "ABCDEF"; // вариант 3 Строки, как и массивы, нельзя копировать простым присваи- ванием. Это можно делать только посимвольно или при помощи библиотечных функций. Программист должен сам позаботиться о памяти для размещения копии. Пример. Скопировать строку si в s2, и________________________;________________________ char sl[ ] - "1234567890", s2[ll]; for (int i ~ 0; sl[i]; i++) s2[i] si Li]; s2[i] = 0; // ставим замыкающий 0
32 Часть 1 5.3. Строковые библиотечные функции Функции для работы со строками объявлены в заголовочном файле string.h. Приведем некоторые из них: char *strcpy(char *dest, const char *src); копирует второй аргумент в первый. Возвращает указатель на ко- пию. Память для dest должна быть заранее зарезервирована. char *strdup(const char *s); копирует строку во вновь создаваемую функцией malloc( ) область памяти. Возвращает указатель на созданную копию или 0 при не- удаче. Программист ответственен за освобождение памяти функ- цией free( ); size__t strlen(const char *s); подсчитывает размер строки. Возвращает количество символов строки без нулевого символа. Тип size_t определен в файле string.h и других заголовочных файлах как целое без знака: typedef unsigned sizejt\ char *strcat(char *dest, const char *src); присоединяет вторую строку к первой. Возвращает указатель на начало нарощенной строки. char *strchr(const char *s, int c); сканирует строку s в поисках первого вхождения заданного симво- ла с. Нулевой символ можно искать наряду с другими. Возвращает указатель на найденный символ или 0, если символа нет. char *strrchr(const char *s, int c); то же, что strchry но находит последнее вхождение символа с в строку 8. char *strstr(const char *sl, const char *s2); находит первое вхождение подстроки s2 в строку si. Возвращает указатель на место первого вхождения или 0, если такового нет. int strcmp(const char *sl, const char*s2); сравнивает две строки. Возвращает целое меньше нуля, если si < s2, равное нулю, если si === s2, и большее нуля, если si > s2.
Основы языка 33 char *strpbrk(const char *sl, const char *s2); сканирует первую строку в поисках первого вхождения любого сим- вола из второй строки. Возвращает указатель на найденный сим- вол или 0 при неудаче. char *strtok(char *sl, const char *s2); сканирует первую строку в поисках первого участка, не содержа- щего символов из s2. Первый вызов функции возвращает указатель на начало первого участка и записывает 0 в si сразу после конца участка. Последующие вызовы с 0 в качестве 1-го аргумента обра- батывают строку дальше, пока еще есть такие участки. Если их нет, возвращается 0. Функцию применяют для выделения слов из пред- ложения si. В строке s2 находятся символы-разделители. Пример. Ввод, клонирование и вывод строки. и____________________________________________________ #include <scring.h> //. . . char si 180]; cin >> si; char *s2 - st.rdup (si); cout « si << s2 « endl; free (s2); Замечание. Функция strdup( ) резервирует память для копии uJb при помощи вызова функции malloc(size_t), поэтому про- граммист должен освободить эту память вызовом функции free(void*). Функции mallocf ) и free() составляют пару по- добно операциям new и delete. Пример. Заменить в строке $1 первое вхождение слова а словом Ь. и int main(int argc, char* argv[])_________________________ { char *sl - "I see nothing."; char *a -• "see", *b ~ "hear"; char s2[100]; .// для начала скопировать всю строку strcpy(s2 , si);
34 Часть 1 // установить pl на слово а в оригинале char *pl = strstr(si, а); // установить р2 на слово а в копии char *р2 = s2 - si + pl; // копировать слово Ь strcpy(p2, Ь); // сместить указатель в оригинале pl + = strlen(a); // сместить указатель в копии р2 += strlen(b); / / скопировать остаток строки strcpy(p2, pl); cout « s2 « endl; return 0; } 5.4. Задачи Задача 1 Реализуйте функцию mystrstr( ), которая делает то же, что биб- лиотечная функция strstr(). Сравните производительность своей и библиотечной функций. Используйте для этого функцию clock( ), объявленную в заголовочном файле time.h. Задача 2 Сделайте то же для функции strcmpf ). Задача 3 Сделайте то же для функции strtok( ). Задача 4 Задано английское предложение в виде одной строки. Распе- чатайте в столбик все слова этого предложения. Задача 5 Задан текст в виде одной длинной строки. Распечатайте этот текст на полосе шириной в N символов, выполнив выравнивание как в газетном столбце.
Основы языка 35 6. Структуры и объединения 6.1. Структуры Структура — это составной тип данных, который состоит из элементов разных типов. Объявление структуры следует рассмат- ривать как объявление типа. Замечание. В C++ структуры заключают в себе не только <1н1Э данные, но и код и относятся к средствам объектно-ориен- тированного программирования. В данном разделе объект- ные свойства структур не рассматриваются. J. Пример. Объявление структуры, которая хранит сведения о журнале: название, год, номер. V___________________________________________________ struct magazin { char* tittle; int year; int number; }; Ниже определена и инициализирована переменная структура mag. и________________________________________________________ j ma да z in mag {"Nature”, 3, 1995}; Доступ к элементам структуры осуществляется по составному имени: имя_структуры.имяэ лемента. Здесь точка обозначает оператор выбора, а составное имя дает еще один пример адресного выражения. Если есть указатель на структуру, то доступ к элементу возмо- жен при помощи операции косвенного выбора: указатель_паструктуру->имя_элемента. Стрелка, составленная из двух символов, обозначает оператор косвенного выбора. Структуры можно присваивать, передавать в виде параметров функции, возвращать в виде результата функции. Структуры
36 Часть 1 нельзя сравнивать операциями ===, /«, <, > и т.п. Структуры, как и массивы, можно инициализировать списком значений. Пример. Работа со структурой magaztn. // объявления и инициализация magazin *pm, m = {"Nature", 2000, 4}; // доступ к элементам cout « m. tittle « " " << m.number « endl; // одна структура в свободной памяти pm = new magazin; *pm = m; pm->tittle - "Nature”; delete pm; I/ массив структур в свободной памяти pm = new magazin[10]; pm[0].tittle = "Природа”; delete[] pm; 6.2. Списки Массив — не единственный тип, способный играть роль контей- нера для данных. Другим таким типом является связанный список. В отличие от массива, он реализуется не языком, а программистом. Пример. Определить связанный список и операции над ним. U___________________________________________________ // Структура - элемент списка struct Item { int info; Item* next; }; void main() { Item *first = 0; //Указатель на начало списка Item *р;
Основы языка 37 int i ; // Создать список for(;;) { // Вводить числа, пока не введем О cin » i; if (!i) break; // Создать новый элемент списка р - new Item; p->info = 1; // Присоединить новый элемент к началу списка p->next - first; first - р; } // Прейти список и вывести элементы р ~ first; while (р) { cout << p->info << " ”; р = p~>next; } // Пройти список и удалить элементы while (first) { р = first; first - first->next; delete p; } } Другие виды списков: • список с указателями на первый и последний элементы — по- зволяет добавлять и удалять элементы с обеих сторон списка; • двунаправленный список — позволяет перемещаться по спис- ку в обоих направлениях; • кольцевой список — позволяет достичь любой элемент списка, начав движение с любого места в списке.
38 Часть 1 6.3. Объявление структур Объявить структуру можно многими способами: 1) typedef struct {int х;}Т, сравни с typedef int Т; 2) typedef struct tagTfint x;}T; то же, но устаревшее из С; 3) struct Tfint х;}', так делают в C++; 4) struct Tfint x;}t; объявлен тип и переменная этого типа; 5) struct fint х;} t; сравни с int t;. Последний пример объявляет переменную структуру, не да- вая имени ее типу. При объявлении типов и переменных следует помнить, что: 1) Имя становится доступным сразу после его первого появления. Это позволяет писать так U____________________________________________________ struct S { S* а; 2) Переменная не может быть объявлена, пока не завершено объявление ее типа и_________________________________________________ struct S { Sa; // ОШИБКА Размер структуры, как и размер любого типа или переменной, можно получить операцией sizeof выражение или sizeof (тип). Лк Замечание. Размер структуры может быть больше суммар- СшЬ ного размера ее элементов из-за выравнивания — размеще- ния элементов на границах слов. Выравнивание делается для повышения скорости обработки данных в программе.
Основы языка 39 6.4. Битовые поля Для компактного размещения малоразрядных переменных используют битовые поля. Член структуры считается битовым по- лем, если после его имени указано число битов, которые он зани- мает. Допустимы неименованные поля, они нужны для выравни- вания при размещении других полей. Хотя использование битовых полей экономит память, оно уве- личивает объем кода и снижает эффективность программ. Битовые поля — это удобный способ внесения и извлечения информации из части памяти, занимаемой переменной. Пример. Упаковать информацию о дате (день, месяц, год) в одно слово. Диапазон изменения года — от 1900 до 2000. U_____________________________________________________ struct date { unsigned int d:5, m:4, у:7; }; date D = {19,10,1951 -1900 } ; 6.5. Объединения Объединение во всем похоже на структуру, но его элементы занимают одно и то же место в памяти. Пример. Массив, в котором можно хранить и числа, и строки. U__________________________________________________ union U{ char* name; long value; }; U arr[20]; arr[0].name - ”123456”; arr[1].value - 123456; arr[2] = arrfl]; Объединение может быть элементом структуры.
40 Часть 1 Пример. Для журнала храним название, год и номер, а для книги — название, год выпуска и издательство. V___________________________________________________________ struct magazin_book { char* tittle; int year; union{ int number; // для журнала char* publ; // для книги }; }; 6.6. Задачи Задача 1 Объявите структуру, которая является узлом простого спис- ка, т.е. в ней есть поле данных и поле указателя на следующий эле- мент списка. Запрограммируйте функции добавления элемента к началу списка, удаление элемента из начала списка, добавление всех элементов из другого списка. Задача 2 Запрограммируйте функцию прохождения списка из задачи 1. Эта функция должна получать в качестве аргумента указатель на другую функцию, которая будет вызываться для каждого элемен- та того списка, который проходят. Задача 3 Реализуйте функцию добавления списка к списку и функцию прохождения списка в рекурсивной форме. Задача 4 Определите несколько функций для работы с кольцевым списком: а) подсчет количества элементов; б) вставка нового элемента после заданного; в) удаление элемента, расположенного после заданного; г) вставка нового элемента перед заданным; д) соединение двух кольцевых списков в один.
Основы языка 41 Задача 5 Запрограммировать сортировку простого списка. 7. Дополнительные сведения о функциях 7.1. Параметры по умолчанию Бывают функции с параметрами, заданными по умолчанию. Например, функция вывода числа в заданной системе счисления принимает само число п и основание q той системы счисления, в ко- торой число должно быть выведено. U___________________________________________________ | void print(int n,int q = 10); При обращении к функции можно задать оба параметра p]rint(1995,2);, а можно только один print (1995);. В этом случае число будет выведено в десятичной системе счисления. 7.2. Произвольное число параметров Для некоторых функций нельзя заранее предвидеть число и тип параметров вызова. Такие функции объявляют, завершая список параметров многоточием и____________________________________________________ [~ int print f (const char*/...); ~ ~ ~ ~] Тем самым говорится, что в вызове print f должен быть, по мень- шей мере, один параметр char*, а остальные — не обязательны. Работа с нефиксированными аргументами основана на том, что аргументы, передаваемые функции, располагаются в памяти по порядку, один за другим. Для упрощения доступа к нефикси- рованным аргументам применяют макросы va~arg, va_end, и va_start, определенные в заголовочном файле stdarg.h. U____________________________________________________ typedef char* va_list; # define _INTSIZEOF(n) \ ((sizeof(n)+sizeof(int)-!)&-(sizeof(int)-1))
42 Часть 1 #define va_jstart (ар, v) \ (ap = (va_list)&v + _INTSIZEOF(v)) # de f ine va_arg(ap,t) \ (*(t*)((ap „INTSIZEOF(t)) - „INTSIZEOF (t) ) ) # define va„end(ap) (ap ~ (va„list)0) Внешне вызов макроса похож на вызов функции, подробнее о макросах будет сказано позже. Макрос va starl(ap, и) направляет указатель ар на первый нефиксированный аргумент функции (тот, что следует за послед- ним фиксированным аргументом и). Заметим, что указатель ар дол- жен быть определен как локальная переменная функции vajlist ар;. Макрос va_arg(ар, t) возвращает значение того нефиксирован- ного аргумента типа t, на который указывает ар, и передвигает ука- затель ар на следующий по порядку нефиксированный аргумент. Макрос va_end(ар) просто обнуляет указатель ар. Пример. Определить функцию, которая складывает значе- ния своих аргументов, начиная со 2-го. Первый аргумент содержит количество слагаемых. Остальные аргументы должны быть целы- ми или автоматически приводиться к ним. и_______________________________________________________ #include <stdarg.h> int sum(int number, ...) { va„list ap; // указатель на аргумент int total = 0; // накапливаемая сумма // установить указатель на 1-й аргумент va_start(ар, number); for (int i - 1; i <~ number; i++) // суммировать и сдвинуть указатель // на размер целого числа total +~ va arg(ap, int);
Основы языка 43 // обнулить указатель va„end(ар); return total; } Пример. Определить функцию void соп(char* buf, int limit,...) присоединения к строке, находящейся в buf, других строк. Параметр limit содержит общий размер буфера. U_______________________________________________________ #include <stdarg.h> .void con(char* buf, int limit, ...) { char *p; va_li s t ap; va__start (ap, limit); while (p = va_arg(ap, char*)) if (strlen(buf) + strlen(p) < limit) strcat(buf, p); va_end(ap); } 7.3. Неиспользуемые параметры Иногда в результате модификации кода функции надобность в некоторых параметрах отпадает. Чтобы не лишать работоспособ- ности те программы, которые вызывают функцию, ее определяют с неиспользуемыми параметрами. Например, и_______________________________________________________ void func(int pl, int) { // тело функции } Второй параметр не используется.
44 Часть 1 7.4. Перегруженные функции Использование одного имени для обозначения различных фун- кций называется перегрузкой. и________________________________________________________ void print(int); // печать целого void print(char*); // печать строки Когда вызывается перегруженная функция, компилятор вы- бирает одну из одноименных функций, сообразуясь с количеством и типом параметров. В С 4-4- различают пять типов соответствий: 1) Параметры функции точно согласуются с параметрами вызова или требуют лишь неизбежных преобразований (имени масси- ва в указатель, имени функции в указатель на функцию, Т в const Т). 2) Для согласования достаточно расширения целых типов (char в int, short в int), а также float в double. 3) Для согласования достаточно стандартного приведения типов (int в double, unsigned int в int, derived* в base* и т.п.). 4) Для согласования достаточно приведения типов, определяемо- го пользователем в конструкторах и конверторах. 5) Согласование использует многоточие (...) в определении функции. Рассмотрим сначала функции с одним аргументом. Всегда нужно выбирать лучшее соответствие, а именно то, которое распо- ложено выше в приведенном списке. Если есть два лучших соот- ветствия, вызов расценивается как неоднозначный, и компилятор выдает ошибку. Если при вызове передается несколько параметров, то выби- рается функция, у которой хотя бы для одного из них соответствие лучше, а для остальных не хуже. Если согласовать параметры нельзя, вызов функции расцени- вается как ошибочный. 7.5. Указатель на функцию С функцией можно делать только две вещи: вызывать функ- цию и определять ее адрес. Указатель, хранящий адрес функции, можно использовать для вызова этой функции.
Основы языка 45 Пример. Объявление указателей на функции. U________________________________________ int (*pfl)(int, int); void (*pf2)(char*); Если в программе определена какая-нибудь функция, например, и____________________________________________________ void MyFun(char* р) { // тело функции } то ее адрес можно поместить в указатель и________________________________________________________ | pf2 = &MyFun; j а затем использовать его для вызова функции error U I (*pf2) ("Hello!"); | Поскольку операция вызова функции ( ) имеет приоритет выше, чем операция разыменования, необходимы скобки (*pf2). JSk Замечание. Вызывая функцию через указатель, операцию ра- зыменования можно опускать, компилятор поймет вас пра- вильно, т.е. pf2 ("Hello!"); означает то же, что (*pf2) ("Hello!");. Пример. Определить функцию, которая с каждым элемен- том вещественного массива выполняет действие, заданное функ- цией-параметром, например, берет синус. #include <math.h> // здесь прототип sin() void for_each(doublе m[] , int n, double fun(double)) { for (int i = 0 ; i<n; i + + ) m[i] - fun(m[i]); }
46 Часть 1 double М[] - {0.01, 0.02, 0.03 , 0.04, 0.05}; for„each(M, 5, sin); 7.6. Спецификатор inline Спецификатор inline говорит компилятору, что он должен попытаться встроить в программу код функции всюду, где нахо- дятся вызовы функции. Это увеличит быстродействие программы. Пример. U____________________________________________ [ inline void error'(char* p) { /★тело функции*/} | Замечание. Если тело функции расположено в одной строке СмЬ с заголовком, спецификатор inline можно не писать. 7.7. Макросы Макрос без параметров определяется так: #define имя расширение макроса. Когда в тексте программы встречается лексема имя она заме- няется словами расширение макроса. Эта замена выполняется до трансляции программы. За то, что из этого получится, перед ком- пилятором отвечает только программист. Макрос может занимать несколько строк, в этом случае зна- ком переноса служит обратная косая черта. Можно определить макрос с параметрами. Пример макроса с параметрами. V #define WRITELN(a) cout < < a < n — 11 .< a < < endl; WRITELN("Ку"); При расширении макроса значение параметра займет свое ме- сто в тексте расширения. После компиляции и выполнения будет выведено «Ку-Ку».
Основы языка 47 Пример макросов выделения старшего и младшего байта слова. U_______________________________________________________ #define LOBYTE(w) (char(w)) #define HIBYTE(w) (char ( (unsigned) (w) »8) ) Чтобы сделать расширение макроса безопасным, применяют скобки. Пример. Описать макрос для операции вычитания. Вариант 1 — неправильный. U___________________________________________________ #define SUB(а, b) а - Ь int i = SUB(5, 2 - 3);// расширяется неверно Вариант 2 — неправильный. U______________________________________ #define SUB(а, Ъ) (а) - (Ь) int i - SUB(3, 2) * 2;// расширяется неверно Вариант 3 — правильный! V__________________________ ptdefine SUB(а, b) ((а) - (Ь) ) В определяющях макросов можно использовать три препро- цессорных оператора: # — приведение к строке; ## — склейка лексем; @# — приведение к символу. Примеры применения оператора # и__________________________________________________ f #define LEN(х) strlen(#x) j Макрос LEN(12345) расширяется в strlen("12345" ). u__________________________________________________ |~ tdefine WRITELN(a) cout « #au~n#a << endl; | Макрос WRITELN(Ky) расширяется в cout « « endl;.
48 Часть 1 Оператор склейки « ## » объединяет в одну две лексемы, меж- ду которыми он находится. Пример. Определение макроса, создающего имена переменных. и___________________________________________________ | fldefine DEF VAR(n) int var ## n [ Макрос DEFVAR( 1) расширяется в int varl. Макросы могут быть вложенными. После каждого расшире- ния макрос сканируется заново на предмет поиска еще не расши- ренных макросов. Макрос можно отменить при помощи директивы # unde f. После этого ссылка на имя макроса будет вызывать ошибку компиляции. Макросы позволяют создать эффективный код, но затрудня- ют отладку. Там, где это возможно, вместо сложных макросов сле- дует пользоваться встроенными функциями inline, а вместо про- стых — символическими константами const и нумераторами епит. 7.8, Задачи Задача 1 Определите функцию, которая суммирует значения своих ар- гументов. Аргументы-слагаемые могут иметь тип long, int, short и char. Задача 2 Определите функцию, которая получает в виде строки ариф- метическое выражение. Выражение содержит одноразрядные целые числа и знаки арифметических операций: сложение, вы- читание и умножение и каждая операция окружена парой круг- лых скобок. Функция должна вернуть значение арифметическо- го выражения. Задача 3 Добавьте к функции из предыдущей задачи необязательный параметр — систему счисления, в которой записаны числа ариф- метического выражения. Задача 4 Напишите макрос, который находит наибольшее из трех чисел.
Основы языка 49 Задача 5 Напишите макрос, определяющий массив типа Туре, который называется Name, имеет Number элементов, и все элементы содер- жат значение Value. 8. Потоковый ввод и вывод 8.1. Разновидности ввода и вывода Средства ввода и вывода формально не входят в стандарт язы- ков С и C++, но фактически стандартизованы и содержатся в биб- лиотеках функций. В них можно выделить следующие группы: 1) Консольные — ориентированы на ввод с клавиатуры и вывод на дисплей. Описаны в заголовочном файле conio.h. 2) Файловые — предназначены для работы с файлами. Описаны в io.h. 3) Потоковые — аналогичны файловым, но предоставляют боль- ший сервис программисту. Описаны в stdto.h. 4) Объектные — объектно-ориентированный ввод/вывод, толь- ко в C++. Описаны в iostream.h, fstream.h, iomanip.h. В данном разделе мы ознакомимся с 3-й группой — потоко- вым вводом и выводом. 8.2. Открытие и закрытие потока Поток является программной надстройкой над файлом, пре- доставляющей программисту дополнительный сервис. Схема рабо- ты с потоком такая же, как и с файлом: открыть поток, выполнить чтение и/или запись, закрыть поток. Открывает поток вместе с ассоциированным файлом функция FILE* fopen(const char *filename, const char *mode). Функция получает filename — имя файла, ассоциированного с потоком, и возвращает указатель на поток, который идентифи- цирует его в последующих операциях. В строке режимов mode могут находиться следующие символы: г — открыть только для чтения;
50 Часть 1 w — создать для записи. Существующий файл будет перекрыт новым файлом; а — открыть для дозаписи или создать для записи, если файла нет; 4- — операции будут выполняться с уже существующим файлом; t — текстовый режим (обработка символов CRLF); b — двоичный режим (никакой обработки). При отсутствии в строке символов b или t, режим определяет- ся глобальной переменной _fmode, определенной в заголовочном файле fcntlJi. FILE — это управляющая структура для потока, объявленная в stdio.h. Она не предназначена для прямого использования. Помимо совместного открытия потока и файла (функция fopen(можно открыть поток и ассоциировать его с уже откры- тым файлом (функция fdopenf 7), открыть файл и ассоциировать его с уже открытым потоком (функция freopen()). Закрывает поток и выгружает буферы функция int fclose (FILE *stream) . Функция возвращает 0 при успехе и константу EOF при ошибке. Функция int fcloseall() закрывает все открытые потоки, кро- ме стандартных: stdin, stdout, stdprn, stderr м stdaux. 8.3. Ввод и вывод символов Чтение символа из потока выполняется (функцией int fgetc(FILE *stream). Запись символа в поток выполняется функцией int fputc(int с, FILE *stream). Обе функции возвращают код символа, а при ошибке — кон- станту EOF. Чтение символа из стандартного потока stdin выполняется функцией int fgetchar(void). Запись символа в стандартный поток stdout выполняется функцией int fputchar(int с).
Основы языка 51 Пример. Скопировать файл ххх.Ып в файл yyy,bin. U___________________________________________ FILE *in = fopen (”с:\\ххх.bin", "rt"); FILE *out - fopen ("c:Wyyy.bin", "wt"); if (!in) return; while (’feof(in) ) fputc(fgetc(in), out); fcloseall(); 8.4. Ввод и вывод строк Чтение строки из потока выполняется функцией char *fgets(char *s, int n, FILE *stream). Первый параметр указывает на буфер, принимающий строку, второй — задает размер буфера. Функция возвращает указатель на буфер или 0 — при ошибке. Чтение прекращается, когда введены символы CR LF или прочитано п - 1 символов из файла. Строка в буфере замыкается символами '\п' и '\0\ Указатель файла переме- щается за символы CR LF. Запись строки в поток выполняется функцией char *fputs(char *s, FILE *stream). Функция возвращает указатель на последний записанный сим- вол или EOF при ошибке. Терминальный символ строки ’\0' не копируется. Пример. Скопировать текстовый файл xxx.txt в файл yyy.txt по строкам. U_____________________________________________________ const int n - 100; char buf [n]; FILE *in - fopen ("c : \\xxx. txtr" ) ; FILE *out ~ fopen ("c : Wyyy . txt"w" ) ; if (’in) return; while (’feof(in))
52 Часть 1 fgets(buf, n, in); fputs(but, out); ) fcloseall(); 8.5. Ввод и вывод записей Чтение записей из потока выполняется функцией sizejt fread (void *ptr, sizejt size, size_t n, FILE *stream), где ptr — указатель на буфер в памяти, принимающий записи; size — размер записи в байтах; л — количество читаемых записей. Функция возвращает количество прочитанных записей, кото- рое может быть и нулевым. Замечание. Тип size_t определен в файле stdio.h следующим образом: typedef unsigned int sizejt;. Вывод записей в поток выполняется функцией size_t fwrite(const void *ptr, size_t size, size_t n, FILE *stream), где ptr — указатель на буфер в памяти, содержащий записи; size — размер записи в байтах; п — количество выводимых записей. Функция возвращает количество выведенных записей, кото- рое может быть и нулевым. 8.6. Управление указателем файла Чтение и запись выполняются в том месте файла, где находит- ся указатель файла. Установить указатель можно функцией int fseek(FILE *stream, long offset, int whence), где offset — смещение указателя; whence — способ отсчета смещения. Функция возвращает ноль при успехе, и не ноль — при неудаче.
Основы языка 53 Для указания точки отсчета смещения используют константы: SEEK_SET — отсчет от начала файла; SEEK_CUR — отсчет от текущей позиции указателя; SEEK_END — отсчет от конца файла. Функция long ftell(FILE *stream). возвращает текущую позицию указателя. При ошибке возвра- щает -1 и устанавливает глобальную переменную еггпо в нену- левое значение. Замечание. Те же действия выполняются функциями fcJJb fsetpos() и fgetposf ). Пример. Определить длину файла хххМп. U_________________________________________________ FILE *in = fopen("с:\\xxx.bin”,"rb"); if (’in) return; // ставим указатель в конец файла fseek(in, 0, SEEK_END); // определяем позицию указателя cout « ftell(in); 8.7. Состояние потока Макрос, проверяющий достижение конца файла потока, int feof (FILE *stream) возвращает не 0, если достигнут конец файла, и 0 — в противном случае. Макрос, тестирующий индикатор ошибки потока, int ferror(FILE *stream) возвращает не 0, если обнаружена ошибка записи или чтения. Однажды установленный индикатор ошибки сохраняется до выполнения функций clearerr, rewind или закрытия потока. Индикатор «конец файла» переустанавливается каждой опера- цией чтения. Макрос, обнуляющий индикаторы ошибки и конца файла.
54 Часть 1 void clearerr(FILE *stream). Макрос void rewind(FILE *stream) делает то же, что clearerr, а также устанавливает указатель в нача- ло файла. При возникновении ошибки глобальная переменная еггпо (определена в файлах errno.h, stddef.h, stdlib.h) получает ненуле- вой номер ошибки. 8.8. Форматированный вывод Функции, рассмотренные ранее, выводят информацию в по- ток без или почти без преобразования. Функция int fprintf(FILE ♦stream, const char *format [,argument) преобразует выводимые данные [, argument, ...] в последователь- ность символов, руководствуясь в этом строкой формата format. При успехе функция возвращает количество выведенных в поток байт, при неудаче — константу EOF. Квадратные скобки говорят о необязательности аргумента. Строка формата содержит простые символы и спецификации формата. Простые символы копируются в выходной поток без из- менения, спецификации формата применяются для форматирова- ния остальных аргументов функции. Если аргументов меньше, чем спецификаций, последствия непредсказуемы. Если аргументов больше, чем спецификаций, лишние аргументы игнорируются. Общий вид спецификации формата следующий: % [флаги][ширина][.точность] тип, где флаги — признаки выравнивания, использования знаков, деся- тичной точки, конечных нулей, 8-ичных и 16-ичных префиксов; ширина — минимальное число печатаемых символов с учетом пробелов и нулей; точность — максимальное число печатаемых символов (для целых — минимальное число цифр); тип — символ спецификации типа — обязательный элемент формата.
Основы языка 55 Пример. Вывести в текстовый файл c:\xxx.txt таблицу ум- ножения на 5. и________________________________________________________ FILE *out fopen("с:\\ххх.txt”,”wt"); for (int i - 2; i < 10; i + +) fprintf(out, "%ld x 5 - %2d\n”, i, i * 5); fclose(out); 8.9. Форматированный ввод Для форматированного ввода из потока применяют функцию int fscanf(FILE ♦stream, const char *format [,address,...] ). Функция возвращает число полей ввода, отформатированных и размещенных в памяти. При неудаче функция возвращает EOF. Функция fscanf ( ) рассматривает входной поток как последо- вательность полей ввода. Поле ввода заканчивается: • первым символом пробела (но не включает его); • первым символом, который не может быть преобразован по спецификации формата, сопоставленной этому полю; • (п + 1)-м символом, если спецификация включает ширину поля в п символов. Функция просматривает последовательность полей ввода, форма- тирует их и размещает по адресам — аргументам fscanf. Число адре- сов, спецификаций формата и полей ввода должно быть согласовано. Строка формата состоит из неотображаемых символов (' ’, V, \п), отображаемых символов (все прочие, кроме «%») и специфи- каторов формата. Если функция fscanf встречает неотображаемый символ в форматной строке, она будет считывать, но не сохранять все неотображаемые символы входного потока вплоть до первого отображаемого символа. Если fscanf встречает отображаемый сим- вол в форматной строке, она прочитает, но не сохранит соответству- ющий символ входного потока. Спецификация формата предписывает fscanf чтение, преобра- зование и размещение в памяти одного входного поля. Общий вид спецификации формата: % [ширина] тип,
56 Часть 1 где ширина — максимальное число считываемых символов; тип — символ спецификации типа, обязательный элемент формата. 8.10. Другие функции форматного ввода и вывода Ниже перечислены другие функции форматного вывода с ука- занием заголовочного файла и выходного потока: cprintf() CONIO.H Консоль fprintf() STDIO.H Поток printf() STDIO.H stdout sprintfO STDIO.H Строка То же — для форматного ввода: cscanfQ CONIO.H Консоль fscanf() STDIO.H Поток scanf() STDIO.H stdin sscanff) STDIO.H Строка 8.11. Примеры .Пример. Вводить строки с клавиатуры и сохранять их в текс- товом файле. U_____________________________________________________ char buf[100]; FILE *F; F -- fopen("111.txt”, "w"); for (;;) { gets(buf); if (strlen(buf) > 40) break; fputs(buf, F) ; fputs("\n”, F) ; ) fclose(F);
Основы языка 57 Функция getsf ) вводит строки из стандартного входного по- тока stdin. При вводе с клавиатуры символ '\п не попадает в строку. Если вводить при помощи функции fgetsfbuf, 100, F), то специального вывода fputs('\ri ,F ) не понадобится. Пример. Имеется текстовый файл. Напечать его самую длин- ную строку. char buf[100], maxbuf[100]; maxbuf[0] ~ 0; FILE *F = fopen("111.txt", "r"); while (’feof(F)) { fgets(buf, 100, F); if (strlen(buf) > strlen(maxbuf)) st rcpy(maxbu f, buf); } fclose(F); fputs(maxbuf, stdout); Замечание. Вывод строки в поток stdout можно выполнить СХзЬ функцией puts( maxbuf );, Пример. Вводить целые числа с клавиатуры и сохранять их в двоичном файле. и________________________________________________________ int buf; FILE *F = fopen("111.dat", ”w") ; for (;;) { scanf("%d", &buf); if ( ’buf) break; fwrite(&buf, sizeof(int), 1, F); } fclose(F); Для ввода целых чисел из входного потока необходимо исполь- зовать только форматный ввод (scanf или fscanf,). Полученный файл не является текстовым и может быть пра- вильно прочитан только функцией fread.
58 Часть 1 8.12. Файловый ввод-вывод Поскольку файловый ввод-вывод в основном перекрывается потоковым, покажем лишь пример перенаправления стандартно- го ввода и вывода в файлы. V___________________________________________________ #include<io.h> // „close, „open #include<stdiо.h> // scanf, printf #include<fcntl.h> // „O_RDONLY, _O_WRONLY void main() { „close (0) ; „open ("in.txt”, „O„RDONLY); „close (1); „open ("OUt.txt", „O„WRONLY | _O„CRE’AT) ; int x, y; scanf("%d %d" , &x, &y) ; printf(“%d\n", x + y); } Стандартные файлы имеют дескрипторы: 0 — stdin; 1 — stdout; 2 — stderr. Если закрыть файл с таким дескриптором, то следующий от- крытый файл получит этот освободившийся дескриптор. 8.13. Задачи Задача 1 Имеется два текстовых файла, строки которых упорядочены по алфавиту. Слейте их в третий файл, тоже упорядоченный. Задача 2 Имеется текстовый файл, строки которого не упорядочены. Упорядочите их при помощи сортировки слиянием.
Основы языка 59 Задача 3 Дан большой текстовый файл с длиной строк не более 100 сим- волов. Распечатайте 15 последних строк файла. Задача 4 В текстовом файле находится словарь, по одному слову в строке. Запрограммируйте поиск заданного слова, приспособив для этой цели алгоритм двоичного поиска. Задача 5 Постройте дерево файлов и подкаталогов для заданного ката- лога. Воспользуйтесь функциями findfirstf ) и findnextf ) из заго- ловочного файла io.h. Структуру дерева покажите при помощи от- ступов от левой границы экрана. 9. Уточнение понятий языка 9.1. Объявление, определение, инициализация Прежде чем имя будет использовано, оно должно быть объяв- лено, т.е. указан его тип. Это необходимо транслятору для генера- ции правильного кода. Определение соединяет имя с материальной сущностью. Для переменной — это область памяти, для функции — код програм- мы, для константы — конкретное значение. Большинство объяв- лений являются одновременно и определениями. Примеры объявлений и определений, и int i; // объявление и определение переменной int f(int i); // объявление функции extern char с; // объявление внешней переменной Объявлений имени может быть несколько (но одинаковых), а определение имени — только одно. Инициализация — это задание значения имени при его опре- делении. А Примеры объявления, определения и инициализации.
60 Часть 1 u_______________________ const float pi = 3.14; int i = 10; Замечание. Глобальные и статические локальные (см. ниже) Ъи1шп11> переменные неявно инициализируются нулем. 9.2. Область действия и время жизни Объявление вводит имя в область действия. Для локального имени (объявленного в блоке или в функции) область действия про- стирается от точки объявления до конца блока. Для глобального имени область действия распространяется от точки объявления до конца того файла, в котором имя объявлено. Локальное имя может скрывать глобальное или внешнее ло- кальное имя. К скрытому глобальному имени пате можно полу- чить доступ с помощью операции разрешения области действия: ::паше. К скрытому локальному имени получить доступ нельзя. Параметры функции считаются объявленными в самом внеш- нем блоке функции. Пример. Сокрытие имени в блоке. U___________________________________________________ int i; // глобальная переменная void main() { int i - 1; // внешняя локальная { int i - 2; // внутренняя локальная cout << i « ::i; } } Время жизни Как правило, объект создается, когда встречается его опреде- ление, и уничтожается, когда управление выходит из его области действия. Глобальные объекты создаются один раз и «живут»
Основы языка 61 до окончания программы. Так же ведут себя локальные объекты с описателем static. В этом разделе термин «объект* понимается в широком смыс- ле: переменная, константа, тип, функция, макрос и т.п. Временные объекты Временные объекты создаются при вычислении выражений и существуют, пока вычисляется выражение. C++ запрещает из- менять временные объекты встроенного типа. V + + 5; // ОШИБКА! int х = 6; -(х + 1); // ОШИБКА! 9.3. Типы Каждый объект в C++ имеет тип, который определяет возмож- ные операции над ним. Имена типов появляются: • в объявлениях, например, int i; • в операциях new и sizeof, например, new char, sizeof(int); • в явных преобразованиях типов, например, long (b), (long) b. Неявные преобразования типов могут искажать или терять информацию, поэтому их следует избегать. Явные преобразования типов выполняются в двух формах: • традиционной (double) а; • функциональной double (а). Функциональная форма преобразования не может быть при- менена, когда тип составной. 9.4. Производные типы Производные типы конструируются из базовых при помощи следующих операций (в порядке убывания приоритета): а) массивы [ ]; б) функции ( ); в) ссылки
62 Часть 1 г) указатели *• д) константы const; е) структуры struct; ж) объединения union; з) классы class. В инструкции объявления можно выделить 2 части: заголо- вок и декларатор. Например, в объявлении int i, *т[ 10]; int — заголовок; *р, i и *т[ 10] — три декларатора. Инструкция объявления переменной производного типа тако- ва, что если в программе встретится выражение в форме деклара- тора, оно должно иметь тип, объявленный в заголовке. Примеры объявлений переменных производных типов. U int *x // указатель на целее, int x[10] // массив целых, int *x[10] // массив указателей на целое, int (*x)[10] // указатель на массив из целых, int *(*x)[10] // указатель на массив указателей на целое. Если объявление сложное, его можно построить поэтапно при помощи инструкций объявления типа typedef. и________________________________________________ // tpi - указатель на целое typedef int *tpi; Il tapi - массив указателей на целое typedef tpi tapi [10]; // tpapi - указатель на массив указ, на целое typedef tapi *tpapi; Теперь объявим переменную: tpapi х;. Чтобы проверить, что получилось то же самое, подставим ниж- ние определения в верхние: typedef tpi (*tpapi) [10]; typedef int *((*tpapi) [10]);. Внешние скобки можно опустить по соглашению о старшин- стве операций: [],(),<£,*.
Основы языка 63 typedef int *(*tpapi) [10];. Уберем слово typedef и получим объявление переменной. int * (*х) [10] ;. Пример. Описать переменную типа: а) массив указателей на float; U________________________________________________ float *pf[2]; pf[0] - new float (0); // пример использования б) массив указателей на вещественную функцию такого же ар- гумента; U__________________________________________________ float (*f [21) (float); f[0] = sin; // пример использования в) массив указателей на целые константы. U___________________________________ const int *m[2]; int n - 10; m[0J = &n; 9.5. Числовые константы В языке С, а следовательно и в C++, есть 4 вида целых констант: а) десятичные (типа Int или long): 0,1234, 567893456; б) 16-ичные (начинаются с Ох): ОхЗА, 0x3а; в) 8-ичные (начинаются с 0): 03216, 01; г) символьные (типа char): 'a', D', '\п. Тип константы определяется транслятором по ее виду. Для явного указания типа служат суффиксы: U, и — unsigned; L, I — long; F, f— float. Так константа О определяется транслятором как int, а константа 0L — как long. Константы с плавающей точкой имеют тип double: 11-23, .01, З.еЮ.
64 Часть I 9.6. Именные константы Ключевое слово const, добавленное к объявлению переменной, делает переменную именной константой. Значение константы не может быть изменено, поэтому она должна быть инициализирова- на при объявлении, например, const int model - 90;. В отношение «указывания» вовлечены 2 объекта: указатель и то, на что он указывает. Один из них или оба объекта могут быть константами. U_______________________________________________________ const char *р = "abed”;// указатель на константу char const *р - "abed";// то же самое char *const р = "abed";// постоянный указатель const char *const р = "abed";// постоянный указатель на константу Замечание. «*const» — это оператор объявления указателя iLXJb наряду с « * ». Объект, являющийся константой при доступе к нему через один указатель, может быть переменной при доступе другими путями. U char v[] = «АВС»; // массив переменных const char* P = V; // указатель на константу V[l] = ”D"; // правильно p[l] = "D"; // ошибка! Попытка изменить константу Указателю на константу можно присвоить адрес переменной, наоборот поступать нельзя. Указатели на константы широко при- меняются в качестве аргументов функций, чтобы исключить воз- можность модификации аргументов в теле функции. 9.7. Перечисление Перечисление — это способ задания целых констант посред- ством объявления типа.
Основы языка 65 Пример перечисления. и______________________ enum { one, two, three J; Это аналогично следующему: | const one - б/ two ~ Г, three ~2; Перечисления могут быть именованными: и__________________________________________________ | enum Enum {one, two, three}; Имя перечисления является именем типа. Если объявлено: Enum х; то х может принимать только значения: one, two, three. Значения перечислителей можно задавать явно: U________________________________________________ enum Enum { one - 1000, two = 200, three -- 3 }; Такие значения не обязаны быть различными, возрастающи- ми или положительными. Размер значений перечисления определяется, исходя из коли- чества битов, необходимого для представления множества значе- ний перечисления, причем 0 всегда входит в это множество. Разные перечисления представляют собой разные типы, и это можно использовать при перегрузке функций. U enum El {one, two, chree}; enurn E2 {one2, two2, three2);
66 Часть 1 void f(El e) {} // две разных функции, void f(E2 e) {} // а не одна, определенная дважды 9.8. Порядок вычисления выражений Порядок вычисления подвыражений в выражении не опреде- лен. Например, если определены int i = 1, и[10]; стандарт языка не говорит, как будет проинтерпретирована инструкция v[i] = i++; как v[ 1 ] = 1 или v[2] - 1. Другой пример того же самого дает код int i ~ 0; cout << i << i + +; Вместо ожидаемого «00» можно увидеть «10». Это произой- дет при следующем порядке вычисления подвыражений 2 1 cout < < i < < i + +; Неопределенность отсутствует, когда можно руководство- ваться приоритетом и ассоциативностью. Так порядок вычисле- ния выражения «10-5-3» однозначно определен левоассоциа- тивностью вычитания, а порядок вычисления «10 - 5 * 3» — приоритетом операций. В операциях «,» (следование), «<£<£» и «||» левый операнд всег- да вычисляется раньше правого, а сами логические выражения вычисляются по короткой схеме. При вызове функции порядок вычисления значений аргумен- тов также не определен в языке.
Основы языка 67 10. Операции и операторы 10.1. Сводка операций Все операции в C++ разделены по 16 категориям приоритета. Унарные операции (№ 2), условная операция (№ 14) и опера- ции присвоения (№ 15) правоассоциативны, все остальные — лево- ассоциативны. Таблица 10,1. Сводка операций Категория приоритета Оператор Назначение 1. Высшая ( ) ( ] Вызов функции Индексация массива Косвенный выбор компоненты Разрешение области действия Прямой выбор компоненты 2. Унарные + + + & sizeof new delete Логическое отрицание Битовое дополнение Унарный плюс Унарный минус Пред- или постинкремент Пред- или постдекремент Взятие адреса Разыменование Размер операнда в байтах Выделение свободной памяти Высвобождение свободной памяти 3. Доступа к компоненте . * Прямой доступ к компоненте Косвенный доступ к компоненте 4. Мульти- пликативные / % Умножение Деление Остаток от деления нацело 5. Аддитивные + Бинарный плюс Бинарный минус 6, Сдвиги V Л V Л Сдвиг влево Сдвиг вправо 7. Сравнения > Меньше Меньше или равно Больше Больше или равно
68 Часть 1 Таблица 10,1, Продолжение 8. Эквива- лентности । = Равно Не равно 9. Битовое AND 10. Битовое XOR 11. Битовое OR 12. && Логическое AND 13. 1 1 Логическое OR 14. Условный а? х: у 15. Присвоение Н II II II II II II II II У, А 11 * <*> -г 1 СЪ < — Д Присвоение Присвоить произведение Присвоить частное Присвоить остаток Присвоить сумму Присвоить разность Присвоить побитное И Присвоить побитное НЕ Присвоить побитное ИЛИ Присвоить левый сдвиг Присвоить правый сдвиг 16. Запятая Последовательное выполнение Замечание. Запятая в языке C++ является не только раздели- CsjJb телем, но и оператором последовательного выполнения. Зна- чением этой операции является значение правого операнда. 10.2. Перечень инструкций Квадратные скобки означают необязательность заключенно- го в них элемента. инструкция объявления { [список^ инструкций] } [выражение]; if (выражение) инструкция
Основы языка 69 if (выражение) инструкция else инструкция while (выражение) инструкция do инструкция while (выражение) for (иницинструкция [выражение]; [выражение]) инструкция где иниц_инструкция это объявление или [выражение]; switch (выражение) инструкция case константное__выражение: инструкция default: инструкция break; continue; return [выражение]; goto идентификатор; идентификатор: инструкция. 10.3. Инструкция выражения Она имеет форму [выражение];. Обычно инструкция выражения — это присвоение или вызов функции. Некоторые условные инструкции можно заменить услов- ным выражением. Например, условная инструкция U______________________________________________________ if (а >“ Ъ) max - а; else max ~ Ь;
70 Часть 1 заменяется на и_______________________ | max - а >~ b ? а : Ь; После того, как выяснится значение условия, будет вычисле- но либо только второе (если условие истинно), либо только третье — (если условие ложно) выражение. 10.4. Инструкция switch Инструкция switch передает управление на одну из несколь- ких меток в зависимости от значения выражения. Она имеет вид swi tch (выражение) инструкция, где инструкция обычно составная. Тип выражения — арифмети- ческий или указатель. Любую инструкцию в пределах инструкции switch можно по- метить одной или несколькими метками case константное_выражение:. Константное выражение должно быть того же типа, что вы- ражение в инструкции switch с точностью до обычных преобразо- ваний. Ни одна пара констант в пределах одной инструкции switch не должна иметь одинаковое значение. Допускается использование не более одной метки вида default: При выполнении инструкции switch выражение вычисляется, и управление передается инструкции с соответствующей меткой case. Если ни одна из констант не совпадает со значением выраже- ния и нет метки default, то не выполняется ни одна из инструкций. Сами по себе метки case и default не меняют управления, ко- торое просто пропускает эти метки. Для досрочного выхода из ин- струкции switch используется инструкция break;. Пример инструкции switch. V
Основы языка 71 switch (val) "{ case 1: cout<<!; break; case 2: cout<<2; return; default: cout<<0; 10.5. Инструкции break и continue Инструкция break прерывает выполнение ближайшей инструк- ции цикла или инструкции switch, в которой находится. Управление передается инструкции, следующей за прерванной инструкцией. Инструкция continue вынуждает цикл переходить к следую- щей итерации, не закончив текущую. 10.6. Инструкция goto и метки Инструкция goto метка; передает управление инструкции, помеченной меткой. Помечен- ная инструкция имеет вид метка: инструкция. Меткой может служить любой идентификатор. Метка видна в те- кущей функции или блоке, за исключением всех субблоков, в кото- рых был объявлен такой же идентификатор. Единственное назначе- ние метки — принимать управление, переданное инструкцией goto. Замечание. Практически единственным разумным примене- СиЗ нием инструкции goto является досрочный выход из несколь- ких вложенных циклов, т.к. инструкция break завершает лишь тот цикл, в котором сама находится.
72 Часть 1 10.7. Директивы препроцессора Директивы препроцессора начинаются с символа « # » и выпол- няются во время 1-й фазы компиляции. Результат выполнения за- тем компилируется, но прежде может быть выведен в stdout или в файл, что регулируется опциями компилятора. Директива #inc!ude Эта директива предписывает препроцессору поместить на ее место содержимое другого файла. Синтаксис директивы #include <имя файла> или #include «имяфайла». Если имя файла не является полным именем, в первом случае поиск происходит только в пределах специфицированных катало- гов включаемых файлов. Во втором случае сначала просматрива- ется текущий каталог. Директива #define С помощью директивы #define можно связать имя с некото- рой лексемой или последовательностью лексем т.е. описать мак- рос (см. раздел 7.7. Макросы). С помощью директивы #undef эту связь можно разорвать. Условная компиляция Можно избирательно компилировать части файла. Синтаксис директивы условной компиляции следующий: #if выражение! // эта часть файла компилируется, если выражение_! истинно #elif выражение_2 И эта часть файла компилируется, если выражение! ложно, а // выражение_2 истинно #else
Основы языка 73 И эта часть файла компилируется, когда все выражения ложны #endif. Директивы #elif и #else могут быть опущены. Пример. Простое исключение секции кода. V___________________________________________________ #if О // исключенная секция кода #endif Значения выражений в директивах #// и #elif должны быть целыми константами. В выражениях нельзя использовать опера- цию sizeof. В качестве выражения в директивах #if и #elif можно исполь- зовать оператор defined . Он проверяет, был ли определен макрос. Пример. Выбор между секциями кода. V__________________________________________________ #if defined(DEBUGGING) space = 1000; #else space - 10; #endif Директивы #ifdef и ttifndef являются сокращением для ди- ректив #lf defined и #if /defined соответственно. .Пример. Предотвращение многократной трансляции заголо- вочного файла sample.h. U________________________________________________________ #ifndef ___SAMPLE_H___ #define ___SAMPLE__H__ // здесь содержание файла #endif Если файл sample.h окажется многократно включенным в ис- ходный код программы, то при таком «обрамлении» его содержи- мого будет оттранслировано лишь первое вхождение заголовоч- ного файла.
74 Часть 1 Замечание. В Visual C++ та же цель достигается применени- ем ем директивы ttpragma once. Директива #error Директива #error вызывает сообщение об ошибке во время компиляции: #еггог сообщение_об_ошибке. Пример. Обеспечение правильности параметров компиляции. U________________________________________________ #if !defined(___cplusplus) #error Файл должен компилироваться в режиме С+ + #endif Сообщение может содержать идентификаторы макросов, ко- торые будут расширены препроцессором. Директива обычно при- меняется, когда не был определен необходимый макрос. Директива #Ппе С помощью директивы можно изменить внутренний счет- чик строк компилятора и имя компилируемого файла. Она имеет следующий вид: #line номеркетроки [«имя_файла»]. Директива #pragma Директива ttpragma позволяет влиять на процесс и результат компиляции, набор ее возможностей зависит от системы програм- мирования. Вот несколько опций для компилятора Visual C++: • ttpragma hdrstop — предписывает компилятору не включать дальнейшую информацию в прекомпилируемый заголовок; • ttpragma once — предписывает компилятору лишь однажды включать данный заголовочный файл в программу; • ttpragma warning — выборочно разрешает или подавляет пре- дупреждающие сообщения.
Объектно-ориентированное программирование 1. Классы 1.1. Классы и объекты Класс — это такой программный тип, который определяет не только данные, но и функции, применимые к этим данным. Конк- ретное значение такого типа называют объектом или экземпляром класса. Говорят, что данные задают состояние объекта, а функции — его поведение. В C++ объявление класса похоже на структуру в языке С, но помимо данных-членов класса, включает функции-члены, которые иначе называют методами. Пример. Объявить класс «дата». Объект хранит день, ме- сяц, год. Способен установить дату, сообщить ее и изменить на следующую. и_________________________________________________________ class date { public: int day, month, year; void set (int, int, int); void get (int&, int&, int&); void next (); }; Определение функций-членов можно расположить внутри или за пределами объявления класса. В первом случае транслятор по- пытается создать функции inline. Если определение функции-члена расположено за пределами объявления класса, имени определяе- мой функции должно предшествовать имя класса вместе с опера- цией разрешения видимости.
76 Часть 2 Все данные класса доступны из методов того же класса. XПример. Определение функции-члена date::set^ расположен- ное за пределами объявления класса. V________________________________ void date::set (int d, int m, int y) { day = d; month ~ m; year = у; } Напомним, что класс представляет собой тип данных, а объек- ты являются его значениями. Каждый объект имеет собственную копию всех данных класса. Чтобы создать объект в памяти, его надо определить, например, date d;. После создания объекта получить доступ к его членам можно при помощи операции прямого выбора, обозначаемой точкой. U________________________________________________________ cout << d.day; d.set(19,10,2001); 1.2. Инкапсуляция Инкапсуляция в объектно-ориентированном программирова- нии — это сокрытие деталей реализации класса. Класс вводит еще одну область видимости (другие области — это файл, функция, блок, прототип функции, пространство имен). В пределах класса видимы все его члены, за пределами — только некоторые. Спецификации доступа public и private регулируют видимость членов класса извне. Пример. Данные класса date скрыты от пользователя, а ме- тоды открыты. и_________________________________________________________ class date { private: int day, month, year; public:
Объектно-ориентированное программирование 77 void set (int,int,int); }; Члены класса всегда можно именовать, применяя операцию разрешения области видимости, например, вместо int day, month, year; в объявлении класса можно написать int date: :day, date: .-month, date::year;, но обычно операция разрешения видимости применяется лишь за пределами объявления класса. 1.3. Конструктор После создания объекта в памяти все его данные должны по- лучить значения. Конструктор — это функция, специально пред- назначенная для инициализации объекта. Конструктор носит то же имя, что и класс, и не возвращает значения. Класс может иметь несколько конструкторов. Пример. Класс с тремя конструкторами. •U_________________________________________ . class date { int day, month, year; public: date(int,int, int); date(char*); date(); }; Конструктор вызывается тогда, когда создается объект. Благо- даря конструкторам оператор объявления в C++ вызывает выпол- нение действий. Лоримеры инициализации объектов: U__________________________________________________ void main(int argc, char* argvf]) { date dl = date(19, 10, 2001);
78 Часть 2 // то же, но сокращенно date d2(19, 10, 2001); date d3 = date(”19-0ct-2001“); // когда в конструкторе ровно 1 параметр date d4 - "19-0ct-2001"; // работает конструктор без параметров date d5; } Если программист не определил конструктор, транслятор сам строит конструктор по умолчанию, который не имеет параметров и не выполняет никаких действий. При наличии в классе других кон- структоров конструктор без параметров требуется определять явно. Если объект не имеет явного конструктора, для его инициа- лизации можно воспользоваться списком. и________________________________________________________ struct А { int i; char* с; }; void main() { A a ~ {1, "kkk"}; } 1.4. Деструктор Деструктор решает задачу, обратную задаче конструктора. Он вызывается всякий раз, когда объект уничтожается. Имя дест- руктора состоит из знака тильды «~» и имени класса. Например, -date() . Деструктор не имеет параметров и не возвращает значения. Компилятор сам генерирует деструктор, если программист его не определил. Когда объект выходит из области видимости, деструк- тор вызывается автоматически. Явный вызов деструктора никог- да не требуется.
Объектно-ориентированное программирование 79 1.5. Указатели на объекты Объявление date* р создает указатель на объект класса date. Если р указывает на объект, обратиться к его членам можно при помощи двух операций (*р).set(), но для этого существует специ- альная операция косвенного выбора p-->set(). Указателю можно присвоить адрес уже существующего объекта. U________________________________________________________ date dl; date* р - &dl; Каждый объект имеет указатель на себя, который обознача- ется идентификатором this. Он является скрытым параметром каждой функции-члена. Для класса X указатель this имеет тип: X* const this;. 1.6. Константные методы Функцию-член можно объявить так, чтобы поля объекта были доступны ей только для чтения. и__________________________________- | int readme () const {/*тело функции*/};~| Указатель this, передаваемый такой функции, будет иметь тип: const X *const this. 1.7. Операции new и delete Объект размещается в свободной памяти (в куче) при помощи операции new. и______________________________________________________ Г dat~e*~p - new date(19, 30, 2001); | Если выделить память не удалось, то по новому стандарту вы- брасывается исключение, по старому — возвращается О.
80 Часть 2 Замечание. Программист может перегрузить операцию new для своего класса и тем самым изменить ее поведение. Удаляется объект из кучи операцией delete. и____________________________________ | delete р; Дважды удалять из кучи один и тот же объект нельзя! 1.8. Пример класса — список в свободной памяти Объявим класс «Список», который представляет собой связан- ный список элементов с указателями на первый и последний элемен- ты списка. Для задания типа элементов объявим структуру «Эле- мент», содержащую указатель на строку С в свободной памяти, и указатель на следующий элемент списка. В классе «Список» пре- дусмотрим методы добавления элемента в начало списка, в конец списка, вывод элементов списка в стандартный выходной поток. U________________________________________________________ #include <iostream> #include <cstring> using namespace std; /I Элемент списка struct Elem { char* line; Elem* next; Elem(const char* v) { line = new char[strlen(v) + 1]; strcpy(1ine, v); next - 0; } ~Elem() { deleted line; } }; // Список class List { Elem *first, *last;
Объект! ю-ориснтировашюе программирование 81 public: List () {first = last = 0;} ~List(){/* заглушка */} void addFirst(const char* v); \zoid add'Last (const char* v) {/* заглушка */}; void print() const; }; void List::addFirst(const char* v) { if (first -= 0) first - last - new Elem(v); else { Elem* p = new Elem(v); p->next - first; first = p; } void List::print() const { for (Elem* p - first; p L- last; p = p->next) { cout << p->line << " " ; } cout << p->line « endl; int main(int argc, char* argv[]) { List 1; 1.addFirst(” 1 ”); 1.addFirst("2"); 1.addFirst("3"); 1.print(); return 0;
82 Часть 2 1.9. Задачи Класс «Комплексное число» Объявите класс «Комплексное число», полями которого явля- ются действительная и мнимая части числа, а методами — сложе- ние и умножение на другое комплексное число, определение моду- ля и вывод на экран. Класс «Прямоугольник» Объявите класс «Прямоугольник» с полями: Int xl ,yl>x2, у2 (координаты левого верхнего и правого нижнего углов) и метода- ми: пересечься с другим прямоугольником, проверить, попадает ли точка в данный прямоугольник, масштабировать при условии не- подвижности верхнего левого угла, передвинуть по плоскости без вращения. Класс «Вектор» Объявите класс «Вектор», полем которого является массив чи- сел в свободной памяти, а методами — очистить вектор, добавить эле- мент в конец, вставить элемент в i-ю позицию, удалить f-й элемент. Класс «Рекурсивный список» Объявите класс, который реализует односвязный рекурсивный список строк в свободной памяти. Список представляется двумя указателями: указателем на строку в свободной памяти (поле info) и указателем на список же, только более короткий (поле tail). В частном случае одноэлементного списка этот указатель равен 0. 2. Производные классы 2.1. Наследование Производный класс (потомок) получает поля и методы уже существующего базового класса (предка). Наследование позволяет «вынести за скобки» то общее, что присуще нескольким классам, общие свойства определяются в базовом классе, а различия — в производных.
Объектно-ориентированное программирование 83 Пример базового и производного классов: Date и Birthday. U___________________________________________________ class Date { int day, month, year; public: Date(int,int,int); Date(char*); Date(); ); class Birthday: public Date { public: char* name; }; Конструкторы не наследуются. Если конструктор базового класса имеет параметры, он вызывается в списке инициализации конструктора производного класса. Конструктор без параметров вызывается автоматически. Деструктор производного класса всегда вызывает деструктор базового класса. 2.2. Уровни доступа Любой член класса имеет один из трех уровней доступа: • private (закрытый) — доступен только из функций-членов дан- ного класса и дружественных функций; • protected (защищенный) — доступен как private, а также из функций-членов производных классов; • public (открытый) — доступен всюду, где виден класс. В объявлении класса обычно сначала перечисляются все от- крытые члены, потом защищенные, далее закрытые. В заголовке производного класса указываются имена всех ба- зовых классов вместе со спецификаторами доступа. class Derived: спф_доступа Basel [, спф_доступа Base2,...] {...}; Замечание. Базовых классов будет больше одного, если наследование множественное (см. раздел «Множественное наследование »).
84 Часть 2 Спецификаторы определяют уровень доступа к членам базо- вого класса. Уровень доступа к члену определяется как меньший из двух — того, что член имеет в базовом классе, и того, что указан в производном классе (private < protected < public). Явное указание спецификатора доступа при наследовании не обязательно. По умолчанию принимается private для базовых клас- сов и public для базовых структур и объединений. Если базовый класс наследуется как private* его публичные члены будут иметь уровень private в производном классе. Одна- ко можно выборочно вернуть статус публичных некоторым чле- нам базового класса, переобъявив их в секции public производ- ного класса. Пример. Возврат утраченного статуса. и___________________________________________________ class Base { public: void f 1 () ; // открытый член базового класса и _____________________________________________ class Derived: private Base { public: Base::fl; // делает fl() снова открытым }; Если член описан в базовом классе как private* его никак нельзя сделать public в производном классе. Замечание. С помощью объявления некоторых членов зак- иЗ рытыми можно добиваться необычных эффектов. Например, если объявить закрытым деструктор, то будет запрещено раз- мещение объектов в стеке и глобальной памяти, а также слу- чайное употребление delete. 2.3. Виртуальные функции Механизм виртуальных функций позволяет уже в базовом классе вызывать функции, которые будут определены позднее в производных классах.
Объектно-ориентирова иное программирование 85 Пример. Класс с виртуальными функциями. Определим базовый класс date V______________________________________ class Date { protected: int day, month, year; public: Date(int, int,int) ; void set_year(int y); void print(); }; Date::Date(int d, int m, int y) { day - d; month - m; year = у; void Date::set_year(int y) { year = y; print(); void Date: .-print () { printf(“%d-%d~%d\n", day, month, year); и производный от него класс Birthday: и__________________________________________________ class Birthday: public Date { public: char name[80]; Birthday(int d, int m, int y, char* n) ; void print(); }; Birthday::Birthday(int d, int m, int y, char* n) : Date(d, m, y) {
86 Часть 2 strcpy(name, n); } void Birthday::print() { printf ("%d--%d~%d, dear %s\n" , day, month, year, name); } Следующий код __u_____________________________________________ Date* d = new Date(19, 10, 1851); Birthday* b = new Birthday(19, 10, 1851, "Me"); d->set_year(2001); b->set_y ear(2001) ; просто напечатает: «19-10-2001» два раза. Если же сделать метод date::print( ) виртуальным, V______________________________________________ [virtual void" print (int у) ; то тот же код напечатает: «19-10-2001» «19-10-2001, dear Me». Чтобы сделать метод виртуальным, достаточно объявить его с описателем virtual. При переопределении функции в производном классе специ- фикатор virtual писать не обязательно, переопределенная функция все равно будет виртуальной. Для виртуальных функций действуют следующие правила: а) виртуальную функцию нельзя объявлять как static; (о static см. в разделе «Статические элементы»); б) виртуальную функцию нельзя вызывать из конструктора, в противном случае замещающая функция будет вызвана в ус- ловиях, когда объект производного класса еще не готов; в) виртуальная функция должна быть определена в классе, где она впервые объявлена или должна быть чисто виртуальной, т.е. не иметь кода. V_______________________________________________________ [ virtual void print() - 0; |
Объектно-ориентированное программирование 87 Абстрактные классы Класс, содержащий чисто виртуальные функции, называется абстрактным. Объекты такого класса не могут быть созданы. Назна- чением абстрактных классов является отделение интерфейса от реа- лизации и обеспечение полиморфизма, о котором речь впереди. 2*4. Реализация виртуальных функций Виртуальные функции реализуются с использованием поздне- го связывания, которое заключается в следующем. Для каждого класса, имеющего виртуальные функции, транслятор строит таб- лицу виртуальных методов (viable), которая хранит адреса вирту- альных функций. Для любого класса в иерархии наследования ад- рес некоторой виртуальной функции имеет одно и то же смещение в viable. Каждый объект класса, имеющего виртуальные функции, со- держит скрытый указатель vptr на таблицу виртуальных функций класса. Компилятор автоматически вставляет в начало конструк- тора код, который инициализирует vptr объекта. Вызов виртуальной функции происходит через viable, на ко- торую указывает vptr объекта, делающего вызов. Адрес вызывае- мой функции определяется не во время трансляции (это было бы раннее связывание), а во время выполнения программы (позднее связывание). Это и позволяет из функции set__уеаг( ), унаследован- ной от базового класса, обратиться к функции print ( ), определен- ной в производном классе.
88 Часть 2 Замечание. Хотя методы объекта можно вызывать уже в кон- структоре, vtable для этого не используется. Поэтому, когда в конструкторе базового класса вызывается метод, это всегда будет метод базового класса, даже когда сам конструктор вызван в процессе создания объекта производного класса. То же относится и к косвенным вызовам. Пример. U______________________________________ class А { virtual void init() { cout << "A";} public: A() {init(); } }; class B: public A { void init() { cout « "B";} public: В () {init(); } }; void main() { A* b = new В; // будет напечатано AB } 2.5. Полиморфизм На объект класса Birthday может указывать как переменная типа Birthday*, так и переменная типа Date*. Независимо от типа указателя виртуальные методы будут вызываться в соответствии с истинным классом объекта. Пример. и_________________________________________ Date *pd; pd - new dateflO, 10, 2000); pd->print(); // Выводится объект класса date delete pd;
Объект! ю-орист гги рованное програм ми рова! шс 89 pd - new birthday(10, 10, 2000, "Peter"); pd->print(); // Выводится объект класса birthday Тот факт, что один и тот же код выполняется по-разному (pd- >prtnt( ) в данном примере и вызов виртуальной функцииprint() из функции set_уеаг( ) в предыдущем), произвел глубокое впечат- ление на программистов и был назван ими полиморфизмом. Выбор вызываемой функции зависит от того, на какую таблицу виртуаль- ных методов указывает vptr объекта, а это определяется тем, кон- структор какого класса инициализировал объект. Замечание. Пример полиморфизма иного типа в C++ дает перегрузка функций. 2.6. Список инициализации конструктора Если конструктор базового класса имеет параметры, он дол- жен вызываться явно в списке инициализации конструктора про- изводного класса. Пример. Конструктор со списком инициализации. и_________________________________________________ // Базовый класс class Complex { public: Complex( float re, float im ) { real = re; imag = im; } } // Производный класс class Triplex : public Complex { public: Triplex(float re, float im, int co): Complex(re, im)
90 Часть 2 { color = со; } } Данные-члены класса можно инициализировать и в теле кон- структора, и в списке инициализации. V______________________________________________________ Tripl ex(float re, float im, int co): Complex(re, im), color(co) {}; Список инициализации является единственным способом при- дать значение членам-константам, членам-ссылкам и членам- объектам, конструкторы которых имеют параметры. 2.7. Задачи Система из трех классов Объявите систему классов: «Точка», «Прямоугольник», «Эл- липс». Определите методы, которые перемещают фигуру по плос- кости, изменяют ее размеры и выводят на экран. Классы для работы с файловой системой Объявите класс File для работы с файловой системой (удаление, переименование, чтение и запись атрибутов). Унаследуйте от него класс Catalog для работы с каталогами (получение массива файлов, находящихся в каталоге) и вспомогательный класс Filter, объект ко- торого должен передаваться в метод получения содержимого. Клонирование объекта Объявите некоторый класс и напишите виртуальный метод, воз- вращающий копию объекта. Заместите его в производном классе. Список из полиморфных элементов Перепроектируйте класс «Список» таким образом, чтобы он был способен хранить элементы различных типов, производных от базового типа «Элемент». В класс «Элемент» добавьте виртуальный методprint() для вывода элемента в стандартный поток.
Объектно-ориентированное программирование 91 3. Дополнительные сведения о классах 3.1. Классы, структуры и объединения Единственным отличием классов от структур является умал- чиваемый уровень доступа к членам: для классов — это private, для структур — public. В отличие от структуры, объединение не просто дублирует класс, но дает такие возможности, которых у класса нет,— проеци- рование данных разных типов на одну область памяти. На объединения накладываются ограничения. Они не могут: • наследовать класс и быть базой для класса-наследника; • иметь члены-объекеты с конструкторами и деструкторами. В C++ имеется особый тип объединения — анонимное (anonimus union), которое просто сообщает компилятору, что все его члены за- нимают одно и тоже место в памяти. В остальном члены анонимного объединения ничем не отличаются от других переменных той же области видимости, и их имена не должны конфликтовать с имена- ми переменных. union { int i ; char ch [ 4 ]; i = 65; cout « c[OJ; // Напечатает символ ”A" 3.2. Присваивание и инициализация объектов Когда один объект присваивается другому, делается почленное копирование всех его данных-членов и объектов базового класса. Присвоить объекту В объект D можно, если их тип идентичен или класс D наследует класс В. То же справедливо и для указате- лей на объекты.
92 Часть 2 Замечание. Идентичность типов означает не только одина- CnimiJ ковое внутреннее устройство, но и совпадение имен. Если име- на все же отличаются, они должны быть синонимами, опре- деленными в инструкции typedef. Присваивание возможно, когда оба объекта уже созданы. При инициализации же нового объекта d2 уже готовым объек- том d выполняется не операция присваивания, а конструктор ко- пирования. U_____________________________________________________ // работает конструктор date(int, int, int) date d(19,10,2001); // работает конструктор копирования date d2 = dl; По аналогии с конструктором без параметров можно считать, что транслятор автоматически создает конструктор копирования, когда он не определен программистом явно. Неявный конструктор копирования выполняет почленное копирование данных объекта- прототипа. JJk Замечание. Почленное копирование не означает побайтовое. Для членов-объектов вызываются их конструкторы копи- рования. Пример явного определения конструкторов копирования. Для класса date: U____________________________________________________ date(const date &x) { day ~ x.day; month = x.month; year = x.year; } Аргумент копирующего конструктора обязан быть ссылкой, в противном случае возникнет бесконечная рекурсия. Пример. Копирующий конструктор класса list (см. пример из предыдущего раздела):
Объектно-ориентированное программирование 93 U____________________________________ list: :list (const list& 1) { info = _strdup(1.info); if (l.next) next - new list(*(1.next)); else next = NULL; } Замечание. По аналогии с инструкцией wJb date dl(d); можно подумать, что инструкция date d2 () ; создает объект d2 при помощи конструктора без параметров, но это не так! Эта инструкция является объявлением функ- ции без аргументов, которая возвращает объект date. Объект date создается инструкцией date d3;. Инициализация, а значит и вызов конструктора копирования происходит в трех случаях: • в операторе объявления, например, date d2 = dl; • когда объект передается функции как параметр; • когда объект возвращается в качестве значения функции. 3.3. Передача объектов функциям Как и другие параметры, объекты передаются в функцию по значению. При этом создается временная копия объекта, для чего вызывается конструктор копирования. После завершения функции временная копия уничтожается, для чего вызывается деструктор. 1. Пример. Объект создается и передается функции. U_____________________________________________________ date dl - date (1.9,10,2001) ; f (dl) ; В этом примере вызывается два конструктора — инициализа- ции и копирования и дважды вызывается деструктор.
94 Часть 2 Другой вариант того же самого: U ‘ date dl(19, 10, 2001); f (dl) ; Если же написать так U [ f(date(19, 10, 2001)); то вызывается только конструктор инициализации, а конструктор копирования не вызывается, Функции можно передать не сам объект, а его адрес — указа- тель или ссылку. При этом создается копия адреса, а не объекта. Если функция возвращает объект, то он создается во время работы функции и удаляется после ее завершения. Удаление объек- та происходит после того, как он будет скопирован в вызывающую программу. Пример. Функция получает и возвращает объект date. и_____________________________________________________ date ffdate х) { return х; } При выполнении блока U____________________ { date dl; date d2 - f(dl); } конструкторы и деструкторы работают в следующем порядке: а) конструктор по умолчанию для dl; б) конструктор копирования для параметра х; в) конструктор копирования для d2; г) деструктор для возвращаемого х; д) деструктор для d2\ е) деструктор для dl.
Объектпо-ориснтироваппос программирование 95 3.4. Массивы объектов Объекты можно объединять в массивы: date dl [ 2 J ;. Массивы можно инициализировать с помощью списков инициализации (не путать со списками инициализации конст- рукторов). и______________________________________________________ date dl[2] ~ {date(0,0,1), date(0,0,2)}; date d2 [2 ] = {date (" 19-0ct-2000") , date("19-Oct-2001")}; dated3[2] = {”19-0ct-2000”, ”19-0ct-2001”}; Инициализация массива d3 является сокращенной записью инициализации массива d2 (сравни с инициализацией одиночного объекта). В свободной памяти массивы объектов размещаются при по- мощи операции new'. date *d - new date[2];. При размещении в свободной памяти элементы массива объек- тов инициализируются только конструктором без параметров, по- этому он обязательно должен существовать. Массив объектов удаляется из свободной памяти операцией delete [ ] delete[J d;. 3.5. Дружественные функции и классы Дружественные функции имеют доступ ко всем членам клас- са, в том числе закрытым, но сами членами класса не являются. Это означает, что доступ к членам класса функция получает толь- ко через объект этого класса, а не через указатель this, как это де- лают методы класса. Дружественная функция определяется как обычная функция, но в объявление класса необходимо включить ее прототип с ключе- вым словом friend. ААример. Функция, распознающая високосный год, друже- ственная классу Date.
96 Часть 2 u_____________________________________________ // прототип функции в объявлении класса class Date { friend bool is_leap_year(const date& x); } // определение функции bool is__leap_year (const date& x) { return !(x.year % 4) && (x.year % 100) I I ’ (x.year % 400); } Функцию можно сделать дружественной сразу нескольким классам. Для этого достаточно поместить прототип функции в объ- явления этих классов. 4 Замечание. Сделать функцию дружественной нескольким 03 классам не удастся без предварительного объявления (forvard declaration) классов: class имя_класса;. Не являясь методами класса, дружественные функции не на- следуются. Функция может быть членом одного класса и дружественной другому: friend void Class::MemberFuncName( );. Дружественным может быть целый класс friend class FriendClass;.
Объектно-ориентированное программирование 97 К дружественности классов приложимы следующие правила: • дружественность не симметрична, если А объявляет В другом, это не означает обратное, что А — друг В; • дружественность не наследуется, если А объявляет В другом, наследники В не будут автоматически получать доступ к эле- ментам А; • дружественность не является переходным свойством, если А объявляет В другом, наследники А не будут автоматически признавать дружественность В. 3.6. Статические элементы Элемент данных, определенный как static, является общим для всех объектов данного класса. В программе статические эле- менты данных играют роль глобальных переменных. Память под статический элемент выделяется, даже если не существует никаких объектов класса. Обращаться к статическому элементу данных следует в форме имя__класеа:: элемент хотя теоретически, если существует экземпляр класса, это можно делать через него объект, элемент или указатель_на_объект->элемент. Статическая переменная может быть объявлена, но не может быть определена в объявлении класса. Определение статической пе- ременной должно находиться за пределами класса, но не внутри функции. Определение может сопровождаться инициализацией. Пример. Объявление и определение статического члена класса. U____________________________________________________ class А { publ1с: // объявление static int S; };
98 Часть 2 // определение int А::S; Статическим функциям-членам не передается при вызове ука- затель this. Из этого следует, что статическая функция-член мо- жет вызываться независимо от того, существует или нет какой-либо экземпляр класса, но может обращаться только к статическим чле- нам класса. Статическая функция не может быть виртуальной. 3.7. Константы и типы в объявлении класса В объявлении класса могут находиться объявления типов, в ча- стности, объявления других классов. Объявленным типом можно пользоваться и за пределами класса, если выполнять операцию раз- решения видимости. Пример. Тип, объявленный в классе. и________________________________________________ class А { public: typedef int INT; }; A::INT i 5; Замечание. Определить константу, например, static const int Llj С ~ 100; внутри объявления класса нельзя, но можно объя- вить постоянный статический член класса, который позднее надо будет определить. Пример. Объявление и определение постоянного статичес- кого члена класса. и______________________________________________________ class А { public: // объявление константы static const int С; };
Объек'1'но-ориентированное программирование 99 // определение константы const int А::С = 100; Замечание. Можно объявить постоянный не статический член класса и инициализировать его в списке инициализа- ции конструктора. и__________________________________________________ class А { public: const int С; A(): С(100) {}; }; но это не будет общая константа для всех объектов класса. 3.8. Вложенные классы Класс может быть объявлен внутри другого класса или внут- ри метода, также как любой другой тип. и_____________________________________________________ class А { class В { int i; } ; } ; А: : В * Ь -- new А: : В; Для вложенных классов, как и для обычных, допустимо опе- режающее объявление. U__________________________________________________________ class А { class В; }; class А::В { int i ; } ;
100 Часть 2 3.9* Указатели на члены класса Указатель на член фактически задает смещение члена от на- чала объекта. Чтобы воспользоваться методом или полем через ука- затель на него, необходимо иметь объект. Общий синтаксис указа- теля на член класса таков: ClassName::*p. Пример. Сравнение указателя на функцию с указателем на метод объекта. и_________________________________________________________ // Объявление обычной функции void f(int i) { cout « i « endl; } struct S { int x; S(int v) {x ~ v; } // Объявление метода void f() { cout << x << endl; } ); // Указатель на функцию (объявление и инициализация) void (*pf)(int) = &f; // Указатель на метод (объявление и инициализация) void (S::*qf)() - // Указатель на поле (объявление и инициализация) int S::*px = &S::х; // Вызов функции через указатель pf(7); // Вызов метода через указатель, // для этого нужен объект S* ps = new S(123); (ps->*qf)();
Объектно-ориентированное программирование 101 Пример передачи указателя на функцию другой функции. V___________________________________________________ // Функция F() , которая получает указатель на // другую функцию и вызывает ее void F( void(*f)(int) ) { f (5); } //А так можно вызвать F() F(&f); Такие указатели позволяют использовать методы класса в ка- честве функций обратного вызова. Указатели на данные просто округляют синтаксис. ЗЛО. Задачи Класс «Слово» Объявите класс «Слово», полем которого является массив сим- волов в свободной памяти с конструктором копирования и метода- ми: получить длину слова, определить расстояние между словами, сравнить объект с другим словом. Класс должен иметь статическое поле, определяющее порядок следования букв в алфавите. Класс «Депозитный вклад» Объявите класс «Депозитный вклад», полями которого явля- ется денежная сумма и дата последнего перерасчета, а методами — добавить сумму, начислить проценты, снять с вклада только про- центы. Статическое поле — годовой процент. Класс «Стек» Объявите класс «Стек» с методами push() ирор(). Сделайте это на основе массива. Класс «Очередь» Объявите класс «Очередь» с методамиput( ) и get( ). Сделайте это на основе связанного списка.
102 Часть 2 Класс «Очередь с приоритетами» Объявите класс «Очередь с приоритетами», который наследу- ет класс «Очередь». Каждый элемент очереди имеет приоритет — целое число. Первым обслуживается не первый пришедший, а эле- мент с самым высоким приоритетом. 4. Перегрузка операторов 4.1. Общие принципы Операции следует рассматривать как особый способ вызова функций. Пользователь может сам задать (перегрузить) функцию, которая представляет собой ту или иную операцию. Это возможно при условии, что хотя бы один операнд является объектом пользо- вательского, а не встроенного типа. Перегрузка операторов позволяет классам пользователя не отличаться синтаксически от встроенных типов данных. Только че- тыре оператора языка C++ не допускают перегрузку: • прямой выбор компоненты (.); • прямой доступ к компоненте (.* ); • разрешение видимости (г:); • условная операция ( ? :). Замечание. Для запоминания: все эти операторы содержат CUb хотя бы одну точку в своей записи. Перегрузка операторов подчиняется следующим правилам: • приоритет и ассоциативность операций не меняются при пе- регрузке; • смысл операции не меняется по отношению к встроенным ти- пам данных; • функция-оператор должна быть либо членом класса, либо при- нимать объекты в качестве аргументов, либо то и другое вместе; • операторы =, [ J, -> должны быть нестатическими функциями- членами; • функция-оператор не может иметь аргументов по умолчанию; • за исключением присваивания, функции-операторы, объявлен- ные как члены класса, наследуются производными классами.
Объектно-ориентированное программирование 103 В наследовании присваивания нет смысла, т.к. унаследован- ная от предка операция присваивания ничего не сможет сделать с теми полями, которых у предка нет. Имя функции-оператора состоит из ключевого слова operator, за которым следует знак операции. Количество параметров функ- ции, как правило, совпадает с количеством операндов операции, но если перегрузку осуществляет член класса, то левый операнд пред- ставлен объектом класса, а правый, если он есть,— параметром. Типы аргументов при вызове могут меняться в рамках общих пра- вил определения соответствия при перегрузке функций. Перегружать оператор-функцию можно многократно. Для определения того, какую из нескольких функций использовать, применяется обычный механизм разрешения перегрузки. Только три оператора: «&» и «,» имеют предопределен- ный смысл для объектов. 4.2. Перегрузка бинарных операторов Бинарная функция-оператор может быть определена как фун- кция-член класса с одним параметром (правым операндом) или как обычная функция с двумя параметрами (левым и правым опе- рандами). Пример перегрузки оператора «+» для класса Complex. U__________________________________________________ class Complex { float real, imag; public: Complex(float re, float im) { real - re; imag - im; ) Complex operator+ (const Complex ^second); }; Complex Complex::operator* (const Complex ^second) { return Complex (real * second. real., imag * second.imag); }
104 Часть 2 Покажем вариант перегрузки оператора «+» внешней функ- цией. Чтобы это работало, данные-члены должны быть публичны- ми или функция-оператор — дружественной. и_______________________________________________________ Complex operator* (const Complex kfirst, const Complex &second) { return Complex(first.real * second.real, first.imag * second.imag); } 4.3. Перегрузка операторов помещения и извлечения из потока Перегруженные операторы помещения и извлечения не могут быть членами класса, т.к. левый операнд является потоком, в ко- торый выводятся объекты. И помещение, и извлечение должны возвращать ссылку на поток, чтобы обеспечить написание таких цепочечных выражений, как cout << х << у << Z;. Пример. Помещение и извлечение из потока для класса Complex. U________________________________________ class Complex { friend ostream& operator«(ostream kstrm, const Complex &c) { return (strm«c. real « « « « c.imag); } friend istreamk operator» (istream &strm, Complex &c) { return (strm » c.real >> c.imag); } }
Объектно-ориентированное программирование 105 Чтобы получить доступ к закрытым членам класса Complex, функции-операторы объявлены дружественными по отношению к этому классу. Оператор помещения можно объявить немного по-иному, на- пример, и____________________________________________________ [friend. ostream& operators (ostream &strm, Complex c) [ но первоначальный вариант более эффективный, т.к. в нем не со- здается копия комплексного числа. 4.4. Перегрузка унарных операторов Унарная операция может быть определена как функция-член без параметров или как обычная функция с одним параметром. Пример. Перегрузка унарного минуса. U______________________________ Complex Complex::operator- () const { return Complex(-real, -imag); } Если вложить в эту операцию другой смысл — инвертирова- ние самого объекта, то функция-член будет иной. и_____________________ Complex &Complex: -.operator- () { real = - real; imag = - imag; return *this; } Обе операции вырабатывают одно и то же значение, но вторая изменяет состояние объекта, а первая нет. Более сложной является перегрузка унарной операции инкре- мента. Как известно, она существует в префиксном (++С) и постфик- сном (C++) вариантах. Обе операции равным образом изменяют зна- чение операнда, но вырабатываемое значение разное не только по величине, но и по типу. Значением префиксного инкремента
106 Часть 2 является ссылка на операнд, а значением постфиксного инкремента — первоначальное значение операнда. V_______________________________________________________ int i - 0, j - 0; cont << + *i ; // выведет 1 cout << j+* ; // выведет С Пример. Перегрузка префиксного инкремента. V Complex& Complex::operators* () { +*real; *+imag; return *this; } // +tC Функция возвращает ссылку на объект, что позволяет исполь- зовать результат операции в левой части оператора присваивания, например, +-Н = 5. Чтобы отличить постфиксную унарную операцию от префик- сной, постфиксная функция-оператор имеет фиктивный целый параметр. Постфиксная операция не может возвращать ссылку на опе- ранд, т.к. значение выражения не совпадает со значением операнда. В теле функции нужно создать временный объект, который и вер- нет функция. Пример. Перегрузка постфиксного инкремента. V__________________________________________________ Complex Complex::operator** (int) { return Complex (++rea], ++imag); } Хотя такая перегрузка кажется вполне естественной, все же для гарантированной согласованности смысла префиксного и пост- фиксного инкремента принято стандартным образом выражать постфиксный инкремент через префиксный. и______________________________________________________ Complex Complex::operator** (int) { Complex old - *this;
Объектн о-ориентирова иное п рогра м мирование 107 ++*this; return old; 4.5. Оператор присваивания Операция присваивания по умолчанию определена для любо- го класса и состоит в почленном копировании нестатических по- лей класса и объектов базового класса. Оператор присваивания следует перегружать, когда нельзя обойтись присваиванием по умолчанию. Например, какое-то поле является указателем на индивидуальную память объекта, разме- щенную в куче. Пример. Перегрузка присваивания членом класса. ____и._________________________________________________ Complex^ Complex: .-operator "(const Complex &c) { real = c.real; imag = c.imag; return *this; } Функция возвращает ссылку на текущий объект, что позво- ляет писать выражения вида X — Y — Z. Оператор присваивания следует отличать от конструктора ко- пирования. Конструктор копирования вызывается лишь однажды, в момент создания объекта. Операция присваивания может выпол- няться неограниченное количество раз. В момент создания объек- та его поля пусты, в момент присвоения объекту нового значения его поля могут хранить ссылки на занятые ресурсы, например, сво- бодную память. Правильная перегрузка оператора присваивания должна ис- ключать какие-либо действия в тривиальном случае «х = х» и ос- вобождать ресурсы, если они были заняты. Контроль допустимости копирования Чтобы запретить копирование объектов класса X, достаточ- но сделать закрытыми оператор присваивания и копирующий конструктор.
108 Часгь 2 u_____________________________________________ class X { X& operator^(const X& x) {return *this;} X(const X&) {} publi c: X() {} void main(int argc, char* argv[]) i X a; X b = a; // ошибка, конструктор закрыт X с; с - а; // ошибка, присваивание закрыто 4.6. Оператор индекса массива Оператор индекса массива можно перегрузить функцией-чле- ном с одним целым аргументом тип имя класса :: operator [] (int индекс). Замечание. Индекс не обязан быть целым. Пример. Перегрузка оператора индекса, которая позволяет по номеру дня недели определить его название, и class Day { public: char* operator[](int n) { switch (n) { case 1: return "Monday"; case 2: return "Tuesday"; case 3: return "Wednesday"; case 4: return "Thursday”; case 5: return "Friday"; case 6: return "Saturday"; case 7: return "Sunday";
Объектно-ориентированное программирование 109 default: return "”; } } }; Day d; cout « d[3]; // Выведет слово ’’Wednesday” Перегруженный оператор индекса не мешает нам объявлять массивы объектов класса Day. В этом случае первый оператор [] выделяет элемент массива, а второй является перегруженным. U______________________________________________________ Day d[10]; cout << d[1] [3] ; // будет выведено слово "Wednesday" Замечание. Функции, перегружающие операторы присваи- cLsJb вания «=», косвенного выбора «->», индексации массива «[]» и вызова функции «()» обязательно должны быть нестатичес- кими членами класса. 4.7. «Умные» указатели Пользователь может создать не только собственные типы дан- ных, но и собственные типы указателей на данные. Для них он мо- жет перегрузить те операции, в которых участвуют указатели: ра- зыменование *, индексацию [], косвенный выбор -> . Хотя на первый взгляд оператор косвенного выбора бинарный: class->member, компилятором он рассматривается как постфиксная унарная опе- рация, результат которой применяется к имени члена класса: (class->) member". Следовательно, перегружать его надо функцией-членом с од- ним аргументом. Замечание. Подобная позиция показывает, что связь между операторами и соответствующими функциями условна и оп- ределяется разработчиком языка, а не исключительно при- родой самих операций.
110 Часть 2 Такой подход к операции косвенного выбора открывает возмож- ность создания «умных» указателей — объектов, которые связывают с операцией косвенного выбора какие-то действия или проверки. Пример. Определить класс Ptr, объекты которого служат указателями на структуры типа У. При каждом указывании отсы- лается «донесение» в стандартный выходной поток. и_________________________________________________________ // структура Y struct Y { int m; }; // умный указатель на Y class Ptr { Y* р; public: Ptr(Y* V) { p - V; } Y* operator->() { // отсылка "донесения" cout << p->m << endl; return p; } }; void main(int argc, char* argvf]) { Ptr x(new Y); x->m ” -1; x ~ >m - 1 ; } Замечание. Создавать пользовательские указатели на СиЗ встроенные типы допустимо, но это не имеет смысла. Для них можно перегрузить operator->f ), но использовать его
Объектно-ориентированное программирование 111 в инфиксной форме все равно не удастся, поскольку встро- енные типы не имеют членов. Для обычных указателей операция косвенного выбора -> яв- ляется синонимом некоторых выражений с операциями *и[]. U______________________________________________________ [~~р -> m -- (*р) .т -- р [0] .т] Как обычно, для определенных пользователем операций таких гарантий не дается. При желании синонимию можно обеспечить, перегрузив операции * и []. 4.8. Задачи Класс «Длинное целое двоичное число» Объявите класс «Длинное целое двоичное число», объект ко- торого можно сконструировать из массива символов. Перегрузите все арифметические операции, операции сравнения, извлечение из потока и помещение в поток. Класс «String» Объявите класс String и перегрузите все операции, которые покажутся вам полезными при работе со строками, например: =% +=, операции сравнения. Подсчет ссылок Создайте умный указатель, подсчитывающий ссылки на пользовательский объект, который существует не более, чем в од- ном экземпляре. Объект возникает в памяти при создании первого указателя на него и удаляется из памяти при удалении последнего указателя на него. Эквивалентность выражений Перегрузите операции * и [] для «умного указателя» Ptr так, чтобы обеспечить эквивалентность выражений: р -> m -= (*р).m == р[0].m
112 Часть 2 5. Распределение памяти Статические объекты занимают постоянное место в памяти, автоматические объекты располагаются в стеке, оператор new раз- мещает объекты в свободной памяти. Существуют два оператора: ::new — общий, с глобальной областью видимости; X::new — частный, с видимостью только в классе X. Общий оператор распределяет память для объектов встроен- ных типов данных и классов, в которых new не перегружен. Перегружать оператор new следует: а) если нужно повысить эффективность распределения в спе- циальных случаях, например, создается много маленьких узлов сети; б) если нужно привязаться к конкретным адресам памяти; в) если нужно использовать специальные виды памяти, напри- мер, память какого-нибудь устройства или ПЗУ. 5.1. Распределение памяти и инициализация объекта Компилятор генерирует вызов распределителя памяти X: .operator new() и вызов конструктора Х() там, где находит опе- ратор «new X». Функция X::operator new() вызывается раньше конструктора, поэтому она должна вернуть указатель void*, а не X*, т.к. до вызова конструктора объект просто не существует. Кон- структор создает объект в уже выделенной памяти. X::operator new() имеет обязательный параметр типа size__t, через который в него передается количество байтов, необходимое для размещения объекта. X::operator new( ) можно делать статическим членом класса. Замечание. Тип size_ t — это зависящий от реализации це- лый тип, используемый для задания размеров объектов в памяти. Деструктор уничтожает объект, но не освобождает его память, полагая, что это сделает operator delete(). Поэтому функция X::operator delete( ) принимает аргумент типа void*, а не X*.
Объект) ю-орис!ттированнос программирова! тис 113 Пример. V______________________________ class X { //. . . public: void* operator new(size_t sz); void operator delete(void* p); По правилам наследования память для объектов производного класса может выделяться при помощи operator new() базового класса, и________________________________________________________ class Y: public X { / / объекты класса Y распределяются // с помощью функции X: .-operator new() } Чтобы этот механизм работал, функции X::operator new() нужен аргумент, указывающий, сколько памяти выделить — sizeof (Y) чаще всего не совпадает с sizeof (X). Поэтому параметр оператора new объявляется, но не передается при вызове, а до- бавляется компилятором. 5.2. Дополнительные параметры оператора new Кроме обязательного параметра, функция X::operator new() может иметь и другие параметры. Если они есть, синтаксис вызова следующий. U_____________________________________________________ X* рх - new(pl, р2,...) X; // pl, р2,... - дополнительные параметры Через дополнительные параметры можно передать конкрет- ный адрес памяти, по которому следует разместить объект, или другую информацию, например, объект, который отвечает за рас- пределение памяти — аллокатор. Пример перегрузки оператора new, который позволяет ини- циализировать выделяемую память.
114 Часть 2 и______________________________________________________ class Blanks { public: Blanks(){} void* operator new(size_t size, char chlnit) j / void* Blanks:: operator' new(size_t size, char chlnit) { void* pvTemp = malloc(size); if (pvTemp) memset(pvTemp, chlnit, size); return pvTemp; } void main() { Blanks* x - new(" " ) Blanks; } Поскольку для объекта Blanks глобальная операция new те- перь скрыта, оператор подобный этому: Blanks ^SomeBlanks ~ пей? Blanks; вызовет ошибку компиляции. 5.3. Освобождение памяти Освобождение памяти выполняется оператором delete, кото- рый получает указатель на область свободной памяти, которую надо освободить. Оператор работает после деструктора, поэтому указа- тель бестиповый. Пример переопределения оператора delete. и__________________________________________________ class X { public: static void operator delete(void* p) { cout << p << endl; // печатаем адрес
Объектно-ориентированное программирование 115 Между функциями operator new( ) и operator delete( ) имеется асимметрия: первую можно перегружать, вторую — нет (это похо- же на асимметрию между конструктором и деструктором). Каким бы способом вы не создали объект в памяти, уничтожает его един- ственный оператор delete(), который получает указатель на область памяти, занятую объектом. Если способ уничтожения объекта нужно сделать зависимым от способа размещения, то при создании объекта следует сохранить какую-то информацию за пределами объекта. Например, можно поместить что-нибудь в байты памяти, расположенные перед нача- лом объекта. Замечание. Распределение памяти более низкоуровневая опе- рация, чем создание объектов, поэтому подобные приемы до- пустимы. В случае, когда нужно уничтожить объект производного клас- са, а указатель на него имеет тип базового класса, не обойтись без виртуальных деструкторов. и
116 Часть 2 5.4. Выделение памяти для массива Для распределения памяти под массив объектов X функция X::operator new() не используется. Если бы ее можно было приме- нять к массивам, то автору X: operator new( ) пришлось бы програм- мировать и эту возможность, а это нужно не всегда. Для специального размещения и удаления массивов объектов следует перегружать функции: void* operator new[] (size_t) и void operator deletef] (void*). Распределитель памяти для массивов применим к масси- вам любой размерности, т.к. через обязательный параметр он просто получает общее количество памяти, которое необходи- мо выделить. 5.5. Нехватка памяти Поскольку распределение памяти проектировалось до появле- ния в языке исключений, были приняты следующие соглашения. При невозможности выделить память: • X::operator new() возвращает 0 и выражение «new X» также принимает значение 0; • конструктор объекта не вызывается; • вызывается обработчик new_Jiandler(), который переопре- деляется пользователем и устанавливается функцией set new handle г (адрес^обработчика). По современному стандарту языка при нехватке памяти вы- брасывается исключение (см.раздел «Обработка исключений»). 5.6 Перегрузка глобальных операторов Глобальные операторы new и delete представлены функциями: void* operator new (size t); void operator delete (void*); и
Объектно-ориентированное программирование 117 void* operator new[] (size_t); void operator delete[] (void*); Правильная перегрузка этих функций должна поддерживать все соглашения о нестандартных ситуациях при выделении и осво- бождении памяти. 5.7. Задачи Перегрузка new для объекта Объявите объект, хранящий одно целое число. Перегрузите опе- ратор new так, чтобы память выделялась из большого массива байтов. Распределите в памяти 1 млн. объектов и сравните затрачен- ное время при стандартном и перегруженном распределителе. Перегрузка new для массива объектов Перегрузите для того же объекта operator new[] и распреде- лите память для двумерного массива объектов. Продолжительность жизни объекта Перегрузите операторы new и delete для класса X так, чтобы при освобождении памяти на экран выводилось время пребывания в памяти объекта X. Для этого размещайте в свободной памяти и сам объект, и время его создания. 6. Обработка исключений Язык C++ имеет встроенный механизм обработки ошибок, на- зываемый обработкой исключений (exception handling). Назначение этого механизма в том, чтобы в момент возникновения ошибки со- хранить информацию о ней в объекте исключения и передать этот объект туда, где известно, как реагировать на ошибочную ситуацию. 6.1. Создание и перехват исключений Механизм исключений использует три ключевых слова: throw, try и catch. Исключительная ситуация создается (говорят «выбра- сывается исключение») при помощи оператора throw.
118 Часть 2 throw значение; где значение идентифицирует возникшее исключение и может быть любого типа. Пример. U__________________________________________________ if (у 0) throw "Деление на 0”; Z “ х / у; Если исключение не перехватывается в той же функции, где оно было выброшено, оператор throw завершает функцию подобно оператору return. Рискованный код, т.е. код, который может выбросить исклю- чение, помещают в блок try, и_______________________________________________________ try { // здесь код, способный выбросить исключение } а код обработки исключений помещают в блок catch, который всег- да следует за блоком try. и__________________;__________________________________ catch (исключение) { // здесь код обработки исключения } Все исключения соответствующего типа, выбрасываемые внут- ри блока try, попадают в блок catch и там обрабатываются. Пример. Исключение выбрасывается в данной функции. и______________________________________________ try { if (b == 0) throw 0; double d = a / b; cout << d;
Объектно-ориентированное программирование 119 } catch (int) { cout << " ERROR: division by zero."; } Заметим, что блок try реагирует не только на исключения, возбуждаемые непосредственно внутри него, но и на все не пере- хваченные исключения, возбуждаемые внутри функций прямо или косвенно вызываемых из блока try. Пример. Исключение выбрасывается в вызываемой функции. U____________________________________________________ double Son(double a, double b) if (b) return a / b; else throw 0; } void Father() try { double d - Son(3, 0) ; cout << d; } catch (int) { cout « "ERROR: division by zero."; } } 6.2. Обработка различных исключений Каждый блок catch способен ловить исключения того типа, который задан в его параметре, и всех типов, производных от типа параметра. Если нужно обрабатывать исключения разных типов, создают несколько блоков cath один за другим.
120 Часть 2 Пример. Обработка разнотипных исключений. V________________________________________ void main() { double х - 8.0, у; try { if {x == 0) throw 1; if (x < 0) throw "abc"; у = sqrtd / x) ; } catch (int) { cout « "Деление на О”; } catch (char*) { cout << ’’Отрицательное число”; } } Однотипные исключения можно различить по их значению. Пример. Обработка однотипных, но разных исключений. и_________________________________________________ try { if (х 0) throw 1; if (х < 0) throw 2; у = sqrt(l / х); } catch (int ex) { switch(ex) { case 1:
Объектно -ориентированное программирование 121 cout << "Деление на 0й; break; case 2: cout « “Отрицательное число"; break; } } Как видно из примера, в блоке catch можно указать не только тип, но и переменную исключения. Чтобы перехватить в блоке catch абсолютно все исключения, его аргументом должно быть многоточие U________________________________________________________ catch (...){ // здесь код обработки исключений } Такой блок следует располагать последним в серии блоков catch( ). Для правильной организации программы важна групповая обработка исключений. Это достигается ловлей общего предка груп- пы исключений, т.е. параметром блока catch должен быть указа- тель или ссылка на общего предка группы. Замечание. Если в параметре передается объект предка, а не указатель на него, при обработке исключения нельзя будет воспользоваться динамическим приведением типа и полимор- физмом. 6.3. Стандартная обработка исключения Те исключения, которые не будут обработаны в данном блоке try, попадут во внешний блок try. Если такого не существует, бу- дет вызвана функция terminate!) из стандартной библиотеки. Замечание. Библиотечная функция terminate!) вызывает wJb функцию abort() из стандартной библиотеки, но может быть изменена функцией set_termlnate( ).
122 Часть 2 6.4. Повторная обработка исключения Обработанное исключение автоматически уничтожается. Если необходимо продолжить обработку исключения во внешнем блоке try у его следует повторно выбросить оператором throw без параметра. Смысл повторного создания исключения в том, чтобы разослать ин- формацию об ошибке нескольким обработчикам. Пример повторного возбуждения исключения. _____U_____________________________________________________ try г t гу if (!х) throw 1; У = 1 / х; } catch (int mes) // внутренний блок { if (mes ~= 1) cout << "Деление на О"; throw; } } catch (int) // внешний блок cout << "Повторяю, деление на О"; } 6.5. Спецификация исключений в функции Для любой функции можно объявить типы исключений, ко- торые она может выбросить за свои пределы. Это делается при по- мощи ключевого слова throw в определении функции. Пример спецификации исключений. U_____________________________________ | double Son(double a, double b) throw(int, bool); ]
Объектно-ориеншрованное программирование 123 В скобках после слова throw находится список типов. Если внутри функции выбрасывается исключение не упомянутого в скобках типа, оно должно быть поймано в теле функции, иначе будет вызвана функция unexpected^ ) из стандартной библиотеки, что приведет к аварийному завершению программы. Пустой спи- сок означает, что функция не должна выбрасывать исключений. U______________________________________________________ | void f() throw () ; | JSL Замечание. Компилятор MS Visual C++ игнорирует специфи- CJj кацию исключений при объявлении функции. 6.6. Стандартные исключения В заголовочном файле стандартной библиотеки <stdexcept> объявлены классы исключений, которыми следует пользоваться. Отступами показана иерархия классов, а их смысл понятен из на- званий. U_____________ class exception; class logic_error; class domain„error; class invalid_argument; class length„error; class out„of„range; class runtime_error; class range„error; class overflow_error; class underflow_error; 6.7. Исключения, выбрасываемые оператором new В первых версиях языка при неудачной попытке выделения памяти оператор new возвращал 0. По современному стандарту языка оператор new должен вы- брасывать исключение bad alloc. Гарантируется, что оператору new всегда хватит памяти, чтобы выбросить исключение bad alloc.
124 Часть 2 Замечание. В Visual С+4- 6.0 оператор ::пеш при нехватке па- мяти возвращает 0. В С 4—FBuilder при нехватке памяти опера- тор ::new выполняет пользовательскую функцию F, задавае- мую при помощи вызова set_new_handler( &F), По умолчанию эта функция выбрасывает исключение bad alloc. Если задать set new—handler(0 ), то оператор ::new станет возвращать 0. 6.8. Исключения в конструкторах Если в конструкторе выброшено исключение, объект не счи- тается созданным и его деструктор никогда не выполнится. Если к моменту возникновения исключения конструктор успел захва- тить какие-то ресурсы, то возникает проблема их освобождения. Пример. U ________________________________________ // Объект А моделирует ресурс struct А { А() { cout << ”1 am" << endl; } -А() { cout « "----<< endl; } } ; class В { A *a; public: B() { a ~ new A; // Исключение не даст деструктору освободить // ресурс throw 1; } ~В () { delete а; } }; void main() { В Ь; }
Объектно-ориентированное программирование 125 Создатель C++ полагает, что при последовательном проведе- нии принципа «инициализация есть выделение ресурса» релиза- ция могла бы взять на себя контроль за высвобожднием ресурсов, занятых незавершенным конструктором. Пример. Определяем указатель на ресурс, в конструкторе кото- рого происходит захват, а в деструкторе — высвобождение ресурса. U________________________________________________________ struct Aptr { А* р; Aptr(A* v) { р - v; } -Aptr() { delete р; } }; class В2 { Aptr а; public: В2(): a(new А){ throw 1; } ~В2 () { delete а; } }; При раскрутке стека в процессе обработки исключения систе- ма могла бы вызвать деструктор полностью созданного объекта Aptr и тем самым освободить ресурс, захваченный в конструкторе а, но в Visual C++ 6.0 и Borland C++Builder этого не происходит. Поэтому программисту остается не давать исключению покидать конструк- тор, пока все захваченные до возникновения ошибки ресурсы не будут освобождены. 6.9. Исключения в деструкторах Если деструктор выбросит исключение в процессе обработ- ки другого исключения (раскрутка стека), это будет расцени- ваться как ошибка механизма исключений. В то же время при нормальном вызове деструктора (выход из области видимости, выполнение оператора delete) исключения допустимы. Отличить
126 Часть 2 «нормальный» вызов от аварийного позволит библиотечная функ- ция uncautch_exception(), которая возвращает true, если исклю- чение находится в процессе обработки. Вы хорошо сделаете, если вообще не позволите исключениям покидать деструктор. 6.10. Исключения или управляющие конструкции? Язык не запрещает использовать механизм исключений в роли управляющих конструкций. X Ари мер, в котором для выхода из вложенных циклов вы- брасывается исключение. void main() { const int N = 3; int m[N] [NJ - {{1,2,3} , {1,2,3}, {1,2,3} }; int i, j, key ~ 4; try { for (i = 0; i < N; i++) for (j - 0; j < N; J++) if (m[i][j] == key) throw 1; } catch (int) { cout << i << j << endl; return; }; cout << "Ключ не найден." << endl ; } Как правило, такое применение ничем не оправдано, обработ- ка исключений происходит медленнее, чем простая передача уп- равления, и качество кода ухудшается. Но иногда это может быть хорошим решением, например, выход из рекурсивного поиска при нахождении первого из допустимых объектов.
Объектно-ориентированное программирование 127 6.11. Задачи Исключения в классе «Длинное целое число* Введите в методы класса «Длинное целое число» средства воз- буждения и обработки исключений. Охватите все возможные ис- ключительные ситуации. Исключения в классе «String» Сделайте то же в методах класса String. Неупотребление return Каким образом при программировании можно обойтись без инструкции return ? 7. Множественное наследование 7.1. Базовые классы В C++ допускается множественное наследование, в этом слу- чае класс является производным от нескольких базовых классов. class С: public A, public В { Множественное наследование позволяет сочетать в одном клас- се «перпендикулярные» свойства нескольких предков, например, возможность отображения в окне и способность быть отдельным потоком выполнения. Производный класс С наследует все члены базовых классов А и В. Если члены базовых классов имеют одинаковые имена, не- однозначность устраняется операцией разрешения видимости: А::имя или В::имя. При создании производного объекта конструкторы базовых классов вызываются в том же порядке, в каком базовые классы перечисляются в объявлении производного класса. Деструкторы выполняются в обратном порядке. В списке инициализации конструктора вызываются все нетри- виальные конструкторы базовых классов. U____________________________________________________ I С (int х, int у) : А(х) , В(у) {...} ; I
128 Часть 2 7.2. Представление объектов в памяти При множественном наследовании у объекта может быть не- сколько таблиц виртуальных функций. Пример. Класс С наследует классы А и В. и class А { public: virtual void f() { } ; }; class В { public: virtual void f() { } ; virtual void g() { }; } ; class C: public A, public В { public: virtual void f() { }; }; Объект класса С мог бы образом: располагаться в памяти следующим Такой подход требует дополнения таблицы витуальных функ- ций vtbl еще одним столбцом. В основном столбце vtbl стоит адрес виртуальной функции, а в дополнительном — куда должен быть направлен указатель this при выполнении функции.
Объектно-ориентированное программирование 129 Заметим, что преобразование типа указателя от С* к В* изме- няет числовое значение указателя. В этом легко убедиться на сле- дующем примере: и____________________________________________________ С* рс = new С; А* ра = рс; В* pb “ рс; cout << рс << ” “ « ра << " " << pb « endl; 7.3. Множественное наследование с общим предком Возможен случай, когда оба класса А и В имеют в числе пред- ков класс R (рисунок а). R R R II / \ АВ АВ \ / \ / С С а) б) В этом случае класс АВ косвенно наследует два разных экземп- ляра класса R и при обращении к членам R из функций класса АВ потребуется уточнить, какой из двух объектов R имеется в виду: А::имя или В::имя. Пример. Множественное наследование с общим предком. V__________________________________________________ class R { public: void f() { cout « "f from R"; }; }; class A: public R { public: void f() { cout « ”f from A”; }; }; class B: public R { public:
130 Часть 2 void f() { cout << "f from B"; }; } ; class C: public A, public В { public: void f() { cout << ”f from C" << endl; }; }; ////////////// Использование классов ///////////// С С ; с. f () ; // ”f from C” С.А::f(); // ”f from A" с . В : : f ( ) ; // "f from Btt c.R::f(); // Ошибка неоднозначности ((A)c).R::f(); // "f from R“ 7.4. Виртуальные базовые классы Чтобы объект С содержал не два, а один экземпляр класса R, класс R надо объявить виртуальным в определении классов А и В. л cl ass A: virtual public R class B: virtual public R class C: public A, public В С С ; C.R: : f ( ) ; // Ошибки неоднозначности нет Общий объект базового класса экономит ресурсы и, при необхо- димости, позволяет установить связь между классами-« братьями ». Примеры применения виртуальных базовых классов дает стан- дартная библиотека. *os istream ~| ostream | iostream
Объектно-ориентированное программирование 131 7.5. Почему наследование называется виртуальным При обычном множественном наследовании положение каж- дого подобъекта в памяти фиксировано и определяется порядком перечисления предков в объявлении класса-наследника. При виртуальном наследовании положение общего предка не определено и задается смещением, которое для каждого потомка свое. Это похоже на то, как задается смещение виртуальных функ- ций в vtbly отсюда и сходство в названии. Поскольку положение виртуального предка не фиксировано, по указателю на его подобъект нельзя определить, где начинаются подобъекты потомков. Из этого вытекает запрет C++ на нисходя- щее приведение типа от виртуального предка к потомку. V________________________________________________________ class А { }; class В: virtual public А{ }; // Восходящее преобразование возможно А* а = new В; // Ошибка! Нельзя приводить от виртуального предка В* Ь ~ (В*)а; Восходящее же приведение возможно, так как смещение вирту- ального базового объекта хранится в каждом подобъекте-потомке. 7.6. Виртуальные базовые классы и виртуальные функции Виртуальные функции общего базового класса могут незави- симо перекрываться всеми наследниками. Пример. Система классов с виртуальными функциями. U________________________________________________ class R { public: virtual void f() {
132 Часть 2 cout << "f from R” « endl; } virtual void g() { cout << ”g from R" « endl; } }; class A: virtual public R { public: void g () { cout << ”g from A" « endl; } }; class B: virtual public R { public: void f() { cout << “f from B” << endl; } }; class C: public A, public В { }; void main(int argc, char* argv[]) { С* pc = new С; // // Все указатели указывают на один и тот же объект А* ра В* pb = pab? ~ pab; pc->f() ; // "f from В” pc->g() ; // "g from А” pa->f() ; // "f from В” pb->g() ; / / ’’ g from А" Правило доминирования имен состоит в том, что если класс У происходит от класса X, то имя Y::f доминирует над именем X::f. Если одно имя доминирует над другим, то компилятор однозначно выбирает доминирующее имя для разрешения вызова функции или выбора поля.
Объектно-ориентированное программирование 133 7.7. Задачи Множественное наследование. Объявите систему классов: «Точка», «Отрезок», «Прямоуголь- ник», «Ящик». Классы «Отрезок» и «Прямоугольник» унаследо- вать от «Точки», а «Ящик» — от «Отрезка» и «Прямоугольника». 8. Преобразования типов 8.1. Идентификация типа во время исполнения Механизм идентификации типа во время исполнения (Run- Time Type Identification — RTTI) состоит из трех частей: • оператор dynamic_ca$t, приводящий от типа «указатель на ба- зовый класс» к типу «указатель на производный класс»; • оператор typeid для определения точного типа при наличии ука- зателя на базовый класс; • структура typeinfo для получения дополнительной информа- ции о типе. Информация RTTI в C++ доступна лишь для полиморфных типов, т.е. таких, которые имеют виртуальные методы. Замечание. Динамическая идентификация типов возможна, СХЭ только если компилятор включил в программу информацию о типах времени выполнения. В MS Visual C++ за это отвеча- ет опция компилятора /GR или меню Prpject / Settitg, вклад- ка C/C++, список Category « C++ Language. 8.2. Оператор dynamic—cast Оператор dynamic_cast — основной способ использования ди- намической информации о типе. Он приводит указатели или ссыл- ки и сочетает приведение типа с его проверкой. Синтаксис опера- тора выбран таким, чтобы сделать его заметным в программе. Он похож на синтаксис шаблонов: dynamic_cast <целевой_тип> (выражение).
134 Часть 2 Оператор может привести тип В* к типу D* лишь в том слу- чае, если указатель В* действительно указывает на объект D и D является потомком В (нисходящее преобразование). В отличие от традиционной формы преобразования — (целе- вой тип) выражение — dynamic__cast проверяет во время выполне- ния фактический тип объекта и возвращает 0, если преобразова- ние не допустимо. Пример нисходящего преобразования. V_____________________________________________________ В* d = new D; D* dl - dynamic cast<D*> (d) ; Информации времени выполнения достаточно, чтобы выпол- нить приведение от виртуального базового класса, поэтому запрет на этот вид приведения оператором dynamic cast снимается. ________________________________;_________________________ class В { virtual void f() {}; }; class D: virtual public В { }; void main() { // восходящее преобразование в* b ~ new D; // нисходящее преобразование D* d - dynamic_cast<D*>(b); } Оператор typeid() Оператор dynamic cast позволяет определить, является ли объект наследником некоторого типа, но не дает возможности ус- тановить точный тип объекта. Информацию о типе объекта полу- чают с помощью оператора typeid. Оператор typeid (аргумент)
Объектно-ориентированное программирование 135 возвращает ссылку на объект типа typeinfo, который описывает аргумент оператора — тип или выражение. Класс typejnfo Чтобы использовать этот класс, в программу нужно включить заголовок <typeinfo>. Точное определение класса type_info зависит от реализации, но это полиморфный тип, в котором имеются операторы сравнения и функция, возвращающая имя типа. int operator == (const type_info& объект); int operator != (const type_info& объект); const char* name (); int before (const type_info& объект). Функция beforef ) возвращает истину, если имя типа вызыва- ющего объекта предшествует в алфавитном порядке имени типа объекта-аргумента. Она нужна, чтобы объекты type info можно было сортировать, хранить в хэш-таблицах и т.п. ХХример идентификации полиморфных типов. и__________________________________________________ class В { public: virtual void х (){}; ); class D : public В { public: void x() {}; void у() {}; }; В* b - new D; cout « typeid(*b).name(); // напечатает "class D" Слова «class D» будут напечатаны только при наличии хотя бы одной виртуальной функции, иначе будет напечатано «class В». Оператор typeid может работать и с обобщенными классами (см. раздел «Шаблоны»).
136 Часть 2 Модель размещения объекта в памяти Стрелка, ведущая от vtbl к объекту МуТ, представляет сме- щение, по которому можно найти начало полного объекта, имея лишь указатель на полиморфный подобъект* Это точный аналог смещения delta, используемого для реализации виртуальных функций при множественном наследовании. Это смещение и поз- воляет оператору dynamic cast выполнять приведение от вирту- альных предков. 8.3. Другие преобразователи типов Новые операторы приведения типов делят между собой функ- циональность старого приведения. Помимо уже рассмотренного оператора dynamic cast это: • statiC-Cast — преобразование типов, основанное на статичес- кой информации о типе; • const _cast — изменение статуса const и volatile*, • reinterpret_cast — «опасное» приведение типов. Оператор static_cast Как и dynamiccast, он выполняет навигацию по иерархии классов, но использует не RTTI, а статическую информацию о типе. С его помощью преобразуют перечисления в целые и обратно или типы с плавающей точкой — в целые. и enum Е {one, two};
Объектно-ориентированное программирование 137 Ее- stat ic__cast<E> (1) ; int j - static—cast<int>(one); long i = static—cast<long>(0.5); Static cast может привести указатель на базовый класс к ука- зателю на производный класс, но такое приведение не является безопасным. Статическое приведение может привести к скрытым ошибкам. Пример. и__________________________________________________ struct В { virtual void х(){}; }; struct D : public В { int i ; void x() {}; void у() {}; }; // Создать объект базового класса В* Ь ~ new В; // Компилятор не возразит против приведения типа D* d - static—cast<D*>(b); // ОШИБКА1 Функции у() нет у объекта d->y(); // ОШИБКА! Переменной i нет у объекта d->i ~ 5; Оператор static_cast можно применять для полиморфных и не- полиморфных типов, когда программист уверен в корректности преобразования. Оператор const—cast Используется для добавления-удаления атрибутов const и volatile (прочие преобразователи этого не делают). Целевой тип
138 Часть 2 должен совпадать с исходным типом за исключением этих атри- бутов. Обычно с помощью оператора constcasl объект лишают ат- рибута const. Пример применения оператора const_cast. V__________________________________________________ // указатель на константу const int* pc = new int(7); // указатель на переменную int* pv; pv = pc; / / ОШИБКА! pv ~ const_cast<int*>(pc); *pv = 8; Снять атрибут const с переменной, объявленной как const int i = 5; чтобы затем изменить ее значение, нельзя. Оператор reinterpret— cast Оператор reinterpret __cast используется для любых преобра- зований указателей несовместимых типов: char* — int*, Classi* — Class2*, причем классы не обязаны находится в отношении на- следования. В результате применения reinterpretcast получаются значения другого типа с той же последовательностью байтов. Преобразование reinterpret_cast можно применить для преоб- разования указателей в целые с целью последующего вывода или низкоуровневой работы с памятью. Для преобразований указателей на функции также следует использовать reinterpret cast. u____________________________ ! / Указатель на функцию без параметров void (*pf) () ; // Функция с одним параметром void fl(int) {}; void main() {
Объектно-ориентированное программирование 139 pf = reinterpret_cast<void(*j ()>(f1); } Пример некорректного преобразования reinterpret cast при множественном наследовании. U_______________________________________________________ class А { }; class В { }; class С: public A, public В { }; void main() { В* b ~ new С; // Указатель на начало объекта С С* cl ~ static__cast<C*> (Ъ) ; // ОШИБКА! С* с2 ~ reinterpret—cast<C*>(b); } 8.4. Функции преобразования объектов Такие функции преобразуют объект в значение другого типа, зачастую это тип, встроенный в язык. Пример. Определение функции преобразования объекта к целому типу. и class A { int i; public: A(int v): : i (v) {} operator int () { return i; } }; void main() { A a ( 5 ) ; if ( a =• = 5) cout < << a 4- 3; }
140 Часть 2 Замечание. Конструктор с одним параметром можно рас- СХЗ сматривать как способ неявного обратного преобразования — от существующего типа к типу объекта. Пример кеиотруктере . роли нреовразо.а™» типа. U а А (6) ; // или, что то же самое а = 6; // здесь сначала выполняется конструктор // с одним параметром, петом присваивание а = а + 1; // заметим, что это нельзя заменить на // а++ или а •+= 1 Неявное преобразование вида А « 6; можно запретить с помо- щью спецификатора конструктора explicit. U________________________________________________________ [ explicit A(int v): i(v) {}; | 8.5. Задачи Массив в свободной памяти Расположите в свободной памяти массив из 100 целых пере- менных, не используя оператор new. Указание: воспользуйтесь функцией malloc() и оператором reinterpret cast. Смещения подобъектов Дана система классов: class A { virtual void £() {} }; class В { virtual void f 0 {} }; class С { virtual void f (){} } ; class D: public A, public В , , public C {}; Определите смещения подобъектов А, В, С в объекте/).
Объектно-ориентированное программирование 141 9. Пространства имен 9.1. Определение пространства имен В языке С все имена программ и библиотек находятся в одном глобальном пространстве имен. В такой ситуации есть реальная угроза конфликта имен при использовании библиотек от различ- ных производителей. В языке C++ проблема решена при помощи механизма пространства имен, который включает: • определение пространства имен как еще одной области дей- ствия (другие области действия: объявление класса, определе- ние функции, заголовок функции); • различные способы обратится к члену заданного пространства имен. Синтаксис определения пространства имен следующий: namespace ИмяПространства { // объявления и определения Замечание. Ближайшим подобием пространства имен явля- Ъ|шиЛ ется класс с только статическими членами. Пример пространства имен. и_____________________________________________ namespace А { void f(int); void f(char); class String {/*...*/}; } Имена, объявленные внутри фигурных скобок, принадлежат пространству имен А и не конфликтуют ни с глобальными имена- ми, ни с именами из других пространств. 9.2. Доступ к членам пространства За пределами пространства А программист может обращаться к их членам, добавляя имя пространства.
142 Часть 2 u______________________________________________________ A::String si ~ "abc"; A : : f (1) ; Вместо этого можно явно указать, что для отдельного имени использование квалификатора излишне. Это делается при помощи using-объявления. U______________________________________________________ using A::String; using А::f; String si = "abc"; f(1) ; Можно сделать доступными без явной квалификации все име- на из некоторого пространства. Это достигается при помощи using- директивы. U________________________________________________________ using namespace А; String si = "abc"; £(1); Пространство имен открыто, т.е. в него всегда можно добавить новые имена при помощи еще одной инструкции namespace. U________________________________________________________ namespaсе А { // еще что-нибудь } 9.3. Различие между объявлением и директивой Говорят, что using-объявление вводит (объявляет) локальный синоним для одного имени из пространства. Using-объявление под- чиняется тем же правилам, что обычное объявление: • локально объявленное имя скрывает такое же глобальное; • возможен конфликт с таким же локальным именем; • возможна перегрузка функций. и namespace А {
Объект!ю-ориснтированнос программирование 143 inc k, i - 1; void f(char); class String; int i ~ 0; / / Переменная из глобального пространства void main() using A: zig- void f(int); // перегрузка функции void f(char); using A::i; // ОШИБКА, конфликт имен i + + ; // i-локальный синоним для A::i using A::k; i n t k; // ОШИБКА, повторное определение } Using-директива не вводит все имена из пространства имен, а просто делает их доступными, как доступны глобальные имена. Реакция компилятора на возможные ошибки откладывается до конкретного употребления имени. и_____________________________________________________ using namespace А; void f(int); // Перегрузка функции void f(char); // Повторное объявление функции i~+; /,/ ОШИБКА, неоднозначность (::i и A::i) int k; // Определение локальной переменной к Using-директивы — это инструмент, облегчающий переход к программированию с использованием пространства имен. Про- граммы станут яснее, если использовать явную квалификацию или using-объявления. 9.4. Прочие особенности Псевдонимы пространства имен Чтобы облегчить явную квалификацию, пространству имен можно дать псевдоним.
144 Часть 2 u name space KharкоvNa tionalUniver s i ty О f Radi oE1ec t ronic { int x; } namespace KNURE = KharkovNationalUniversityOfRadioElectronic; using KTURE::x; Глобальная область действия С появлением пространств имен глобальная область действия стала еще одним пространством имен. Запись ::f означает, что имя f объявлено в глобальном пространстве имен, а запись X::f означа- ет, что имя объявлено в пространстве имен X. Стандартное пространство имен Все объявления стандартной библиотеки C++ находятся внут- ри пространства имен std, поэтому их надо вводить в действие объявлением using, делать доступными using-директивой или ква- лифицировать явно. и__________________________________________________________ | std:~~:cout << 345; j Анонимное пространство имен Анонимное пространство имен позволяет ограничить область действия идентификаторов одним файлом. Вне этого файла увидеть идентификатор нельзя. и_______________________________________________________ namespace { int х; } Ранее задачу ограничения действия глобальных переменных одной единицей трансляции решал спецификатор static. Благода- ря пространствам имен за static можно оставить только один смысл — объект в статической памяти.
Объект но-ориci ттированнос i грограммирование 145 9.5. Спецификаторы сборки Для связи C++ с другими языками программирования в C++ поддерживаются спецификаторы сборки. Они определяют согла- шения о порядке передачи параметров и освобождении стека. Фор- мат спецификатора: extern "язык” прототип_функции; или extern ’’язык” { / / пр ото типы. ..функций } Visual C++ поддерживает спецификаторы «С» и «C++», а так- же специфичные только для Microsoft: __cdecl — стек очищает вызывающая функция, что позволяет вызывать функции с переменным количеством аргументов, но увеличивает объем кода. Аргументы попадают в стек справа налево. Это спецификация по умолчанию. Имена функций де- корируются подчерком перед именем. __stdcall — стек очищает вызываемая функция, аргументы по- падают в стек справа налево. Имена функций декорируются подчерком перед именем и суффиксом, зависящим от типа и ко- личества аргументов, например, func( int a, double b ) будет названа _func@l 2. Эта спецификация применяется для функ- ций Win32 API. __fastcall — первые два аргументы размером DWORD или меньше передаются функции через регистры процессора, остальные — через стек справа налево. Стек очищает вызываемая функция. 9.6. Задачи Класс и пространство имен Объявите константу с, переменную и и функцию f() как ста- тические члены класса С и как члены пространства имен S. Сде- лайте так, чтобы S::f() вызывала C::f(Л a C::f() вызывала S::f()•
Wb3 Стандартная библиотека шаблонов 1. Шаблоны Обобщенное программирование достигается не только с по- мощью наследования и полиморфизма, но и применением шаб- лонов. Шаблоны позволяют определить параметризованные классы и функции, в которых параметрами служат не только пе- ременные, но и типы. Шаблоны — это механизм генерации типов. Сами по себе они не являются типами, никак не представлены во время выполнения и не оказывают влияния на модель размещения объектов в памяти. Сначала в C++ были введены только шаблоны классов и они использовались для проектирования контейнеров. Позже были добавлены шаблоны функций. 1.1. Обобщенные функции (шаблоны функций) Пример. Шаблон функции выбора минимальной величины. U__________________________________________________ template <class Т> Т min (Т х, Т у) { return х < у ? х : у; } Угловые скобки используются с целью подчеркнуть иную при- роду аргументов шаблона. Ключевое слово template введено, что- бы шаблоны были легко различимы в программе. Слово class обо- значает не только класс, но вообще любой тип. Вместо слова class в шаблоне можно писать слово typename. Пример. Использование шаблона функции min.
Стандартная библиотека шаблонов 147 и int zl " min(3, 5); long z2 - min(333333333, 555555555); doublе z3 - min(3333333.3, 5555555.5); char z4 - min(“a", "b"); inc* z5 - min(&zl, new int); char* z6 ~ min("bbbbbb“, ”aaaaaa“); В данном примере компилятор создаст шесть различных функ- ций — по числу различных типов параметров в операторах вызова функций. Использование шаблонов не снижает эффективности работы программ. Если типы аргументов функции min( ) разные, компиля- тор не сможет самостоятельно перегрузить функцию (инстан- цировать шаблон). Здесь ему можно помочь, указав необходи- мую подстановку. U_______________ | min <float> ( 8. 5, 9); | 1.2. Параметры шаблонов Кроме типов, параметрами шаблонов могут быть парамет- ризованные типы (т.е. шаблоны) и обычные переменные. Одни параметры шаблона могут использоваться для задания других параметров. Пример. Определение функции, которая возвращает значе- ние порога, если аргумент больше порога. и_______________________________________________________ template cclass Т, Т def> Т threshold(const Т& х) return х <. def '? х : def; } Параметры шаблона, которые не являются типами, должны замещаться константными выражениями, адресами объектов или указателями на члены классов. U_____________________________________________________ | cout « threshold<int, 5> (10); |
148 Часть 3 Указатели-аргументы шаблона должны иметь форму &of, где of — имя объекта или функции, либо Л где f — имя функции. Таким образом, строковые литералы в качестве аргументов шаб- лона запрещены. Параметр шаблона, не являющийся типом, являются констан- той в теле шаблона, изменение его в теле шаблона не допустимо. Бели параметром шаблона служит класс, он должен обеспечить интерфейс, необходимый шаблону. Так, если мы хотим применить обобщенную функцию min() к объектам класса В, он должен до- пускать сравнение а__________________;___________________________________ class В { int i; public: В(int v) { i = v; } bool operator < (const B& b) { return i < b.i; } }; или приведение к типу, который может сравниваться. U class В { int i; public: В(int v) { i = operator int() }; v; } { return i;} 1.3. Специализация шаблонов Альтернативное определение шаблона, данное для некоторого подмножества наборов его аргументов, называется специализацией. Вернемся к шаблону функции min, который мы уже рассмат- ривали. V_________________________ template cclass Т> Т min (Т х, Т у) { return х < у ? х : у; }
Стандартная библиотека шаблонов 149 Очевидно, что он не пригоден для сравнения строк. Если аргу- ментом шаблона является char*, нужна специальная реализация, иными словами, специализация шаблона. U_______________________________________________________ tempiatе<> char* min<char*> (char* x, char* y) { return strcmp(x, y) -- -1 ? x : y; } Такая специализация, в которой все параметры заменены конкретными значениями, называется полной. Стандарт языка предусматривает и частичную специализацию, но она поддержи- вается не всеми реализациями и здесь не рассматривается. 1.4. Выведение аргументов шаблона функции Для шаблонов функций не нужно задавать аргументы шабло- на, компилятор сам выводит их по фактическим параметрам, пе- редаваемым при вызове. Пример. Имеется контейнерный класс Buffer, в котором фун- кция lookup производит поиск. V _____________________________________________________ template cclass Т, int i> class Buffer { / / ... }; template <class T, int i> int lookup (BuffercT, i>* buf, T a); void main() { Buffer <int, 5> b; lookup (&b, 3) ; } Для выведения аргументов шаблона компилятор сопоставля- ет типы параметров функции с типами аргументов, переданных при вызове:
150 Часть 3 а) тип параметра Buffer <Т, i>* — тип аргумента Buffer<int, 5>*\ б) тип параметра Т — тип аргумента int. Можно сказать, что компилятор решает систему уравнений: a) Buffer <Т, i>* = Buffer<int, 5>; б) Т « int; относительно неизвестных параметров Т и /, и в случае единствен- ного решения самостоятельно определяет тип параметра шаблона. В данном примере решением системы является Т — int, i = 5. Выведение аргументов избавляет от необходимости явно за- давать аргументы шаблона при вызове функции и________________________________________________________ [ lookup"с int ,5> (&b, 3 ) ;____________________________2] Обобщенную функцию всегда можно вызвать, явно задав аргу- менты шаблона. Иногда это необходимо, поскольку из аргументов функции нельзя, например, вывести тип возвращаемого значения, и _________________________ template cclass Т, int i, class R> R lookup (BuffercT, i>* buf, T a); void main{) { Buffereint, 5> b; lookup cint, 5, int> (&b, 3); } 1.5. Обобщенные классы Класс, как и функция, может быть обобщенным. Так класс В из предыдущего примера легко обобщается для хранения значений любых типов. и_____________________________________________________ template cclass Т> class D { Т i; public: D (T v) { i = v; } ; operator T() { return i; } }; cout cc mine Dcint> >(Dcint>(5), Dcint>(6));
Стандартная библиотека шаблонов 151 Чтобы сделать выражения менее громоздкими, можно вос- пользоваться оператором typedef, U______________________________________________________ typedef D<int> DI; cout cc mincDI>(DI(5), DI(6)); Обобщенные классы часто применяют для определения кон- тейнеров. Пример. Обобщенный класс «Рекурсивный список». ________________________________________________ template cclass Т> class list { Т info; list* next; public: list (T c); void add (T t); void print(); ) 7 template cclass T> Iist<T>::list (T t) { info = t; next - NULL; template cclass T> void listcT>::add(T t) // set empty element on the second place listcT> *1 - new listcT>(t); l->next = next; next -- 1 ; // change the first info with second info T x -- info; info = next~>info; next->into -• x; )
152 Часть 3 template cclass T> void list<T>::print() { std::cout << info <<“ ” if (next) next->print(); } Использование обобщенного класса «Рекурсивный список». U___________________________________________________ list <int> lst(l); 1st.add(2); 1st.add(3); 1st.print(); или list <char> 1st(“a"); Обобщенный класс, как и обобщенная функция, может иметь более одного параметра — типа данных. Замечание. Члены обобщенных классов сами могут быть шаб- Ceb лонами, но не все реализации языка это поддерживают. 1.6. Методы композиции Шаблоны допускают различные приемы композиции — созда- ния новых типов данных из старых. Например, шаблоны можно применять рекурсивно. и__________________________________________________ // Шаблон списка template cclass Т > class List {/*...*/}; // Список из целых Listcint> li; // Список из списков из целых Listc Listcint> > lli; // Список из списков из списков из целых Listc Listc Listcint> > > Illi;
Стандартная библиотека шаблонов 153 Если нужны особые имена для обобщенных типов, восполь- зуйтесь наследованием. Пример. Класс LL — список из списков из элементов типа Т. U___________________________________________________ template <сlass Т > class LL: public Listc ListcT> > { }; Наследование применяют оттого, что оператор typedef не спо- собен задавать синонимы для обобщенных типов, а наследование не влечет дополнительных расходов времени или памяти. Благодаря наследованию можно частично задавать аргумен- ты шаблона в подклассе. и_______________________________________________________ template cclass U, class V > class Base { /* ... */ }; template cclass U > class Derived: public Base cU, int> { }; В частности, можно снабдить базовый класс информацией о производном классе и template cclass Т> class Base { / / . . . }; class Derived: public Basec Derived > { //. . . );_____________________________________________________ Пример. Пусть имеется множество обобщенных контейне- ров, каждый из которых получает тип элемента в параметре шаб- лона. Существуют групповые операции, выполняемые над контей- нером в целом, например, вывести в поток все элементы, сравнить содержимое двух однотипных контейнеров, присоединить к одно- му контейнеру содержимое другого. Чтобы отделить реализацию групповых операций от кода кон- тейнеров, объявим общего предка всех контейнеров, класс BasicCont, в котором реализуем групповые операции. В этом клас- се не будет данных, а только функции.
154 Часть 3 Для получения доступах содержимому контейнеров-потомков из методов BasicCont, передадим тип контейнера-потомка в пара- метре шаблона в класс BasicCont. U_______________________________________________________ template <class С> // class С - тип потомка class BasicCont { public: // групповая операция сравнения bool operator = ~ (С& Ь) { for (int i = 0; i < 50; i++) { if (((C&)(*this))[i] b[iJ) return false; ) return true; } ; }; Чтобы построения не казались абстрактными, приведем при- мер контейнера-потомка на основе массива. и________________________________ template <class Т> class ArrayCont: public BasicCont <Contl> { T m[100]; public: T& operator [](int i) { return m[i]; } }; 1.7. Отношения порядка и функциональные объекты Предположим, что у нас есть функция, _J________________________________ template <class Т> void sort(Т* begin, T* end) {...} которая сортирует любой участок массива из элементов типа Т, и мы хотим сделать ее независимой от критерия упорядочения. Для этого введем третий аргумент функции — критерий. V________________________________________________________ |~ template <class Т, class Pred> ~]
Стандартная библиотека шаблонов 155 । void sort (Т* begin, Т* end, Predpj I. ♦ »У Критерием упорядоченности может служить функция, кото- рая получает два аргумента типа Т и возвращает истину, если пер- вый аргумент «меньше» второго, и ложь — в противном случае, и________________________________________________________ template <class Т> bool It(const T* a, const T* b) return *a < *b; } Теперь отсортировать массив m можно так u int m[] - {3,4,1,2,6,5,71; sort(m, m + 7, lt<int>); Более удобно задавать параметры в виде объектов, а не указа- телей на функции. Объекты можно заставить хранить состояние, которое впоследствии можно будет как-то использовать, например, для анализа проделанной ими работы. Если так, то функцию, про- веряющую упорядоченность, придется сделать методом класса, и_________ template cclass Т> struct Lt { bool It(const T* a, const T* b) f return *a < *b; } 1; Сортировать теперь будем так u_________________' i sort (ng m + 7 , Lt<int> () ) ; Синтаксис вызова методаp.lt( ) отличается от вызова функ- ции р( ). Чтобы сделать их одинаковыми и позволить применять в качестве критерия упорядоченности и функции, и объекты, в предикате перегружают операцию вызова функции.
156 Часть 3 Оператор вызова и функциональные объекты Если в классе А определен оператор вызова тип operator () (параметры);, то объекты классаА допускают следующий синтаксис а(аргументы);, а сам класс называется функциональным классом или функтором. Пример. Критерий сравнения в виде функтора. U___________________________________________________5 template cclass Т> struct Lt { bool operator ()(const T* a, const T* b) { return *a < *b; } }; 1.8. Задачи Шаблон функции для обмена аргументов Определите шаблон функции, которая меняет местами значе- ния двух своих аргументов. Шаблоны классов «Точка и Линия» Создайте класс «Точка в n-мерном пространстве». Тип и коли- чество чисел, из которых составлена точка, должны быть парамет- рами шаблона. Унаследуйте от точки класс Line — отрезок прямой. Второй точкой класс Line владеет. Шаблон класса «Упорядоченное бинарное дерево» Определите шаблон для класса, реализующего упорядоченное би- нарное дерево с операциями добавления, поиска и удаления элемента. Функция сортировки Определите функцию сортировки массива со следующим про- тотипом: template <class Т, class Pred> void sort(T* begin, T* end, Pred p);.
Стандартная библиотека шаблонов -— 2. Контейнеры и итераторы 2.1. Структура библиотеки Библиотека стандартных шаблонов (STL) содержит классы, Ре’ ализующие наиболее популярные структуры данных и алгоритмы работы с ними. Поскольку библиотека основана на шаблонах, структуры и алгоритмы, находящиеся в ней, применимы почти ко всем типам данных. Ядро библиотеки образуют: • контейнеры — объекты, предназначенные для хранения дру- гих объектов; • итераторы — объекты, играющие для контейнеов ту же роль, что указатели для массивов; • алгоритмы — функции для работы с содержимым контейнеров. Перечислим основные контейнерные классы вместе с заголо- вочными файлами, в которых они определены. Класс Описание Заголовок bitset множество битов <bitset> deque двусторонняя очередь (double ended-queue) <deque> list двунаправленный список <list> map ассоциативный массив: один ключ — одно значение <map> multimap ассоциативный массив: один ключ — много значений <map> multiset множество не обязательно уникальных элементов <set> priorty_queue очередь с приоритетом <queue> queue очередь <queue> set множество уникальных элементов <set> stack стек <stack> vector вектор <vector> При помещении данных в контейнер и при получении данных из контейнера происходит их копирование (как при передаче аргу- ментов и возврате значения функции). Если копирование не жела- тельно, в контейнер надо помещать указатели. Это же необходимо и для полиморфной работы с объектами.
158 Часть 3 2.2. Последовательные контейнеры Это вектор, двусторонняя очередь и список. У них различная внутренняя организация и почти одинаковый программный интер- фейс. Именно внутренняя организация определяет их отличия и об- ласти применения. Организация вектора По стандарту библиотеки вектор должен быть представлен не- прерывным участком памяти. Организация двусторонней очереди Очередь должна занимать непрерывную область памяти и при- растать порциями с обеих сторон. Организация списка Элементы списка располагаются в памяти произвольным образом.
Стандартная библиотека шаблонов 159 Сравнение последовательных контейнеров Функция-член vector deque list push_back — добавление в конец 0(1) 0(1) 0(1) pop back — удаление с конца 0(1) 0(1) 0(1) push_front — добавление в начало нет 0(1) 0(1) pop__front — удаление с начала нет 0(1) 0(1) insert — вставка в любом месте О(п) О(П) 0(1) erase — удаление в любом месте О(п) О(п) 0(1) sort — метод нет нет есть sort — обобщенный алгоритм есть есть нет 0(1) — постоянное время выполнения операции; 0(п) — линейное время выполнения операции. 2.3. Контейнер vector Рассмотрим последовательные контейнеры стандартной биб- лиотеки на примере контейнера vector. Вектор, как и другие пос- ледовательные контейнеры, имеет два параметра шаблона: а) тип хранимых элементов; б) тип аллокатора (параметр по умолчанию). Замечание. Аллокатор — это объект, отвечающий за распре- деление памяти для элементов контейнера. Обычно при ра- боте с контейнерами не задают свой аллокатор, а используют тот, что задан по умолчанию. Создание контейнеров Создать можно пустой контейнер ___U____________________________ |" vector <char> vecl; контейнер, заполненный каким-то одним значением, U_____________________________________________________ vector <char> vec2(10); // заполнен символами ’\0’ vector <char> vec3(10,’а 1); // заполнен символами 'а' контейнер, состоящий из элементов уже существующего контейнера,
160 Часть 3 u_______________________________________________________ | vector <char> vec4 - vec~3; | контейнер, состоящий из части элементов существующего кон- тейнера, U_______________________________________________________ char М[] - "abcdef"; vector <char> vec5(M, M + 3); // "abc" В последнем случае параметрами конструктора служат указа- тели на первый элемент и место в памяти сразу за последним эле- ментом. Пример работы с вектором из целых чисел. и_______________________________________________ #include <iostream> #include <vector> using namespace std; void main() { // определить пустой вектор из целых чисел vector <int> iv; // добавить в него числа от 1 доЮ for (int i = 1; i < 11; i + +) iv. push__back (i) ; // вывести на экран размер вектора cout « iv.size() << endl; // распечатать содержимое вектора for (i - 0; i < iv.size(); i++) cout << iv[i] « ” "; } 2.4. Итераторы Итератор — это объект, который обеспечивает доступ к эле- ментам контейнера. Можно рассматривать итератор как обобщение
Стандартная библиотека шаблонов 161 понятия «указатель на элемент». Если элемент имеет тип Т, то тип итератора — Г*. Замечание. Массивы можно рассматривать как встроенные контейнеры языков С и C++. Указатель на элемент массива служит итератором для массива. Указатели могут указывать на переменные и на постоянные значения. Итераторы тоже бывают переменные и постоянные. Кроме того, итераторы делятся на однонаправленные (прямой и об- ратный), двунаправленные и произвольного доступа. Еще итера- торы бывают входные (только для чтения элементов) и выходные (только для записи элементов). Мощность, т.е. множество допустимых операций, разных ка- тегорий итераторов характеризуется неравенством: прямой == обратный < двунаправленный < произвольного доступа. Для итераторов разных категорий определены следующие операции: • для однонаправленных — *1++I, I++, ; • для двунаправленных — еще и -I,1— ; • для произвольного доступа — еще и /+/?, I-n, I +=, -= . Внутри последовательного контейнера объявлены четыре си- нонима для типов итераторов:
162 Часть 3 Типы ТО и Т1 зависят от реализации и определены за предела- ми контейнера. Именно они определяют мощность итератора. Ите- раторы контейнеров vector и deque относятся к категории прямого доступа. Итераторы контейнера list относятся к категории двунап- равленных. Пример. Распечатать содержимое вектора при помощи ите- ратора. U_______________________________ for (vector<char>::iterator i - iv.beginf); i != iv.endf); i++) cout << *i; Поскольку итератор вектора относится к категории прямого доступа, для него определена операция индексации и vector <char>::iterator m = iv.beginj); m[0j; // первый элемент m[l]; // второй элемент m = iv.end(); m[-l]; // последний элемент Итератор списка — двунаправленный, и с ним такого делать нельзя. 2.5. Обзор методов вектора Для сокращения описания функций будем использовать сле- дующие обозначения для параметров: л — размер — целое положительное число (тип size_typey\ х — элемент вектора (тип value_type)-, first, last, it — значения итератора (тип iterator, const iterator, ...). Конструкторы • vector( ) — создает пустой вектор; • vector(п) — создает вектор из п элементов. Используется кон- структор элемента по умолчанию; • vectorf п, х) — создает вектор из п элементов. Используется конструктор копирования элемента;
Стандартная библиотека шаблонов 163 • vector( first, last) — создает вектор из последовательности эле- ментов. Последовательность задается парой итераторов; • vector^ v) — создает вектор путем копирования другого вектора. Получение итераторов • iterator beginf ) — итератор на первый элемент; • i tera tor end () — итератор элемента, « следующего за последним »; • reverse iterator rbeginf ) — начальный обратный итератор; • reverse iterator rend( ) — конечный обратный итератор. Так же получаются и константные итераторы. Доступ к элементам • reference front ( ) — ссылка на первый элемент вектора; • reference back( ) — ссылка на последний элемент вектора; • [] оператор индексации; • reference at(n) — доступ к элементу с проверкой выхода за гра- ницу вектора. Добавление, удаление элементов • void push backf х) — добавить элемент в конец вектора; • voidpop backf ) — убрать элемент с конца вектора; • iterator insert(it) — вставить элемент по умолчанию перед по- зицией it; • iterator insert(it, х) — вставить элемент х перед позицией it; • void insert (it, n,x) — вставить n элементов x перед позицией it; • void insert(it, first, last) — вставить последовательность эле- ментов перед позицией it; • iterator erasef it) — удалить из вектора элемент в позиции it; • iterator erase( first, last) — удалить из вектора последователь- ность элементов; • void clear() — очистить вектор путем удаления всех его эле- ментов. Присваивания • = — умалчиваемый оператор присваивания; • void assignf п, х) — заменяет текущий вектор последователь- ностью из п элементов х; • void assignf first, last) — заменяет текущий вектор последова- тельностью элементов.
164 Часть 3 Прочие операции ♦ sizejtype size() — количество элементов вектора; • sizetype max_size() — максимальное количество элементов вектора в данной реализации; • bool empty ( ) — проверяет, пуст ли вектор; • size_type capacity() — емкость вектора ( >= size( ) ); • reservef п) — увеличить емкость вектора; • resizef п) — устанавливает новый размер вектора в п элементов. Новые элементы пустые; • resize( п, х) — устанавливает размер вектора в п элементов. Новые элементы равны х. Операции над векторами в целом • swap(о) — меняет местами два вектора: *this и и; • ==, /=, < — операции сравнения векторов (не члены класса). Пример работы с вектором из целых чисел. U__________________________________________________ vector <char> V; // Добавить элементы V .push_back("d"); V .push_back("b"); V .push_back("a"); // Отсортировать sort(V.begin(), V.end()); // Очистить V .clear(); Пример работы с вектором из объектов Complex. U__________________________________________________ // Определить пустой вектор из комплексных чисел vector <Complex> ic; // Добавить в него числа от 1 до 10 for (int i ~ 1; i < 11; i++) ic.push-back(Complex(i, i));
Стандартная библиотека шаблонов 165 // Вывести на экран размер вектора cout << ic.size() << endl; // Распечатать содержимое вектора for (i ~ 0; i < ic.sizeO; i++) cout << ic [ i]; Чтобы приведенный пример работал, в классе Complex долж- на быть определена операция помещения в поток. 2.6. Функции контейнеров deque и list Добавление и удаление элементов Для очередей и списков определены операции: • void push-front (х) — добавить элемент х в начало контейнера; • void pop front ( ) — убрать элемент х из начала контейнера. Только списки реализуют операции: • void splicef it, I) — переносит другой список I в *this в позицию перед it-, • void splicef it, I, first) — переносит первый элемент списка I в *this в позицию перед it', • void splicef it, I, first, last) — переносит часть списка l в *this в позицию перед it-, • uniquef) — из двух соседних одинаковых элементов один уничтожает. В результате операций splice элементы исчезают из одного списка и появляются в другом. Доступ к элементам Для списков не определена операция индексации [] и функ- ция atf ). Прочие операции Для списков и очередей не имеют смысла функции capacity( ) и reserve f ).
166 Часть 3 2.7. Адаптеры контейнеров Адаптер контейнера предоставляет ограниченный интерфейс к контейнеру. Такие классы, как стек (stack) и очередь (queue), оп- ределены не как отдельные контейнеры, а как адаптеры к базовым последовательным контейнерам: vector, list, deque. Программист может выбрать любой из них для своего стека или очереди. Базовым контейнером по умолчанию для стека и очереди яв- ляется дек и_______________________________________________________ template <class Т, class Cont - deque<T> > class stack {...} template <class T, class Cont - deque<T> > class queue {...} 2.8. Задачи Разность векторов Даны два вектора. Удалите из первого вектора все элементы, которые содержаться во втором. Палиндром Проверьте, является ли данный вектор палиндромом. Слияние векторов Слейте два упорядоченных вектора в третий, тоже упорядо- ченный. Два способа задать граф Граф задан в виде матрицы смежности. Сохраните его в виде списков инциденций, воспользовавшись для этого векторами. Для графа из N вершин матрица смежности представляет со- бой массив int G[N][N],b котором G[i][j] = 1, если из вершины I ведет дуга в вершину j nG[i][j] = 0, если дуги нет. Список инциденций представляет собой N последовательнос- тей, таких, что в д-й последовательности содержатся номера всех вершин, в которые из д-й вершины ведут дуги. Очередь из двух стеков Организуйте очередь, используя для этой цели ровно два стека.
Стандартная библиотека шаблонов 167 3. Ассоциативные контейнеры. Строки Ассоциативных контейнеров четыре: set, multiset, тар, multimap. Данные, помещаемые в ассоциативный контейнер, име- ют ключ, причем, кроме ключа, других данных может не быть (set, multiset). В контейнерах set и тар ключи уникальны, в multiset и multimap — нет. Основой для ассоциативных контейнеров служат сбалансиро- ванные деревья поиска. В деревьях поиска каждый узел больше любого узла из своего левого поддерева, но меньше любого узла из своего правого подде- рева. В сбалансированных деревьях ветви не слишком отличаются по длине. 3.1. Ассоциативные массивы Класс тар является контейнером, в котором каждьш элемент, помимо значения, имеет уникальный ключ (контейнер с неуни- кальными ключами поддерживается классом multimap). Ассоциативный массив, который также называют словарем, напоминает массив с произвольным типом индекса. Единственное требование к индексу (ключу) — его значения должны допускать упорядочение. Благодаря древовидному представлению словаря поиск значе- ния по ключу выполняется сравнительно быстро — за время O(log п).
168 Часть 3 В шаблоне словаря два обязательных параметра — тип клю- чей и тип значений и два необязательных — предикат сравнения и аллокатор. U______________________________________________________ template <class Key, class T, class Pred - less<Key>, class A ~ allocator<T> > class map {...} less и allocator — шаблоны классов из стандартной библиотеки. Предикат сравнения Предикат сравнения позволяет задать внешний критерий для сравнения ключей. По умолчанию это шаблон-функтор less — мень- ше. и______________________________________________________ template cclass Т> struct less : public binary_functioncT, T, bool> { bool operator()(const T& xz const T& y) const { return x < y; } }; Как видно из этого определения, предикат по умолчанию тре- бует, чтобы тип ключа поддерживал операцию «меньше». Пользо- вательский предикат может не требовать и этого. Пример. Пусть мы хотим сделать ключом С-строку и определя- ем специальный функциональный класс str lt для сравнения строк, и_________________________________________________________ struct str_.lt { bool operator()(const char* x, const char* y) const { return strcmp(x, y) < 0; } };
Стандартная библиотека шаблонов 169 // Объявляем словарь map <char*, char*, str lt> т; Внутренние типы Внутри шаблона тар объявлены следующие типы: и__________________________________________________ typedef Key key_type; // тип ключа typedef Т mapped__type; // тип значения typedef pair<const Key, Т> value type // пара Индексация Благодаря перегруженной операции индексации mapped_type& operatorf] (const key_type&); работа co словарем похожа на работу с массивом. Оператор индексации выполняет поиск по ключу, заданному в качестве индекса, и возвращает соответствующее значение. Важ- но помнить, что если ключ не найден, в словарь вставляется новый элемент с заданным ключом и значением по умолчанию типа niapped_type. Пример. Словарь, где содержатся фамилии и идентифика- ционные коды. U ______________________________________________________ map <char*, char*, str_.lt > т; т 1234567890" ] = "Ivanov”; т [’’1234567891" ] = "Petrov"; // одинаковые значения возможны т[ "1234567892"] = "Ivanov”; // новое значение заменяет старое m ["1234567890"] = "Sidorov"; Для проверки того, что для заданного ключа словарь не содер- жит значения, не стоит применять операцию индексации, лучше воспользоваться функцией-членом find( ). и________________________________________________________ // В словаре может появиться новый элемент cout « m [" 1234567893"];
170 Часть 3 // Так делать безопаснее if (m.find("1234567894") m.end()) cout « "такого ключа нет"; Пары В отличие от вектора, контейнер тар хранит не элементарные значения, а пары <ключ, значением Для таких пар в STL имеется специальный шаблон. U________________________________________ template<class Т, class U> struct pair { T first; U second; }; В элементе словаря ключ является первым членом пары, а зна- чение — вторым. Итераторы Как и в ранее рассмотренных контейнерах, фактические типы итераторов определяются реализацией, а реверсивные ите- раторы конструируются из них стандартным образом. Итератор для ассоциативного массива предоставляет пары в порядке воз- растания их ключей. Пример. Распечатать все идентификационные коды Ивано- вых, сохраняемые в словаре /и. U______________________________________________________ map <char*, char*, str_lt>::const—iterator p - m.begin(); while (p !- m.endO) { if (strcrnp(p->second, «Ivanov»} =- 0) { cout <<. p->first << endl; ) P++; }
Стандартная библиотека шаблонов 171 Замечание. В условии повторения цикла нельзя писать ^3 Р < m.end(), т.к. итератор словаря двунаправленный, а не произвольный, как у вектора, и не поддерживает операцию «меньше». 3.2 * Операции с ассоциативными массивами Для сокращения описания функций будем использовать сле- дующие обозначения для параметров: • key — ключ (тип key _type); • р — пара — элемент словаря (тип value-type); • first, last, it—значения итератора (тип iterator, const-iterator,...). Предоставление информации по ключу Основное назначение словарей — предоставлять информацию по ключу, и этой цели служат функции: • iterator find(key ) — находит итератор на элемент с ключом key; • size-type count( key) — определяет число элементов с ключом key; • iterator lower_bound(key) — находит нижнюю границу мест в словаре, на которых может стоять элемент с ключом key так, чтоб не нарушать словарного порядка; • iterator upper_bound( key) — находит верхнюю границу мест в словаре, на которых может стоять элемент с ключом key так, чтоб не нарушать словарного порядка; • pair<iteratbr, iterator> equal_range(key ) — находит обе грани- цы диапазона элементов с ключом key. Если элемента с ключом key не существует, функция lower bound() возвращает итератор на первый элемент с ключом большим, чем key, а если и такого нет, то возвращает итератор end( ). Этот же способ сообщения об отсутствии ключа использу- ется функциями upperboundf ) и equal_range(). Вставка и удаление элементов • pair<iterator, bool> insert(p) — вставляет пару р и возвраща- ет ее итератор и признак успешности операции. Если новый ключ является дубликатом уже имеющегося, вставки не про- исходит и возвращается итератор на пару с уже имеющимся в словаре ключом;
172 Часть 3 • iterator insertfit, р) — вставляет пару р и возвращает ите- ратор на вставленный элемент. Параметр it — только под- сказка контейнеру, с какой позиции искать место для но- вой пары; • void insertf first, last) — вставляет последовательность пар из другого словаря; • iterator erase(it) — удаляет пару, на которую показывает ите- ратор it, и возвращает позицию элемента, следующего за уда- ленным (может быть, возвращает end() ); • iterator erasef first, last) — удаляет последовательность пар; • size_Jype erasef key) — удаляет все элементы с ключом key и воз- вращает количество таких элементов; • void clear( ) — очищает словарь путем удаления всех его эле- ментов. Другие функции Подобно вектору словарь имеет функции: size( ), maxsizef ), empty ( ), swap( ) и операции сравнения словарей. 3.3 . Множества и множества с дубликатами Множества можно рассматривать как ассоциативные масси- вы, в которых есть только ключи, а значения отсутствуют. Пара- метрами шаблонов set и multiset являются: тип хранимых элемен- тов, предикат сравнения и аллокатор. Два последних параметра не обязательны. В отличие от ассоциативного массива, внутренний тип множе- ства value_type означает то же, что и key_type. Множество не имеет оператора индексации. Пример. Создать разные множества, задавая различные пре- дикаты сравнения. и________________________________________________________ set<int> si setcint, less<int> > s2; // то же, что ser<int> si; setcint, greater<int> > s3; Пример. Создать множество из С-строк. Проверить, входит ли данная строка во множество.
Стандартная библиотека шаблонов 173 и_____________________________________ set <char*, str_lt> s; char x[] - "abc", char у [] = "abc"; s.insert("def"); s.insert(x); s.insert{y}; В построенном таким образом множестве s два элемента: «а&с» и «def». Проверим, входит ли в него строка «def». и___________________________________________________ | cout «(s.find("def") 1- s.endQ); | Все сравнения элементов множества выполняются при помощи предиката сравнения, заданного в параметре шаблона. Например, и X у эквивалентно ! (х < У 1 1 У < X) х ! ~ у эквивалентно (х < У 1 1 У < X) х >= у эквивалентно ! (х < У) и т.д. 3.4. Строки Символы Строка является последовательностью символов и может рас- сматриваться как специализированный контейнер символов. В ка- честве символов могут выступать объекты различных классов. Что- бы работа со строками была эффективной, этим классам запрещено перекрывать копирующий конструктор, деструктор или оператор присваивания. Особенности произвольного символьного класса х задаются в клас- се char_traits<x>^ который является специализацией шаблона. U_________________________________________________________ struct char__trai ts<E> { /* операции присваивания, сравнения, поиска, преобразования */ }; Тип, используемый в качестве символа строки, должен сопро- вождаться соответствующей специализацией шаблона chartraits.
174 Часть 3 Замечание. Шаблон char traits вообще не имеет свойств, их имеют только специализации: char traits<char>, сЛаг_гг<Шз<«?сЛаг_/> и т.п. Шаблон basic_string Шаблон basic_string обеспечивает все стандартные возможно- сти работы со строками. Библиотечные классы string и wstring яв- ляются простой специализацией шаблона. V________________________________ typedef basic_string<char> string; typedef basic„string<wchar„t> wstring; Тип string поддерживает ASCII-строки, а тип wstring — стро- ки 16-разряд пых символов. Замечание. Внутри последовательности string может быть сколько угодно символов с кодом 0. Завершающий ноль не обязателен. Класс basic string не имеет виртуальных функций и не пред- назначен для наследования. При желании создать собственный строковый класс его надо использовать в качестве члена, а не базо- вого класса. Внутренние типы Подобно другим контейнерам basic _string объявляет внутрен- ние типы: • valuetype — тип хранимых символов; • sizetype — беззнаковый целый тип для выражения размера; • difference jype — целый тип для выражения расстояния меж- ду символами; • pointer, const jpointer — указатели на символ; • reference, const reference — ссылки на символ; • iterator, const iterator — итераторы, определенные в реа- лизации; • reverse iterator, const jreverse-iterator — обратные итераторы.
Стандартная библиотека шаблонов 175 Конструкторы Класс имеет несколько конструкторов, которые мы для яснос- ти опишем в терминах спецализации string. • string( ) — создается пустая строка; • string(п, с) — создается строка, состоящая из п символов с, «сссссс...сссс»; • stringfconst char* с) — строка создается из С-строки; • stringfconst char* с,п) — строка создается из первых п симво- лов С-строки; • string(const string& s) — копирующий конструктор. Строка создается из другой строки; • stringf const string& s,pos, n) — строка создается из части дру- гой строки (п символов, начиная с pos); • string(first, last) — создается строка из последовательности, заданной парой итераторов. Почти все конструкторы имеют еще один параметр по умолча- нию — аллокатор. Получение итераторов Итераторы получаются методами, присущими всем контей- нерам: • iterator begin( ); • iterator end( ); • reverseiterator rbeginf ); • re verse _it er at or rend( ). Так же получаются и константные итераторы. Доступ к элементам • operator [] — без проверки диапазона; • reference at(n) ----- с проверкой диапазона и генерацией исклю- чения out_ofrange. Операции и функции Благодаря тому, что string, в отличие от строк языка С, явля- ется полноценным типом данных, он поддерживает такие опера- ции над строками как: • присваивание: ==, += ;
176 Часть 3 • сравнения: ==, !=, <, >, >=, <= ; • ввод-вывод: », « . Многие из них перегружены, например, операция += может при- соединять к строке другую строку, один символ или массив символов. Наиболее часто используются следующие функции-члены: • size_1уре lengthf ) — возвращает количество символов в строке; • s^ze^fyPe size( ) — синоним для lengthf ), в нем нуждаются стан- дартные алгоритмы; • sizetype find(str, pos = 0) — находит первое вхождение под- строки str, а поиск начинает с позиции pos; • basic ^string subst г (pos = 0, n = npos ) — вырезает часть строки с по- зиции pos размером в п символов. Внутренняя константа npos по умолчанию инициализируется самым большим положительным числом без знака, что в знаковом типе выглядит как -1; • basic_string& insertf sizejtype pos, const E*s) — вставляет мас- сив символов; • basic string& replacefsize type pos, sizejtype n, const E* s) — замещает одну подстроку другой; • iterator erase(first, last) — удаляет подпоследовательность из- строки; • const Е* c_str( ) const — преобразует строку в С-строку. Если в составе строки были нулевые символы, то С-строка будет ог- раничена самым левым из них; • int compare(str) — сравнивает строку с другой строкой. Выра- батывает такой же результат, как strcmpf ) из <cstring>. Почти все эти функции перегружены. Пример работы со строками. U______________________________________________________ // определить пустую строку string s; // извлечь строку из потока cin » s; // поместить в поток три первых символа строки cout << s.substr(0, 3); // поместить в поток три последних символа строки int i - s.size(); cout << s.substr(i - 3);
Стандартная библиотека шаблонов 177 // удалить 2 символа, начиная с 4 (нумерация с 0) s.erase(4 , 2); /; вставить строку "ООО” после 4 символа s.insert(4, "ООО"); 3.5. Задачи Класс «Телефонный справочник» Разработайте класс «Телефонный справочник», который бы содержал произвольное количество абонентов. Для каждого або- нента известна фамилия и один номер телефона. Фамилии разных абонентов могут быть одинаковыми, номера телефонов — нет. Спра- вочник должен выполнять поиск номеров телефонов по фамилии и поиск фамилии по номеру телефона. Класс «Быстрый телефонный справочник» Разработайте класс «Быстрый телефонный справочник», ко- торый бы выполнял поиск телефона по фамилии и поиск фамилий по номеру телефона за время O(log п). Для каждого абонента известна фамилия и один номер теле- фона. Фамилии разных абонентов могут быть одинаковыми, номе- ра телефонов — нет. Частотный словарь текста Составьте частотный словарь для заданного текста. Различные написания одного слова (прописными, строчными буквами, первая буква прописная и т.п.) считайте одинаковыми. Переносов слов в тексте нет. 4. Последовательные алгоритмы Стандартная библиотека содержит алгоритмы, необходимые для выполнения различных операций с контейнерами. Алгорит- мы реализованы в виде шаблонов функций. Для придания алгорит- мам большей гибкости применяют функциональные объекты, на- пример, алгоритм сортировки принимает функциональный объект в качестве критерия упорядоченности.
178 Часть 3 Алгоритмы могут работать с самыми разными последователь- ностями, доступ к которым получают через итераторы-аргументы. Если алгоритм возвращает итератор, его тип такой же, как у аргу- ментов. В частности, аргументами алгоритма определяется, возвра- щает он простой или константный итератор. Алгоритмы не выполняют проверки диапазона на их входе и выходе, программист должен делать это сам. Все стандартные алгоритмы находятся в пространстве имен std, а их объявления — в заголовочном файле <algorithm>. Стан- дартные функциональные объекты также находятся в пространстве имен std, а их объявления помещены в <functional>. 4.1. Немодифицирующие последовательные алгоритмы Перечень алгоритмов • foreachf ) — выполняет операцию для каждого элемента пос- ледовательности ; • find() — находит первое вхождение значения в последова- тельность; • find _Jf( ) — находит первый элемент последовательности, ко- торый удовлетворяет условию; • flnd_firstof( ) — находит значение из одной последовательно- сти в другой; • adjacent_Jind( ) — находит пару соседних значений, которые равны или удовлетворяют условию; • count() — считает количество вхождений данного значения в последовательность; • count_Jf( ) — считает количество выполненш! данного условия для элементов последовательности; • mismatchf) — находит первый элемент, в котором две после- довательности отличаются; • equal() — сравнивает две последовательности путем попарно- го сравнения элементов; • searchf ) — находит первое вхождение подпоследовательности в последовательность; • find_end( ) — находит последнее вхождение подпоследователь- ности в последовательность; ♦ searchjn() — находит п-ое вхождение значения в последо- вательность.
Стандартная библиотека шаблонов 179 Перейдем к детальному рассмотрению алгоритмов. При этом бу- дем пользоваться «значащими» названиями для параметров шаблона: • In — Input Iterator, входной итератор; • Out — Output Iterator, выходной итератор; • For — Forward Iterator, однонаправленный итератор; • Fun — Function, функционал или функция; • Pred — Predicate, предикат; • BinPred — Binary Predicate, бинарный предикат; • BinOp — Binary Operation, бинарная операция; • T — Type, произвольный тип. Алгоритм for_each Достоинство библиотечных алгоритмов в том, что они избав- ляют программу от разнообразных циклов. Именно это и делает алгоритм foreach. U______________________________________________________ templatecclass In, class Fun> Fun for_each(In first, In last, Fun f) { while(first last) f(*first++); return f; } Пример. Распечатать в столбик все символы строки. U // функция печати void f(cnar с) { cout << с << endl; } void ir.ain() { string s (" 123 4 5 " ) ; for_each(s.begin(), } s.end()f t) ; Алгоритм for_each( ) возвращает функцию или функциональ- ный объект, переданный ей в качестве третьего аргумента. Это ис- пользуется, чтобы передать данные, собранные функциональным объектом, в вызывающую функцию.
180 Часть 3 Пример. Задан вектор из символов. Подсчитать сумму ко- дов всех символов. и_______________________________________________________ // Функционал для подсчета суммы кодов символов struct Sum { int sum; Sum() : sum(0) { } void operator () (char c) { sum += c; } }; void main() { // вектор из 10 букв "x" vector <char> v(”x", 10); Sum s = for^each(v.begin(), v.end(), Sum()); cout « s.sum « endl; Алгоритмы find Алгоритмы find просматривают последовательность в поисках элемента, который имеет заданное значение или удовлетворяет за- данному условию. Функция find() возвращает итератор на первый элемент с за- данным значением. U_______________________________________________________ template<class In, class Т> Init find(In first, In last, const T& val);___________ Пример. Найти заданный символ в массиве символов. Ц___________________________________________________ char s[] = ”12345”; char* р = find(s, s + 5, ”3”); Функция find jiff) возвращает итератор на первый элемент, удовлетворяющий условию. V_______________________________________________________ template<class In, class Pred> Init find—if(In first, In last, Pred p);
Стандартная библиотека шаблонов 181 Для задания логических условий в стандартной библиотеке используются функции или функциональные объекты. Пример. Найти в С-строке первый пробельный символ. U__________________________________________________ bool f(char с) { return isspace(c); j char s[] = " 01234\t6; char* p - find„if(s, s + 5,f); Замечание. В стандартном заголовке <cctype> объявлен ряд функций, классифицирующих символы. Это isalpha(), isapper( ), islower( ), isspacef ) и другие. Алгоритм find_Jirst_of() находит в последовательности пер- вый элемент, который имеется и в другой последовательности. U_______________________________________________________ templatecclass Fori, class For2> Fwdltl f ind_f irst_of (Fori firstl, Fori lasti, For2 first2, For2 last2); templatecclass Fori, class For2 , class Pred> Fwdltl find_first„of(Fori firstl, Fori lasti, For2 first2, For2 last2, BinPred pr); Второй из алгоритмов find_Jirst_of получает в качестве пара- метра бинарный предикат, а не унарный, как findjf. Пример. Даны две строки. Найти в первой строке элемент, который был бы больше какого-нибудь элемента второй строки. U__________________________________________________________ bool f(char cl, char c2) { return cl > c2; } char si И - "012346"; char s2[] = "7734888”; char* p = find„first of(si, sl + 6, s2, s2+7, f);
182 Часть 3 В семействе есть и алгоритм find_Jir$t_jiot_of(). Он находит в последовательности первый элемент, которого нет в другой после- довательности. Алгоритмы count Алгоритмы count() и count_if() считают число вхождений в последовательность некоторых значений. и______________________________________________________ template cclass In, class T> typename iterator__traits<In>: :difference_type count(In first, In last, const T& val); template cclass In, class T> typename iterator—traits<In>: :dif feren.ce_type count—if(In first, In last., Pred pr) ; Столь сложный тип возвращаемого значения объясняется стремлением разработчиков создать целый тип, предельная вели- чина которого равнялось бы максимальной разности между двумя значениями итератора. В то же время тип должен быть применим и к обычным массивам. Цель достигается за счет наличия в биб- лиотеке различных специализаций шаблона iterator traits. Пример. Подсчитать количество прописных букв в строке символов. U___________________________________________________ string s("аааААааА"); cout « count—if (s .begin () , s.enclO , i supper) « endl; Функция int isupper(char) определена в заголовочном файле <cctype> и служит для распознавания прописных букв. Алгоритмы equals mismatch Алгоритмы equal() (равенство) и mismatchf) (несовпадение) поэлементно сравнивают две последовательности. и( template cclass Ini, class In2> bool equal(Inl firstl, Ini last, ln2 firs-t2);
Стандартная библиотека шаблонов 183 template cclass Ini, class In2, class BinPred> bool equal(Ini firstl, Ini last, In2 first2, BinPred p); template pair<Inl, <class Ini, class In2> In2> mismatch(Ini firstl, Ini last, In2 first2); t.empidle cclass Ini, class In2, class BinPred> pairdnl, In2> mismatch (Ini firstl,Ini last,In2 first2,BinPred p); Участки двух последовательностей, подлежащие сравнению, задаются не четырьмя итераторами, а только тремя. Предполага- ется, что конец второго участка определяется автоматически, т.е. Iast2 =••= first2 + lastl ~ firstl. Пример. Определить длину общей части двух целых массивов. U____________________________________________ ЛпЛ mil] - {1,2,3,4,5,6,7}; inc m2 И - {1,2,3,8,5,6,71; cout сс mismatch (ml, ml + 7, m2) .first - ml « endl; Поиск Алгоритмы search(), search jn() и find_end() находят вхож- дение одной последовательности в другую. Первый вариант алгоритма search( ) является частным случа- ем второго, когда предикат рг означает простое равенство соответ- ствующих символов из двух последовательностей. u J___________________________________________________ tempLatecclass Fori, class For2> Fori search(Fori firstl, Fori lastl, For2 first2, For2 last2); templatecclass Fori, class For2, class BinPred> Fori search(Fori firstl, Fori lastl, ______________For2 first2, For2 last2, ВinPred pr); Пример. Найти второе вхождение в строку ее начального трехбуквенного сочетания.
184 Часть 3 u________________________________________________ char sl[] = "11122331111233"; char* p = search(si + 1, si + 15, si, si + 3) ; Алгоритм search( ) возвращает итератор на первое вхождение подпоследовательности, а алгоритм find_end( ) — на последнее. U_______________________________________________________ string si (” 01ху4 5ху89; string s2("ху"); char* р - search(si.begin(), sl.end(), s2.begin(), s2 .end() ) ; cout « p -- si. begin () << endl; // напечатает 2 p = find__end (si .begin () , sl.endO, s2.begin(), s2.end()); cout « p - si.begin() « endl; // напечатает 6 4.2. Модифицирующие последовательные алгоритмы Перечень алгоритмов • transformf) — преобразует одну или две последовательности в новую последовательность; • сору() — копирует последовательность, двигаясь от начала к концу; ♦ copy_backward( ) — копирует последовательность, двигаясь от конца к началу (применяется для перекрывающихся участков одной последовательности); • swap() — меняет местами два значения, заданные ссылками; • iter_swap() — меняет местами два элемента, заданные ите- раторами; • swap_ranges( ) — меняет местами две последовательности; • replace() — в последовательности элементов заменяет старое значение новым; • replace_if( ) — делает замену элементов при выполнении условия; • replace_copy( ) — копирует последовательность, попутно делая замены; • replace copy_tf ( ) — копирует последовательность, делая заме- ны при выполнении условия; • fdl( ) — заполняет все элементы последовательности заданным значением;
Стандартная библиотека шаблонов 185 • filljn( ) — заполняет заданным значением п элементов, начи- ная с заданного элемента; • generate^ ) — заполняет все элементы результатом заданной операции; • generate ji( ) — заполняет результатом операции п элементов, начиная с заданного; • remove() — удаляет элементы с данным значением; • reniove jff ) — удаляет элементы, удовлетворяющие условию; • гетоиесору( ) — переносит элементы с данным значением; • rernove_copy_if() — переносит элементы, удовлетворяющие условию; • uniquef ) — удаляет равные соседние элементы; • unique сору( ) — копирует последовательность и при этом уда- ляет равные соседние элементы; • reverse( ) — меняет порядок следования элементов на противо- положный; • reverse сору() — копирует последовательность в обратном порядке; • rotate — перемещает элементы циклически; • rotate_сору() — копирует элементы в циклическом порядке; • random shufflef ) — переставляет элементы случайным образом. Алгоритмы сору Реализация копирования проста и не страхует программу от выхода за границы приемника. и______________________________________________________ templetecclass In, class Out> Out copy(In first, In last, Out res) { for (; first != last; ++res, ++first) *res - *first; return res; }____________________________________________________ Пример. Скопировать одну строку внутрь другой. U__________________________________________________ string si ( "0000") , s2("123456789"); сору(si.begin(), sl.endO, s2.begin() + 3) ; // s2 станет равно "123000089"
186 Часть 3 Обратное копирование начинается с конца последовательнос- ти и требует, чтобы итераторы были двунаправленными. и_____________________________________________________ templatecclass Bi, class Bi2> Out copy„backward(Bi first, Bi last, Bi2 res); Оно применяется, когда элементы копируется из одной части последовательности в другую, эти части перекрываются, причем источник копирования находится левее приемника. —-ч—_...........................п ............. Пример. Сместить первые 5 символов строки на 3 позиции вправо. и_____________________________________________________ string si("123456789"); string::iterator end ~ sl.beginO +- 5; copy„backward(si.begin(), end, end + 3); Алгоритмы transform transform выполняет заданную операцию с каждым элементом последовательности и результат операции сохраняет в э- том элементе. и__________________________________________________ templatecclass In, class Out, class Op> Out transform(In first, In last, Out res, Op op);_____ Пример. В векторе находятся целые числа. Заменить каж- дое число его квадратом. и______________________________________________________ // Кт1э.сс - исполнитель трансформации template cclass Т> struct Sqr { Т operator() (Т х) {return х * х;} }; vector <int> v; v.push_back(1); v.push_back(2); v.push_back(3); v.push„back(4) ; transform(v.begin(), v.end(), v.begin(), Sqr<int>());
Стандартная библиотека шаблонов 187 В качестве исполнителя операции можно использовать функ- цию, а не функциональный объект, но эту функцию нельзя сделать обобщенной, т.к. шаблон функции не может быть аргументом дру- гой функции. и_____________________________________________________ ’ гjt sqrfint х) { return х * х;} transform(v.begin(), v.end(), v.begin(), sqr) ; Имеется вариант алгоритма transform, который привлекает к участию в операции элемент другой последовательности. Операция в этом варианте, естественно, бинарная. у______________________________________________________ templatecclass Ini, class In2 , class Out, class BinOp> Out transform(Ini first, Ini last, In2 first, Out res, BinOp op); Пример. Есть два целых вектора. Сложить их поэлементно, а результат поместить в третий вектор. и ______________________________________________________ int sum (int х, int у) { return х + у;} vector <int> vl, v2 , v3; vl. push__back' (1) ; vl. push-back (2 ) ; vl . push_Jback (3) ; v2.pash—back(10); v2.push_back(20); v2.push_back(30); transfо rm(vl.begin(), vl.end(), v2.begin(), back inserter (v3 ) , sum); Замечание. Для наполнения вектора v3 в примере использо- ван итератор типа back insert iterator, который при каждом присваивании добавляет в последовательность один элемент и инкрементирует свое значение. Такой итератор создает и возвращает библиотечная функция back inserter().
188 Часть 3 Алгоритмы unique Алгоритмы unique() удаляют дубликаты соседних элементов, по- этому их обычно применяют к отсортированным последовательностям. V template cclass For> For unique (For first, For last); template cclass For, class BinPred> For unique (For first, For last, BinPred p) ; Пример. Исключить из целого вектора одинаковые элементы. U__________________________________________________ vector<int> v; v.pushjback(l); v.push_back(2); v.pushjback(2); v.push_back(4); v.push_back(5); v.push_back(5); unique(v.begin(), v.endO); В результате этой операции последовательность не меняет свой размер. Чтобы физически удалить лишние элементы, надо приме- нить алгоритм erase ( ). и______________________________________________________ vector<int>::iterator р - unique(v.begin(), v.endf)); // обрезаем "мусор” v.erase (p, v.endO); Алгоритмы unique copyf ) создает копию последовательности без дубликатов. V___________________________________________________________ template cclass In, class Out> Out unique_copy (In first, In last, Out res) ; template cclass In, class Out, class BinPred> Out unique copy (In first, In last, Out res, BinPred p) ; Алгоритмы replace Эти алгоритмы следуют модели unique/unique_copy, что дает нам четыре алгоритма.
Стандартная библиотека шаблонов 189 U____________________________________________________ template cclass For, class T> void replace(For first, For last, const T& v, const T& new_v); template cclass For, class Fred, class T> void replace(For first, For last, Pred p, const T& new_v); template cclass In, class Out, class T> Out replace__copy (In first, In last, Out res, const T& v, const T& new_v); template cclass In,class Out,class Pred,class T> Out replace„copy(In first, In last, Out res, Pred p, const T& new v); Пример. Задан вектор из строк, содержащий названия язы- ков программирования. Заменить в этом векторе все вхождения строки «Pascal» строкой «C++». и____________________________________________________ vector cstring> v; v.push_back("Pascal" ) ; v.push_back("Basic") ; v.push-back(“Java"); replace(v.begin(), v.end(), string("Pascal"), string("C++")); Алгоритмы remove Алгоритмы remove не удаляют элементы последовательностей, а только перемещают их за пределы заданного отрезка последова- тельности. Они также следуют модели unique/unique_сору. template cclass For, class T> For remove(For first, For last, const T& v); template cclass For, class Pred> For remove^if(For first, For last, Pred p);
190 Часть A template <class In, class Out, class T> Out remove—copy(In first, In last, Out res, const T& v) ; template <class In, class Out, class Pred> Out remove_copy—if(In first, In last, Out res, Fred p); Пример. Из списка названий языков программирования ис- ключить те, которые начинаются на букву «С», и составить из них другую последовательность. U_______________________________________________________ inline bool c_yes(string s) { return s[0] "C”; } inline bool c_not(string s) { return !C—yes(s); } void main() { vector <string> v, c; // Вначале вектор v содержит 5 названии языков v.push_back("С”); v.push—back(”Basic"); v.push.back(“Java”); v.push_back("Pascal”) ; v.push_back (’’C++”) ; // To, что не начинается на “С”, удаляем из // последовательности // Все оставшееся ("С" и “C++”) копируем в вектор с remove_сору—if (v. begin () , v.endO, back-inserter(с), c_not); // To, что начинается на С, удаляем из // последовательности vector <string>::iterator р - remove—if (v. begin () , v.endO, c_yes) ; // Остаток последовательности (мусор) обрезаем
Стандартна» библиотека шаблонов 191 V.erase(р, v.endO); } Алгоритмы fill и generate Алгоритмы fill и generate созданы для присваивания значе- ний элементам последовательностей. и________________________________________________________ t.emplat erclass For, class T> void fill(For first, For last, const T& x); гemplate<.class Out, class Size, class T> void fill_n(Out first, Size n, const T& x); templateeclass For, class Gen> void generate(For first, For last, Gen g) ; templatecclass Out, class Size, class Gen> void generate„n (Out first, Size n, Gen g); Алгоритм fill( ) присваивает значение x, алгоритм generate ( ) присваивает результат операции g. Алгоритм ) и generate_n() п раз присваивают значение, причем алгоритм generate_n( ) для этого п раз выполняет операцию g. Пример. Заполнить вектор факториалами десяти целых чисел. U inc factorial() { static int n -- 0; static int f ~ 1; int res f; f (т+п); return res; } const int SIZE 10; vector <int> v(STZE); /. Заполняет вектор факториалами generate(v.begin(), v.end(), factorial) // Достигает того же результата generate__n (v.begin () , SIZE, factorial);
192 Часть 3 Алгоритмы reverse, rotates random_shuff!e Все эти алгоритмы изменяют расположение элементов после- довательности. Алгоритм reversef) меняет порядок следования эле- ментов на обратный. V__________________ templatecclass Bi> void reverse(Bi first, Bi last); templatecclass Bi, class Out> Out reverse copy (Bi first, Bi last, Out res); Алгоритм rotate() циклически сдвигает последовательность, пока один из ее элементов (middle) не станет первым элементом последовательности. U_______________________________________________________ templatecclass For> void rotate(For first, For middle, For last); templatecclass For, class Out> Out rotate_copy(For first, For middle, For last, Out res); Алгоритм randomshuffle «тасует» элементы последователь- ности при помощи генератора случайных чисел. Элемент в пози- ции i меняется местами с элементом в позиции т, где т — случай- ная величина из диапазона [0, п), a i пробегает все значения от 0 до п - 1 (п — количество элементов последовательности). Первый вариант алгоритма использует равномерно распреде- ленный генератор, второй — позволяет задать произвольное рас- пределение значений т при помощи функции f. и_______________________________________________________ templatecclass Ran> void random__shuf f le (Ran first, Ran last); templatecclass Ran, class Fun> void random.^shuffle (Ran first, Ran last, Fun& f); Алгоритмы swap Алгоритмы swap меняют местами элементы или отрезки пос- ледовательности. Элементы задаются ссылками или указателями.
Стандартная библиотека шаблонов 193 V__________________________________________ templatecclass Т> void swap(T& х, Т& у); template<class Fori, class For2> void iter_swap(Fori x, For2 y); template<class Fori, class For2> For2 swap_ranges(Fori firstl, Fori lasti, For2 first2); Последний вариант алгоритма меняет местами отрезок [firstl, lasti) с отрезком [first2, last2), где last2 = lasti ~ firstl + first2. Замечание. Запись [а, b) означает полуоткрытый интервал последовательности, когда левая граница принадлежит ин- тервалу, а правая — нет. 4.3. Задачи Реализация copyjbackward Предложите свою реализацию стандартного алгоритма copybackwardf ). Сумма предыдущих В векторе находятся целые числа. Замените каждое число суммой всех предшествующих: а) включая заданное; б) исключая заданное. Сложение векторов Сложите поэлементно три вещественных вектора. Результат сохраните в первом из них. Удаление объектов Вектор содержит указатели на объекты типа Т в свободной памяти. Напишите обобщенную функцию, которая уничтожит все объекты, а указатели обнулит. Числа Фибоначчи. Заполните вектор числами Фибоначчи.
194 Часть 5 5. Сортировка и прочие алгоритмы 5.1. Алгоритмы, связанные с сортировкой Перечень алгоритмов • sort() — сортирует последовательность со средней эффектив- ностью О(п log(n)); • siablesortf) — сортирует, сохраняя порядок следования рав- ных элементов; • partial sort () — упорядочивает начальную часть последова- тельности; • partial sort copyf) — копирует, упорядочивая начальную часть результата; • nth_element( ) — ставит на нужное место n-й элемент; • lower boundf ) — находит первое вхождение заданного значения; • upper boundf ) — находит первый элемент, значение которого больше заданного; • equalrangef ) — делает то же, что Zowerboundf ) ииррег boundf ) вместе; • binary searchf ) — проверяет, есть ли данное значение в отсор- тированной последовательности; • mergef ) — сливает две отсортированные последовательности; • partitiiionf ) — помещает в начало последовательности элемен- ты, удовлетворяющие условию; • stablejpartititionf ) — делает то же с сохранением относитель- ного порядка следования элементов. Алгоритмы sort Первый из алгоритмов sort использует для сравнения элемен- тов операцию «<», второй — любой бинарный предикат стр. V_____________________________________________________ templatecclass Ran> void sort(Ran first, Ran last); templatecclass Ran, class Cmp> void sort(Ran first, Ran last, Cmp cmp); Временная сложность сортировки в среднем равна О(п log(n)), но в худшем случае может достигать О(п х п). Гарантированную
Стандартная библиотека шаблонов 195 временную сложность О(п log(n) log(n)) обеспечивает алгоритм stable sort( ). Еще он сохраняет взаимный порядок равных элементов, и______________________________________________________ t ernp 1 a t е < с 1 a s s Ra n > void stable„sort(Ran first, Ran last); templatecclass Ran, class Cmp> void stable_sort(Ran first, Ran last, Cmp cmp); Алгоритм partial sort упорядочивает не всю последователь- ность, а лишь ее отрезок [first, middle), и________________________________________________________ t emp 1 a t е< с 1 a s s Ran> void partial__sort (Ran first, Ran middle, Ran last); templatecclass Ran, class Cmp> void partial„sort(Ran first, Ran middle, Ran last, Cmp cmp); templatecclass In, class Ran> Ran partial__sort_copy (In firstl, In lastl, Ran first2, Ran last2); templatecclass In, class Ran, class Cmp> Ran partial_sort_copy(In firstl, In lastl, Ran first2, Ran last2, Cmp cmp); Пример. Дана последовательность дат. Каждая дата состоит из номера месяца и номера дня. Выберите три самые ранние даты и сохраните их в другой последовательности. U________________________________________________________ struct Date { int month, day; Date () { } Date(int m, int d): month(m), day(d) {} bool operator < (const Date& o) return (month c< 10 I day) c (o.month cc 10 I o.day); } };
196 Часть 3 ostream& operator « (ostream& out, const Date& date) out << date.month << return out; } « date.day << void main() { vector <Date> v, vl(3); v.push_back(Date(0,0)); v.push_back(Date(2,5)); v.push_back(Date(5,5)); v.push-back(Date(5,2)); partial—sort—copy(v.begin(), v.endO> vl.begin(), vl.end()); copy(vl.begin(), vl.end(), ostream—iterator<Date>(cout)); cout « endl; Двоичный поиск Поиск в отсортированной последовательности может быть го- раздо эффективнее (O(log п)), чем поиск в неотсортированной (О(и))- Заявленную эффективность обеспечивает алгоритм дво- ичного поиска. V______________________________________________________ templatecclass For, class Т> bool binary—search(For first, For last, const T& val); templatecclass For, class T, class Cmp> bool binary_search(For first, For last, const T& val, Cmp cmp); Искомых элементов в последовательности может быть много, поэтому binary search не возвращает местоположение элемента, а только сигнализирует о его присутствии. Если мы хотим опреде- лить, где расположен элемент, нужно уточнить, который из них нас интересует.
Стандартная библиотека шаблонов 197 Алгоритм lower ~bound() находит левую границу диапазона искомых элементов в последовательности (естественно, в отсорти- рованной последовательности все они расположены рядом). V_______________________________________________________ templatecclass For, class Т> For lower_.bound (For first, For last, const T& val); templatecclass For, class T, class Cmp> For lower—bound(For first, For last, const T& val, Cmp cmp); Алгоритм upper bound имеет ту же форму, что lowerjbound, и находит правую границу диапазона. Алгоритм equalrange находит обе границы сразу. U__________________________________________________ templatecclass For, class Т> paircFor, For> equal—range(For first, For last, const T& val); templatecclass For, class T, class Cmp> paircFor, For> equal-range(For first, For last, const T& val, Cmp cmp); Если алгоритм lower boundf ) не находит элемент val, он воз- вращает итератор на первый элемент, больший, чем val, или last, если такового не существует. Такой же способ сообщения о неуда- че используется в upperJbound() и equaljrange(). Возвращаемое значение показывает, куда надо вставить новый элемент, чтобы сохранить порядок в последовательности. Пример. Есть упорядоченный вектор из дат. Вставить в него новую дату. и_______________________________________________________ Date d(3, 3); vector <Date>::iterator i ~ lower_bound (v.begin () , v.endO, d) ; v.insert(i, d);
198 Часть 3 Алгоритм merge Алгоритм merge() соединяет две упорядоченные последова- тельности в третью, тоже упорядоченную. Например: [1, 3, 5, 6, 9] 4- + [2, 3, 7, 8] = [1, 2, 3, 3, 5, 6, 7, 8]. U________________________________________________________ templatecclass Ini, class In2, class Out> Out merge(Ini firstl, Ini lasti, In2 first2, In2 last2, Out x) ; templatecclass Ini, class In2, class Out, class Cmp> Out merge(Ini firstl, Ini lasti, In2 first2, In2 last2, Out x, Cmp cmp); Алгоритм inplace_merge() делает то же с двумя частями од- ной последовательности. и_______________________________________________________ templatecclass Bi> void inplace_merge(Bi first, Bi middle, Bi last..); templatecclass Bi, class Cmp> void inplace__merge (Bi first, Bi middle, Bi last, Cmp cmp); При равенстве элементы первой последовательности будут все- гда предшествовать элементам второй последовательности. Алгоритмы partition Алгоритмы partition располагают все элементы, удовлетворяю- щие условию, перед элементами, которые условию не удовлетворяют. U______________________________________________________ templatecclass Bi, class Pred> Bi partition(Bi first, Bi last, Pred p) ; templatecclass For, class Pred > For stable part.it ion (For first, For last, Pred p) ; Алгоритм stable partillonf ) сохраняет внутри этих двух под- последовательностей то взаимное расположение элементов, кото- рое они имели до упорядочения.
Стандартная библиотека шаблонов 199 Операции с множествами Последовательности можно рассматривать как множества и вы- полнять над ними операции объединения, пересечения, разности множеств. Стандартная библиотека поддерживает такие операции только для упорядоченных последовательностей, т.к. для неупоря- доченных они не эффективны. Особенно часто операции выполня- ются для контейнеров set и multiset, которые всегда упорядочены. Алгоритм includes( ) проверяет, входят ли все элементы вто- рой последовательности в первую. U________________________ templatecclass Ini, class In2> bool includes(Ini firstl, Ini lastl, In2 first2, I.n2 last2); templatecclass Ini, class In2, class Cmp> bool includes(Ini firstl, Ini lastl, In2 first2, In2 last2, Cmp cmp); Алгоритмы объединения (set union), пересечения (set intersection), разности (set difference) и симметрической разности (set symmetric difference) множеств имеют сходную форму. Покажем ее на примере алгоритма setunion. U templatecclass Ini, class In2, class Out> tt.it set „union (Ini firstl, Ini lastl, In2 first2, In2 last2, Out x) ; tempiatecclass Ini, class In2, class Out set_union(Ini firstl, Ini lastl, In2 first2, In2 last2, Out, class Cmp> Out x, Cmp cmp); 5.2. Прочие алгоритмы Алгоритмы min и max Алгоритмы семейства min находят меньшее из двух значений V_______________________________________________ templatессlass Т> const Т& min(const Т& х, const Т& у);
200 Часть 3 templatecclass Т, class Cmp> const T& min(const T& x, const T& y, Cmp cmp); и наименьшее значение в последовательности. V________________________________________________ templatecclass For> For min—element(For first, For last); templatecclass For, class Cmp> For min—element(For first, For last, Cmp cmp); Точно такую же форму имеют алгоритмы семейства max. Лексикографическое сравнение двух произвольных последо- вательностей выполняет алгоритм lexicographical_compare(). U_________________________________________________________ templatecclass Ini, class In2> bool lexicographical—compare(Ini firstl, Ini lasti, In2 first2, In2 last2); templatecclass Ini, class In2, class Cmp> bool lexicographical—compare (Ini firstl, Ini lasti, In2 first2, In2 last2, Cmp cmp); Первый из двух алгоритмов использует для сравнения элемен- тов оператор «<», второй — произвольный предикат. Перестановки Для решения комбинаторных и других задач иногда требу- ется построить все перестановки некоторой последовательности элементов. Функция nextjperrnutation() пялуч&ет одну перестановку и вырабатывает другую, следующую за данной в лексикографичес- ком порядке. U_______________________________________________________ templatecclass Bi> bool next—permutation (Bi first, Bi last); templatecclass Bi, class Cmp> boo.l next—permutat ion (Bi first, Bi last, Cmp cmp);
Стандартная библиотека. шаблонов 201 Если следующей перестановки не существует, функция воз- вращает значение false и вырабатывает самую первую (в лексико- графическом смысле) перестановку. Пример. Распечатать все перестановки символов одной строки. V string s(“abed"); while (next„permutation(s.begin(), s.end())) cout << s << endl; Функцияprev_permutation( ) вырабатывает предыдущую переста- новку и ведет себя симметрично по отношению к next_perm,utation(), Накопление Алгоритм accumulate занимается накоплением суммы элемен- тов последовательности. Он относится к категории обобщенных вычислительных алгоритмов и поэтому объявлен в заголовке <numeric>. и____________________________________________________ templatecclass In, class T> T accumulate(In first, In last, T init); templatecclass In, class T, class BinOp> 7 accumulate(In first, In last, T init, BinOp op); Первая версия алгоритма складывает элементы, используя оператор «-+-», который для них определен. В аргументе init пере- дается начальное значение суммы. Тип этого аргумента определя- ет тип результата. Пример. Сложить элементы массива. V________________________________ int m[] = {1,2,3); int sum •- accumulate (m, m + 3, 0) ; Вторая версия алгоритма использует вместо сложения бинар- ную операцию ор, то есть вместо init = init + х; в каждом шаге цикла выполняет init ~ op(init, х);.
202 Часть 3 5.3. Задачи Сортировка слиянием Запрограммируйте обобщенную функцию сортировки template <class Bi> void mysort(Bi first, Bi last) на основе алгоритма inplace merge. Случайные числа Запрограммируйте равномерно распределенный генератор слу- чайных чисел и проверьте равномерность распределения: подсчи- тайте среднее и постройте гистограмму. б. Библиотека потоков Библиотека потоков предоставляет набор классов для управле- ния вводом-выводом. Чтобы получить доступ к библиотеке потоков, в программу нужно включить заголовочный файл < iostr еат>. Также могут понадобиться заголовочные файлы: • <fstream> — файловый ввод-вывод; • <iomanip> — форматирование ввод и вывода при помощи ма- нипуляторов; • <strstream> — резидентные потоки. 6.1. Организация библиотеки потоков Диаграмма наследования главных классов библиотеки пото- ков изображена ниже. | ios base | |basic istream<>[ |basic ostream<>| |basic_iostream<>! V basic__f streamO Г " basic^stringstreamo I
Стандартная библиотека шаблонов 203 Содержание классов: • ios base — не зависимый от национальных особенностей фор- мат состояния потока; • basic_ios — зависимый от национальных особенностей формат состояния потока; • basic istream — операции ввода (виртуальный наследник basic_ios)\ • basic ostream — операции вывода (виртуальный наследник basic_ios); • basic iostream, — операции ввода и вывода (множественное на- следование от двух предыдущих классов); • basic /stream — средства работы с файлами; • basic stringstream — резидентные потоки (потоки в памяти). Все классы, кроме iosbase, представляют собой шаблоны, параметризированные символьным типом (как basic __st ring). Такие типы как, ios, istream, ostream, iostream и т.п., явля- ются синонимами соответствующих шаблонов, конкретизирован- ных типом char (как string). 6.2. Вывод Задача вывода состоит в превращении разнотипных значений в по- ток символов. Эту задачу решает обобщенный класс basic_pstream. Вывод встроенных типов Для вывода встроенных типов члены класса basic_pstream перегружают операцию ««», которую в данном случае называют «помещение в поток». U______________________________________________________ basic„ostream& operator<< (short n); basic_ostream& operator<< (int n) ; basic_ostream& operator<< (long n); Функция operator«() возвращает ссылку на ostream, чтобы можно было выполнять цепочечный вывод. U_______________________________________________________ I cout << "х = " « х << endl; I
204 Часть 3 Значение типа bool по умолчанию выведется как 0 или 1. Как это изменить и как управлять форматом вывода описано в разделе «Форматирование ввода и вывода». Вид выводимых указателей зависит от реализации. Функции put() и write() Функция класса basic upstream put() записывает в поток один символ, а функция writef) — массив символов. Вывод пользовательских типов Для вывода пользовательского типа операцию « « » надо перегру- зить. Как это сделать, говорилось в разделе «Перегрузка операций». Здесь же рассмотрим вопрос, как поступать, когда надо вывес- ти объект, для которого известен только базовый класс. Иными сло- вами, как обеспечить полиморфизм вывода при условии, что пользо- вательская функция operator«() не является членом класса, а значит, не может быть виртуальной. Покажем это на примере. Пример. Пусть имеется базовый класс А и производный от него класс В, Вначале обеспечим обычный полиморфный вывод в системе этих классов, использовав для этого какую-нибудь вир- туальную функцию, скажем type(). и _______________________________________________________ class А { public: virtual ostream& put(ostream& out) const - 0; }; class B: public A { public: ostreamk put(ostream& out) const { return out << "This is B" « endl; } }; Теперь перегрузим функцию operator«() для объектов А и убедимся, что это работает. и_____________________________________________________ ostream& operator<< (ostream& s, const A& a) { return a.put(s);
Стандартная библиотека шаблонов 205 Потоки вывода В <iostream> объявлено несколько потоков вывода: • ostream cout — стандартный символьный поток вывода, ана- лог stdout в библиотеке языка С; • ostream cerr — небуферизованный поток для сообщений об ошибках, аналог sterr в библиотеке С; • ostream clog — буферизованный поток для сообщений об ошиб- ках, аналога в библиотеке С нет. Такие же потоки wo^tream wcout, wcerr, wclog; объявлены для расширенных символов. Стандартные потоки можно переназначить пользовательским объектам-потокам cout = mystream;, что позволяет перенаправить стандартный вывод, например, в файл. 6.3. Ввод Ввод встроенных типов Операции ввода для встроенных типов обеспечивает класс- шаблон basic lstream. Он перегружает функцию operator»() — «извлечение из потока». U_______________________________________________________ basic—istream* operator>> (short& n); basic__istream& operator>> (int& n) ; basic_istream& operator>> (long& n); Оператор «>>» реализован так, что пропускает все символы- разделители, предшествующие вводимому значению. Это упроща- ет код ввода.
206 Часть 3 Пример. Заполнить вектор целыми числами из стандартного потока. и vector<int? > v (5) ; for (int i = 0; i < v.sizef) && cin >> vfi4-*];); Цикл прервется, если будет заполнен весь вектор или в потоке встретится то, что нельзя будет истолковать как запись целого чис- ла. То, что прервет ввод, останется в потоке и будет прочитано дру- гими операторами ввода. Оператор ввода, перегруженный для символов, также пропус- кает все символы-разделители и вводит символы, которые разде- лителями не являются. Имеется вариант оператора для ввода мас- сива символов. и _______________________________________________________ char m[100]; cin >> m; Этот код вводит из потока все символы до первого разделите- ля или конца файла. Введенная в массив последовательность до- полняется нулем, поэтому в переменной т появится С-строка. Из- за возможности переполнения более безопасным считается ввод в string, а не в массив символов. Потоки ввода В <iostream> объявлены следующие потоки ввода: • ostream cin — стандартный символьный поток ввода, аналог stdin из библиотеки С; • wostream wcin — стандартный поток для расширенных символов. Функции для ввода символов Для низкоуровневого ввода символов в классе basic_istream имеются функции get( ) и getline( ). Они не отличают разделители от других символов и помещают 0 в конец прочитанной последова- тельности символов. • intjtype get() — считывает один символ из потока. Функ- ция возвращает код символа или маркер конца файла — char_traits::eof( )\
Стандартная библиотека шаблонов 207 • baste_1stream& get(Ch& с) — считывает один символ в аргумент; • basic istream& get(Ch *s, streamsize n, Ch term) — считывает не более n - 1 символов в массив s и останавливает чтение пе- ред символом term, который остается в потоке; • basic_istream& get(Ch *s, streamsize n) ~ считывает не бо- лее n - 1 символов в массив а, завершающим символом счи- тается *\п’. Пример типичного применения функции get( ) — ввод в бу- фер фиксированного размера. и_______________________________________________________ crieir but [100]; сi n.get(buf, 100, ” \t”); Функции getline() во всем похожи на соответствующие функ- ции get( ), но удаляют из потока завершающий символ. Функция read() еще более низкоуровневая, чем get(). Вызов read(р, п) считывает ровно п символов в р[0], р[1 ], ..., р[п - 1 ]. Она не ищет в потоке ограничителей и не добавляет 0 в память. Функция ignore(n) пропускает п символов из потока. Как и getline( ), она остановится раньше, если встретит завершающий символ и этот символ из потока удаляется. Поскольку число символов, прочитанных из потока функци- ями get(), getline(), read(), зависит от разных причин, в классе есть функция gcount( ), которая возвращает число символов, про- читанных из потока последней операцией. Связывание потоков Рассмотрим программу и____________________ s t .г 1 ng s; esut << "Введите имя:"; tin >' > s ; Поскольку вывод в cout буферизируется, операция ввода мо- жет начаться раньше, чем на экране появится приглашение. Что- бы этого не произошло, выводной поток надо связать с вводным потоком операцией tie(). •U____________________________________________________________ i с in. l. ie (&cout) ; 1
208 Часть 3 Теперь буфер выводного потока будет очищаться (выполнять- ся операция flush(7) всякий раз, когда завершается операция вво- да входного потока. 6.4. Состояние потока Все объекты-потоки происходят от класса basic_ios, который характеризуется состоянием. Состояние представляется в виде на- бора битовых полей. Константы состояния, как и многие другие константы, определяются в классе iosjbase: U_________________________________________________________ class ios_base { public: typedef T2 iostate; // T2 - определен в реализации static const iostate goodbit, // все в порядке eofbit, // конец файла failbit, // ошибка операции ввода-вывода badbit; // некорректная операция Замечание. Поскольку значения констант зависят от реали- зации, в программе следует пользоваться их символической формой. Опрос состояния потока Специально для этой цели в классе basic_ios определены сле- дующие методы: • bool good( ) — все в порядке, следующая операция может вы- полняться; • bool eof( ) — достигнут конец файла; • bool fail() — следующая операция не выполнится; • bool bad() — поток испорчен. Функция iostate rdstate() — возвращает текущее состояние в целом. Очередная операция ввода может выполняться, только если поток находится в состоянии good. Когда установлен флаг fail, это означает, что текущая операция не выполнилась и следующая опера- ция не может выполняться, пока поток не переведут в состоянии good, например, путем вызова функции clearf ).
Стандарта я библиотека шаблонов 209 Когда, помимо fail* установлен флаг bad, это означает, что це- лостность потока не гарантируется, т.е. некоторые символы могут быть потеряны. Пример. Проверка состояния потока. 4______________________________________________________ int i; cin >> i; // вводим не число if (cin.fail()) cout << "Error"; Для проверки нормального состояния потока лучше исполь- зовать следующий синтаксис. и if (cin) { // Все хорошо } Он основан на том, что когда условное выражение оператора if имеет объектный тип, соответствующий класс должен допускать пре- образование к арифметическому, логическому или типу указателя. Оператор преобразования к указателю void*( ) перегружен в basic _ios так, что возвращает 0, если установлен один из флагов ошибок. Этот же оператор преобразования вызывается, когда объект сравнивается с нулем. и______________ if (cin -- 0) cout << "Error"; Логический оператор отрицания переопределен в basic_ios так, что возвращает 0, если установлен хотя бы один из флагов ошибок, и if (!cin)____________________________________ cout << "Error"; 6.5. Итераторы потоков Чтобы согласовать операции ввода-вывода с механизмом кон- тейнеров и алгоритмов, в стандартной библиотеке имеются итера- торы потоков: • ostream iterator — для записи в ostream*,
210 Часть 3 • 1st ream—iterator — для чтения из 1st ream. Итератор ostream__iterator преобразует выполняемые над ним операции записи и инкрементирования в операции вывода в поток. Пример. Вывод десяти целых чисел. и ostream_iuerator<int> os(cout);__________________________ for (int i = 0; i < 10; ++i) { *os ~ i; + + os ; } Ka к для в с яко го од нон а правлен ног 6 й т ё р а т о р а ,“для ostream iterator операции вывода должны чередоваться с опе- рациями инкрементации. Замечание. Хотя некоторые реализации автоматически ин- Са&З крементируют итератор после каждого вывода, а операцию «+-!-» реализуют пустой, для переносимости код должен быть именно таким. 1.Пример. Распечатка вектора. и vector <int> v; copy(v.begin(), v.end(), ostream_iterator<int>(cout)); Итератор istream_iterator преобразует операции чтения и ин- крементирования в операции ввода из потока. 6.6. Задачи Ввод комплексных чисел Перегрузите операцию извлечения из потока в классе struct Complex {double re, im; };. Воспользуйтесь проверкой состояния для обработки ошибок. Возможные форматы чисел в потоке: 3 (3.0) (3.0 5.6).
Стандар’гная библиотеки шаблонов 211 Числа — прописью Создайте надстройку над потоком вывода, которая выводимые числа пишет прописью. Надстройкой называется класс, получаю- щий в конструкторе объект, поведение которого следует изменить или дополнить. Вывод на экран справа налево Создайте потоковый класс, который бы выводил на экран сим- вольную информацию в направлении справа налево. 7. Форматирование ввода и вывода 7.1. Флаги форматирования Управление форматированием ввода и вывода производится классом basicios и его базовым классом iosjbase. Текущее состоя- ние формата определяется набором битовых флагов, хранящимся в защищенном члене класса ios_base. В этом же классе находятся открытые константы и функции для управления флагами. U__________________________________________________ class ios__base { public: typedef T1 fmtflags; s t a t. i с cons t fmt f .1 ags boolalpha, dec, fixed, hex, internal, left, oct, right,... Смысл констант следующий: • skipws — пропуск символов-разделителей на входе; • left — выравнивание значения по левому краю поля вывода; • right — выравнивание по правому краю; • internal — выравнивание, при котором отступ лежит между знаком и значением; • boolalpha — вывод символического представления для true и false* *, • dec - - десятичная система для целых чисел; • hex — шестнадцатиричная система для целых чисел; • oct — восьмиричная система для целых чисел;
212 Часть 3 • scientific — числа с плавающей точкой выводятся как d. ddddEddd; • fixed — числа с плавающей точкой выводятся как ddd.dddd*, • showbase — на выходе префикс 0 перед восьмиричными числа- ми и Ох перед 16-ми; • showpoint — выводит незначащие нули; • showpos — выводит знак « + » перед положительными числами; • uppercase — выводит «£», «X» вместо «е», «х» в записи чисел; • adjustfield — комбинация флагов left | right | internal*, • basefield — комбинация флагов dec | oct | hex*, • floatfield — комбинация флагов scientific | fixed*, • unitbuf — очистка буфера перед каждой операцией вывода. Замечание. Тип fmtflags обычно реализуется типом long. Следующие функции-члены управляют флагами: • flags( ) — читает флаги; • flagsf fmtflags f) — устанавливает флаги; • setf(fmtflags mask) — устанавливает флаги, биты которых ус- тановлены в параметре; • unsetf(fmtflags mask) — сбрасывает флаги, биты которых ус- тановлены в параметре; • setf(fmtflags f, fmtflags mask) — присваивает флагам, биты которых установлены во 2-м параметре, значения соответству- ющих битов 1-го параметра. Все функции возвращают прежнюю комбинацию флагов в ви- де значения типа fmtflags. Пример, Установить 16-ю систему счисления для вывода це- лых чисел в поток cout. и________________________ cout.unsetf(ios::declios::oct); // снять флаги cout.setf(ios::hex); // установить флаг Это же можно сделать одним вызовом функции setff ) с двумя аргументами V________________________ [ cout. set f(ios: :hex, ios: :dec Iios::oct Iios::hex) ; | Специально для подобных применений существует константа iosr.basefield = ios::dec|ios::oct|ios::hex;,
Стандартная библиотека шаблонов 213 поэтому правильным будет использовать именно ее. U __________________________________________ j cout.setf(ios:;hex/ ios::basefield); 7.2. Форматирующие функции-члены Кроме флагов, форматированием управляют 3 функции-члена класса iosbase. Ширина поля — widthO int ios::width() — возвращает текущее значение ширины поля потока; int ios::width(int) — устанавливает ширину поля. При вводе ширина поля задает максимальное число читаемых элементов. При выводе ширина поля задает минимальное пространство, занимаемое выводимым значением. В пространстве, не занятом значением, размещается заполняющий символ. Если ширина зна- чения превышает ширину поля, последняя игнорируется. По умол- чанию ширина поля равна нулю. Ширина поля обнуляется после каждого помещения данных в поток. Пример. Между цифрами 1 и 2 расположено 29 пробелов. U_____________________________________________________ cout << 1; cout.width(30); cout << 2; Заполняющий символ — fill() char ios::fill() — возвращает текущий символ заполнения; char ios::fill(char) — устанавливает новый символ заполне- ния и возвращает значение старого. По умолчанию заполняющим символом является пробел. Точность вещественных чисел — precisionf) int ios::precision() — возвращает текущее значение точности; int ios::precision(int) — устанавливает новое значение точнос- ти и возвращает старое.
214 Часть 5 По умолчанию точность равна 6 цифрам. Если установлен флаг scientific или fixed, точность задает число цифр, выводимых после десятичной точки, иначе — общее число значащих цифр. 7.3. Манипуляторы ввода и вывода Идея манипуляторов в том, чтобы при помощи операций «»» и ««» не передавать данные, а изменять состояние потока, в част- ности, его флаги форматирования. Манипуляторы без параметров Манипуляторы без параметров являются указателями на функ- ции определенного типа. Для этих указателей операции помеще- ния в поток и извлечения из потока перегружены так, чтобы вы- зывались функции, на которые указывает указатель. Вызов этих функций управляет флагами потока. Изменения, внесенные манипуляторами, сохраняются до сле- дующей установки (за исключением ширины поля, которая уста- навливается в 0 после каждой операции вывода). Перечислим некоторые манипуляторы: • endl — помещает в выходной поток символ новой строки и вы- зывает метод flush(); • ends — помещает в выходной поток нулевой символ; • flush — выгружает буфер потока; • dec — устанавливает основание 10; • hex — устанавливает основание 16; • oct — устанавливает основание 8; • ws — заставляет игнорировать ведущие пробелы при вводе. Пример. Самодельный манипулятор без параметров и его ис- пользование. и______________________________________________________ // Манипулятор устанавливает ширину поля в 10 позиций ios& widthlO(ios& х) { х.width(10); return х; }
Стандартная библиотека шаблонов 215 // Использование манипулятора cout setf ill (’’*") « 1 « widthlO < < 2; Как видно из примера, манипулятор без параметров — это ука- затель на функцию с одним параметром, которая получает и воз- вращает ссылку на поток. Приведенный пример носит частный характер, более общее решение выглядит так: и_____________________________________________________ // Определение манипулятора template <class Ch> basic_ios<Ch>& widthlO(basic_ios<Ch>& x) { x.width(10); return x; Манипуляторы с параметрами В заголовке <lomanlp> определены следующие параметризо- ванные манипуляторы: • setbase(int х) — задает основание системы счисления; ♦ resetiosflags(long х) — сбрасывает флаги, указанные в па- раметре; • setiosflags(long х) — устанавливает флаги, указанные в па- раметре; • setfUl(int х) — задает заполняющий символ; • setprecisionf int х) — задает точность вещественных чисел; • setw(ini х) — задает ширину поля. Манипуляторы с параметрами — это функции, которые возвращают объекты особого типа. Для этого типа перегруже- ны операции извлечения и помещения в поток. Когда вызыва- ется манипулятор, он создает объект, в который закладывает значение своих параметров и необходимый алгоритм формати- рования в виде указателя на вспомогательную функцию. Пере- груженная операция «»» (или «<<») через предоставленный объект вызывает эту функцию с параметрами, что и приводит к нужному результату. Программист может создавать в этом стиле собственные ма- нипуляторы, не внося изменений в классы потоков или другие шаблоны стандартной библиотеки.
216 Часть 3 7.4. Файловые потоки Библиотека потоков содержит 3 класса, предназначенных для ввода и вывода в файлы: • basic_ifstream — для операций с входным файлом (наследует basic _ist ream); • basic__ofst ream — для операций с выходным файлом (наследу- ет basic _pstream); • basie fstream — для входных и выходных операций (наследу- ет basiC—iostream). Все классы объявлены в заголовке <fstream> и добавляют к ба- зовым интерфейсам только функции ореп(), close( ) и проверочную функцию isopen(). В этом разделе мы рассмотрим те члены клас- сов, которые полезны при работе с файлами. Пример. Типичная работа с выводным файлом: открыть, что- нибудь вывести, закрыть. у __________________________________________________ ftinclude <fstream> using namespace std; void main() { ofstream f; f.open("t emp.txt"); if (f) { f « "что-нибудь"; f.close(); } } Вводные и универсальные файлы открываются точно так же. Открытие файлов Функция ореп() имеет второй необязательный параметр — режим работы с файлом. Его значения задаются константами типа ios_base::openmode*. • аРР — добавление в конец файла; • ate — указатель вывода устанавливается в конец, но выводить можно в любом месте;
Стандартная библиотека шаблонов 217 • binary — ввод-вывод в двоичном виде, а не текстовом; • in — открыть для ввода; • out — открыть для вывода; • trunk — выводной файл усекается до нулевой длины. Можно объединять эти значения с помощью битового опе- ратора По умолчанию приняты следующие режимы: • для объектов if stream • для объектов of stream • для объектов fstream ios::in; ios::out | ios::trunk, ios::in | ios::out. Открытие потока можно выполнить при его конструировании, т.е. существует конструктор с такими же параметрами, что у функ- ции ореп(), например, и_______________________________________________________ | fstream f("temp.txt"); | Закрытие файлов Метод closef ) выгружает буфер и закрывает связанный с по- током файл. Если при попытке закрыть файл происходит ошибка, устанавливается флаг состояния failbit. Деструктор файлового объекта автоматически закрывает файл. Таким образом, код вывода в файл из предыдущего примера можно сократить. и_______________________________________________________ ofstream f("temp.txt”); if (f) { f << "что-нибудь"; ) Текстовый и бинарный ввод-вывод В бинарном режиме данные при вводе и выводе не интерпре- тируются. Чтобы открыть файл в бинарном режиме, включите флаг ios base::blnary в соответствующий параметр конструктора или функции ореп( ). В текстовом режиме (когда флаг binary не установлен): а) при вводе каждая пара символов \г\п преобразуется в един- ственный символ \п; б) при выводе символ \п преобразуется в пару символов \г\п .
218 Часть 3 Бесформатный ввод и вывод Бесформатный ввод и вывод обеспечивают максимальную ско- рость обмена данными. Пример. Вывести на экран содержимое файла. U_____________________________________________________ char с; ifstream in("temp.txt", ios::in Iios::binary); while (in) { in.get(c); cout.put(c); } Используя функцию int peek(); можно получить очередной символ из потока ввода без его извле- чения из потока. С помощью функции istream& putback (char с); можно поместить символ с во входной поток. Произвольный доступ к файлу Произвольный доступ реализуется при помощи функций, ус- танавливающих указатель файла в произвольную позицию: • istream& seekg(pff_type смещение, seekdir точка_ртсчета); • ostreamdc seekp(pff_type смещение, seekdir точка_отсчета); где off_type — целый тип данных, тип seekdir объявлен в классе ios_base. Там же определены три константы этого типа: • beg — отсчет от начала файла; • cur — отсчет от текущей позиции указателя; • end — отсчет от конца файла. Определить позицию указателя для чтения и указателя для записи можно при помощи функций: • P()S.JyPe iellgf )‘, • pos type tellp(); гдеpos type — целый тип данных. Пример. Поместить символА в пятую позицию файла. Осталь- ные символы не менять.
Стандартная библиотека шаблонов 219 V_______________________________________________________ istream f ( "222 . txt:" , ios::in Iios::out Iios::binary ); f.seekp(5, ios::beg); t .put. ("A"); t.close(); 7.5. Строковые потоки Поток можно прикрепить к строке. Такую строку можно пи- сать или читать, используя средства форматирования потока. Классы строковых потоков stringstream наследуют базовые классы так же, как это делают файловые потоки, т.е. basic istringstream наследует basic jstream и т.д. Они добавля- ют к базовым членам функцию str(): • basic string <Ch> str( ) — получает копию строки; • void str(basic string <Ch> s) — устанавливает копию строки. Классы строковых потоков определены в <sstream>. Пример. Записать в строку отформатированное сообщение. J__________________________________________________ stringstream out; out << "any message”; string s = out.strf); 7.6. Задачи Манипулятор без параметров Определите манипулятор без параметров end2, который бы переводил курсор не в начало следующей строки, а через одну. Манипулятор с параметром Определите манипулятор с одним целым параметром — осно- вание системы счисления. Если значение параметра отлично от 8 или 16, должна устанавливаться десятичная система счисления. Копирование файлов Определите функцию для копирования двоичных файлов. Файл, как массив Объявите класс, который позволял бы работать с символьным файлом, как с массивом, используя операцию индексации.
ПРИЛОЖЕНИЕ Введение в программирование Данное приложение предназначено для тех, кто только начи- нает писать программы и рассматривает С-Н- как свой первый язык программирования. 1. Основные этапы решения задачи на ЭВМ В настоящее время на компьютерах решают самые разнообраз- ные задачи — от расчета баллистических траекторий до завоевания инопланетных территорий (пока только в компьютерных играх). В каждом случае компьютер выполняет какую-то программу, обыч- но довольно сложную. Некоторые из программ требуют от пользо- вателя специальных знаний и особой квалификации, например, программы электронной верстки или автоматизированного проек- тирования, но здесь мы будем говорить не об использовании, а об изготовлении программ. Несмотря на огромное разнообразие про- грамм, в самом процессе их изготовления можно усмотреть нечто об- щее и выделить несколько этапов решения задачи на ЭВМ. 1.1. Постановка задачи Под постановкой задачи понимают математическую или иную строгую формулировку решаемой задачи. Этот этап включает оп- ределение целей создаваемой программы и определение ограниче- ний, налагаемых на программу. При постановке задачи могут быть определены требования: • ко времени решения поставленной задачи; • к объему необходимых ресурсов, например, оперативной памяти; • к точности достигаемого результата.
Введение в программирование 221 1*2. Проектирование программы Если задача вычислительная, то на этом этапе следует выбрать метод расчета. Если разрабатывается компьютерная игра, должен быть определен ее сценарий. В любом случае следует выбрать или создать некую формальную модель, которая будет реализована в программе. На этапе проектирования определяют вид данных, с которыми будет работать программа, ее основные части и то, как эти части будут связаны между собой. 1.3. Разработка алгоритма На этом этапе следует разработать детали проекта программы. Детализацию необходимо довести до той степени, когда кодирова- ние отдельных частей программы (перевод их на алгоритмический язык) станет тривиальным. Возможно, детализация потребует не- скольких стадий — от крупных блоков ко все более мелким. В ре- зультате должно получиться то, что называется алгоритмом реше- ния задачи. Алгоритм — центральное понятие программирования, поэто- му познакомиться с ним следует как можно раньше. Само слово «алгоритм» происходит от имени персидского математика Аль Хорезми, который в IX веке разработал правила четырех арифме- тических действий (сегодня мы бы сказали — алгоритмы арифме- тических действий). В начале XX века алгоритмы стали объектом изучения мате- матиков, появились различные математические уточнения поня- тия алгоритм, и возникла целая отрасль математики — теория ал- горитмов. Результаты, полученные теорией алгоритмов, служат теоретическим фундаментом всей компьютерной технологии, но в повседневной практике программирования не используются, поэто- му сейчас мы будем обсуждать алгоритмы в их интуитивном, про- граммистском понимании. Итак, алгоритм — это описание некоторой последовательности действий, но не всякое, а обладающее определенными свойствами. К этим свойствам относятся: а) дискретность — расчлененность описания на отдельные элементарные действия — инструкции, ко- торые доступны исполнителю алгоритма (человеку, роботу, компь- ютеру,...); б) детерминированность — на одинаковых исходных
222 Приложение данных алгоритм должен всегда давать одинаковые результаты; в) массовость — алгоритм должен работать на множестве одно- типных исходных данных, потенциально бесконечном; г) резуль- тативность — алгоритм должен заканчивать свою работу рано или поздно. 1.4. Кодирование В ходе разработки алгоритма его записывают на алгоритмичес- ком языке, и этот процесс называют кодированием алгоритма. Для выполнения данного этапа необходимо знать хотя бы один из мно- гих существующих языков программирования, а лучше — несколь- ко, чтобы выбрать наиболее подходящий для решаемой задачи. Хотя этап кодирования считается менее творческим, чем пре- дыдущие, для его успешного выполнения требуется хорошее знание как самого языка, так и средств разработки программ: транслятора, компоновщика, программных библиотек и многого другого. 1.5. Отладка и тестирование программы Целью данного этапа является поиск и устранение ошибок в программе. Ошибки бывают синтаксические (нарушение грамма- тики алгоритмического языка) и смысловые (искажение самого алгоритма решения задачи). О первых мы не говорим, их обнаружи- вают и исправляют на этапе кодирования, транслируя программу. Вторые же можно выявить только в процессе проверки программы на специально подобранных входных данных (тестах) или в ходе опытной эксплуатации программы. Разделение процесса разработки программ на 5 этапов носит весьма условный характер. В случае простых программ, которые предстоит писать начинающим программистам, некоторые этапы сливаются, например, проектирование с разработкой алгоритма или кодирование с отладкой. В случае сложных программ могут добавиться новые фазы разработки, например, проектирование базы данных или разработка пользовательского интерфейса. Более важным является то, что работа над сложной програм- мой заключается в многократном повторении цикла разработки, т.к. в процессе тестирования могут быть обнаружены такие ошибки, для исправления которых придется вернуться не только к кодированию
Введение в программирование 223 или алгоритмизации, но и к проектированию, а в тяжелых случаях — пересмотреть саму постановку задачи. Если же удалось разработать полезную программу, то работа над ней не заканчивается этапом тестирования, а переходит в фазу со- провождения. Программа живет, приобретает новые функции, со- вершенствует старые, избавляется от последних ошибок и, наконец, умирает, уступив натиску более молодых программ, покоряющих сердца пользователей сверканием инструментальных панелей, трех- мерностью изображений и стереофоничностью звуков. 1.6. Задачи 1. Какие функции, по вашему мнению, следует включить в про- грамму «Учет и планирование семейного бюджета»? 2. Перечислите известные вам свойства алгоритма. 3. Какими свойствами не обладает следующий «алгоритм»: пой- ди туда, не знаю куда; принеси то, не знаю что. 4. Вспомните как можно больше алгоритмических языков, на- звания которых начинаются на одну и ту же букву. 5. Найдите в следующем предложении одну синтаксическую и одну семантическую ошибку: « Чтобы удалить вирусы с дис- кеты, вставьте ее в стриммер и нажмите клавишу ВВОД». 2. 2.1. Элементарные алгоритмы Последовательность В программировании особое значение имеют три элементар- ных алгоритма: последовательность, выбор и повторение. Последовательностью называется такой алгоритм, в котором его отдельные части (инструкции) выполняются последовательно одна за другой. В качестве примера рассмотрим алгоритм заварки чая. и Вскипятить воду Ополоснуть чайник кипятком Положить в чайник чай Залить чай кипятком
224 Приложение Очевидно, что результат выполнения алгоритма зависит от порядка следования его частей. Изменение этого порядка может плачевно сказаться на качестве напитка. Общий вид алгоритма повторения следующий инструкция инструкция инструкция О находящихся в скобках инструкциях говорят, что они вло- жены в последовательность. 2.2. Выбор Хотя последовательный алгоритм самый простой, а потому и са- мый понятный, далеко не все процессы можно описать в виде простой последовательности действий. Иногда приходится делать выбор меж- ду двумя возможными путями развития событий в зависимости от того, истинно или ложно некоторое условие, например, Если (погода хорошая) идти гулять иначе сидеть на занятиях В общем виде: и Если (условие) инструкция 1 иначе инструкция 2 При выполнении алгоритма сначала проверяется условие. Если условие истинно, выполняется первая из вложенных в алго- ритм инструкций, если условие ложно — вторая. Алгоритм выбора встроен во все процедурные языки програм- мирования. В С4-+ он выглядит так:
Введение в программирование 225 и________________ if (условие) инструкция 1 else инструкция 2 Особо выделен случай, когда вложенная инструкция выпол- няется только при истинном условии, а при ложном — ничего де- лать не надо. и_________________________;_________________________ if (условие) инструкция 1 2.3. Повторение Повторение определенных действий является необходимой частью большинства программ. Рассмотрим алгоритм утоления голода конфетами. и______________________________________________________ Пока (хочется есть) съесть одну конфету В этом алгоритме сначала проверяется условие, и если оно ис- тинно, выполняется вложенная в алгоритм инструкция. Затем сно- ва проверяется условие и так далее, пока очередная проверка не покажет, что условие ложно. На этом алгоритм заканчивается. Общий вид алгоритма повторения следующий: и______________________________________________________ Пока (условие) инструкция На язык C++ эта запись переводится слово в слово: U____________________________________________ while (условие) ин с тру кция 2.4. Комбинация элементарных алгоритмов Значение элементарных алгоритмов в том, что любая из мыс- лимых программ является либо последовательным алгоритмом, либо алгоритмом выбора, либо алгоритмом повторения. В роли
226 Приложение вложенных инструкций также могут оказаться последователь- ность, выбор или повторение. Одни инструкции вкладываются в другие, как матрешки, и глубина вложенности не ограничена. 2.5. Другие инструкции Вы видели, что кроме последовательности, выбора и повто- рения, в алгоритмах встречались и другие инструкции: «съесть конфету», «идти гулять», «ополоснуть чайник кипятком»; для каждого исполнителя определен собственный набор инструкций. От последовательности, выбора и повторения они отличаются тем, что не могут иметь вложенных инструкций и служить каркасом алгоритмической постройки. В начале изучения программирования мы будем использовать лишь несколько простых инструкций. Инструкция ввода: ввести число с клавиатуры в переменную х. у Инструкция вывода: вывести на экран значение переменной х. и _________________________ | cout << х; | Инструкция присваивания: вычислить арифметическое выра- жение Е и результат поместить в переменную х. U_______________________________________________________ I х = Е; I В арифметических выражениях допускаются операции: 4- сложения, - вычитания, * умножения, / деления, % получения остатка от деления (только для целых чисел). Например, значением выражения 5 % 3 является число 2, а значение выражения 12 % 4 равно нулю. Переменные Пока мы будем работать только с числами. Для хранения чи- сел в ходе выполнения программы используются переменные. Переменную можно представлять себе как участок памяти, в который
Введение в программирование 227 помещается ровно одно число — значение переменной. Каждая пере- менная имеет имя, которое отличает ее от других переменных и поз- воляет ссылаться в программе на ее значение. Имена переменным принято давать так, чтобы они напомина- ли о назначении переменной, например, х — горизонтальная коор- дината, у — вертикальная координата, max — наибольшее число, summa — сумма, counter— счетчик. Все целые переменные, необходимые для работы программы, должны быть объявлены специальной инструкцией iat, а все ве- щественные — инструкцией float, например, U_______________________________________________________ int х; float summa; В инструкциях объявления переменным можно придавать на- чальные значения. int х = 100; float summa ~ 0.0; Выражения Выражения составляются из чисел, переменных, арифмети- ческих операций и круглых скобок по тем правилам, которые все учили в школе. Вот примеры выражений. V_______________________________________________________ а + Ь; (D - R) * 3.14; х; 234 ; И переменная, и число являются частным случаем выражения. Условия Условия встречаются в алгоритмах выбора и повторения. Чаще всего они будут простыми сравнениями выражений: <, >, <=, ==, !== (два последних означают равенство и неравенство соответ- ственно). Допускаются и составные условия. Они составляются из про- стых при помощи союзов «И» и «ИЛИ», которые в языке C++
228 Приложение передаются операциями «&&» и «||» соответственно. Так же будем поступать и мы. Например, условие «число X больше 5 и меньше 3» записыва- ется в форме «X > 5 && X < 3». Запись «3 < X < 5» выглядит заманчиво, но на языке програм- мирования не является правильной! Можно приблизиться к ней, если переформулировать и переставить простые условия: «3<Х&&Х<5». При помощи составных условий можно выражать разные ма- тематические ограничения, например: абсолютная величина числа Z больше 5 U I Z > 5 I| z < -5 I неравенство треугольника со сторонами а, 6, с U___________________________________________________ |' а + b > с && а + с > b && b + с > а [ число X делится на 2, но не делится на 4 U___________________________________________________ | X % 2 0 && X % 4!- О | 2.6. Пример: ввод и сложение чисел Покажем пример алгоритма, который вводит числа с клавиа- туры и складывает их, пока не будет введено число 0. Накоплен- ная сумма выводится на экран. Для решения задачи нам понадобятся две переменные: одна для хранения накопленной суммы, назовем ее summa, другая — для приема числа, введенного с клавиатуры, назовем ее number, V { int summa - 0 ; int number; cin » number; while (number != 0) { sununa = suirnia + number; cin » number;
Введение в программирование 229 cout « surma; Алгоритм в целом является последовательностью, одна из ин- струкций последовательности — повторение, а в нее вложена еще одна последовательность. Отступы в записи алгоритма очень важ- ны, т.к. помогают понять, что куда вложено. Скобки { и }, заключающие в себе последовательность, за- писывают одну под другой, чтобы легко было сопоставить откры- вающую скобку закрывающей. Комментарии В программах разрешается писать пояснения к ним — коммен- тарии. Компьютер, исполняющий программу, игнорирует коммен- тарии, а человеку они могут пригодиться. Комментарии бывают однострочные U__________________________________________________ [ //"начинается с двух косых и длится до конца строки [ и многострочные и______________________________~ _________ /* В таких ограничителях комментарий может занимать сколько угодно строк. */ Вот как выглядит программа ввода и сложения чисел с пояс- нениями U_______________________________________________________ /* Ввод и сложение чисел */ { int summa = 0; // Сумма всех введенных чисел int number; // Очередное число cin » number; // Продолжать работу, пока не введут ноль while (number 0) { summa = summa + number; cin >> number; } cout << summa; }
230 1 )риложение Алгоритмический язык: перечень инструкций Составим сводную таблицу инструкций нашего языка. Инструкция Шаблон инструкции Последовательность инструкция инструкция } Выбор if (условие) инструкция else инструкция Повторение while (условие) инструкция Ввод cin > > переменная; Вывод cout« выражение; Присваивание переменная = выражение; Объявление int переменная; float переменная; Вы заметили, что для записи алгоритмов мы использовали язык, который является малым подмножеством С4-4-. Это сдела- но, чтобы алгоритмы можно было проверить на компьютере. Для превращения алгоритма в законченную программу перед ним достаточно дописать две строки, смысл которых мы поясним позднее. U___________;_____________________________________ #include <iostream.h> void main() 2.7. Задачи 1. Напишите алгоритм, который вводит два числа с клавиатуры и: а) выводит на экран большее из них; б) выводит на экран сначала меньшее, а потом большее. 2. Сделайте то же, но для трех чисел. 3. Напишите алгоритм, который вводит числа с клавиатуры и перемножает их, пока не будет введено число 0. Накоплен- ное произведение выведите на экран. За образец возьмите пример из раздела 2.6.
Введение в программирование 231 4. Из множества допустимых инструкций можно исключить выбор. Покажите это, выразив выбор через последователь- ность и повторение. Воспользуйтесь дополнительными пере- менными. 3. Разработка алгоритма 3.1. Пошаговая детализация алгоритма Математически доказано, что трех элементарных алгоритмов (последовательности, выбора и повторения) достаточно, чтобы сконструировать любой алгоритм, который способна выполнить ЭВМ. При этом среди вложенных инструкций элементарных алго- ритмов могут быть эти же самые алгоритмы. Это помогает ответить на мучительный вопрос, встающий перед каждым начинающим программистом: «С чего начать разработку алгоритма?». Во-первых, надо выбрать подходящую структуру для будуще- го алгоритма (последовательность, выбор или повторение). Во-вторых, заняться внутренними инструкциями выбранной структуры так, как если бы они были самостоятельными алгорит- мами, т.е. выбрать структуру, заняться внутренними инструкция- ми и т.д. Углубляться в это следует до тех пор, пока все внутренние ин- струкции не станут понятными исполнителю алгоритма. Такой метод называется пошаговой детализацией алгоритма или разра- боткой алгоритма сверху вниз. Перейдем от теории к практике и рассмотрим примеры разра- ботки алгоритмов. 3.2. Разработка алгоритма для вычисления синуса Задача. Ввести величину угла х, измеренную в радианах, и подсчитать синус х по формуле: sin - X - Х3/3! + Х5/5! - ... + (-1)"41 Хп * V(2n - 1)! Решение. Для реализации алгоритма нам понадобится как минимум две переменные: х — для хранения величины угла; sin — для хранения значения синуса.
232 Приложение Как правило, на верхнем уровне детализации алгоритм явля- ется последовательностью инструкций. U________________________________________________________ /* Вычисление синуса */ { float х; // Угол float sin; // Синус угла сin >> х; /* Подсчитать сумму sin */ cout << sin; } То, что вы видите, является последовательностью, но не со- всем, т.к. строка и___________________________________________________________ | 7*" подсчитать" сумму sin */ -~~j это не инструкция, а обозначение алгоритма, который должен нахо- диться в этом месте и который еще предстоит создать. Чтобы после- довательность инструкций уже сейчас можно было компилировать, название будущего алгоритма записано в виде комментария. Из предыдущего примера мы видели, что для подсчета суммы вначале очищают переменную, предназначенную для хранения суммы, а потом при помощи конструкции повторения накаплива- ют в ней слагаемые. В данной задаче накопление слагаемых будем продолжать, пока очередное слагаемое не станет достаточно малым, т.е. не перестанет заметно изменять сумму. Введем еще одну переменную piece — очередное слагаемое, которая поочередно должна принимать значения всех членов ряда. U______________________________________________________ /* Подсчитать сумму sin */ { sin - 0; // Очистить сумму float piece = х; // Получить первое слагаемое while (piece > 0.0001 I I piece < -0.0001) { sin = sin + piece; // Прибавить к сумме /* Вычислить следующее значение piece */ } }
Введение в программирование 233 Остается уточнить, каким образом можно вычислить следую- щее слагаемое, т.е. детализировать псевдоинструкцию /* Вычис- лить следующее значение piece */. Если присмотреться к формуле в условии задачи, то можно увидеть, что каждое следующее слагаемое получается из предыду- щего путем умножения его на (~х • х) и деления на i (i - 1), где i — число, стоящее под знаком факториала в знаменателе слагаемого. Для краткости назовем его номером слагаемого и смиримся с тем, что слагаемые будут иметь только нечетные номера. Введя такую нумерацию слагаемых, перепишем последний алгоритм. i______________________________________________________ /* Подсчитать сумму sin */ sin - 0; // Очистить сумму float piece - х; // Получить первое слагаемое int i - 0; while (piece > 0.0001 П piece < -0.0001) sin = sin + piece; // Прибавить к сумме i = i + 2; // Номер следующего слагаемого /* Вычислить следующее значение piece */ } } Наконец можно детализировать псевдоинструкцию «/* Вычис- лить следующее значение piece */» и__________________________________________________ । pieCe --piece * х * х / (i * (i + 1)); I Соберем все вместе и получим единый алгоритм. U__________________________________________________ /* Вычисление синуса */ { float х; float sin; cin » х; /* Подсчитать сумму sin */ sin - 0; // Очистить сумму float piece ~ х; // Получить первое слагаемое int i — 0; while (piece > 0.0001 II piece < -0.0001)
234 11риложение { sin - sin +• piece; // Прибавить к сумме i = i + 2; // Номер следующего слагаемого /* Вычислить следующее значение piece */ piece = - piece * х * х / (i * (i + 1) ) ; ) cout << sin; ) При сборке алгоритма мы не стали удалять названия подалго- ритмов, оставив им роль пояснений. 3.3. Разработка алгоритма подсчета простых чисел Для закрепления навыков разработки рассмотрим еще один пример. Задача. Определите количество простых чисел среди первой тысячи натуральных чисел. Простым является число, которое де- лится без остатка только на себя и на 1. Решение. Будем поочередно брать все числа от 2 до 1000, испытывать их «на простоту» и считать, сколько из них про- шли испытание. В первом приближении алгоритм можно запи- сать так: U_____________________________________________________ /* Подсчет простых чисел */ { int п - 2; // Значение первого числа int counter - 0; // Начальное значение счетчика while (п <- 1000) if ( /* число п является простым ★/ ) counter ~ counter + 1; n - n + 1; // Значение следующего числа } cout << counter; // Вывести значение счетчика } Как видно, условие простоты числа само не является простым и для его проверки может понадобиться нетривиальный алгоритм, который, в свою очередь, будет нуждаться в детализации.
Введение в программирование 235 Построим алгоритм, который по заданному значению п уста- новит переменную isPrime в 1, если п — простое число, или устано- вит isPrime в 0, если п — составное* После этого сравнение «isPrime ~~1>> можно использовать вместо условия «Число п является про- стым» . Перепишем алгоритм /* Подсчет простых чисел */ с учетом сделанного предложения. U_____________________ /* Подсчет простых чисел */ { int п ~ 2; // Значение первого числа int counter = 0; // Начальное значение счетчика while (п <- 1000) г I. int isPrime; /* Определить значение isPrime */ if (isPrime ~~ 1) counter ~ counter + 1; r. - ri j. // значение следующего числа } cout << counter; // Вывести значение счетчика Теперь детализируем алгоритм /* Определить значение isPrime */. Чтобы убедиться в простоте числа /г, будем находить остаток от деления п на все числа, меньшие, чем и. Операция взя- тия остатка от деления есть в нашем арсенале: п % d. U______________________________________________________ /* Определить значение isPrime */ { int d = 2; // Установить начальное значение делителя while (п % d != 0) d = d + 1; // Увеличить d на 1 if (d =~ n) isPrime = 1; /'/ Число простое else isPrime = 0; // Число составное }
236 Приложение Алгоритм основан на том, что для простого числа цикл за- вершиться только при достижении делителем d значения и, а для составного числа — раньше. Отметим, что для проверки числа на простоту достаточно пытаться делить его на все числа от 2 до корня квадратного из п потому, что в составном числе хотя бы один сомножитель не бу- дет превышать корня квадратного из п. Возможны и другие усо- вершенствования, которыми мы в данном примере пренебрегли ради простоты решения. Вероятно, вы заметили, что детализация алгоритмов в при- мерах не всегда шла без запинки. И в первой, и во второй зада- че приходилось делать «шаг на месте» и переписывать уже на- писанный алгоритм. Это, скорее, правило, чем исключение, более того, иногда нужно «пятиться» назад, чтобы найти хоро- шее решение. 3.4. Задачи 1. Завершите последний пример, собрав его части в одну про- грамму, и выполните ее на компьютере. 2. Разработайте алгоритм, который вводит числа с клавиатуры и перемножает их, пока не будет введено число 0. 3. Разработайте алгоритм подсчета суммы: 1 + 1/2 + 1/3 + ...+ +1/100. 4. Разработайте алгоритм, который вводит число п и печатает п первых степеней двойки: 1,2, 4, 8, 16, ... 5. Напишите программу, которая вводит четное числор, обозна- чающее общее количество лап гусей и кроликов, и определяет, сколько в этом коллективе гусей и сколько кроликов. Про- грамма должна напечатать все возможные ответы. 4. Алгоритмы поиска Всякий программист должен владеть определенным набором алгоритмов независимо от того, на каком языке он пишет свои программы. В следующих разделах мы познакомимся с просты- ми, но полезными алгоритмами. Начнем с алгоритмов поиска.
Введение в программирование 237 4.1. Поиск наибольшего среди вводимых чисел Задача. Ввести с клавиатуры 10 чисел, найти среди них наи- большее и вывести его на экран. Решение. Идея алгоритма в том, чтобы с момента ввода первого числа и до ввода последнего хранить в некоторой переменной max наибольшее из всех уже введенных чисел. Для этого самое первое число сразу введем в переменную max, а каждое следующее число, будем сравнивать с тем, что храниться в max. Если очередное вве- денное число больше того, что хранится в max, поместим его в max. и_______________________________________________________ /* Поиск наибольшего числа */ int шах; // Хранит наибольшее число cin » max; // Первое число ввести в max int counter = 1; // Одно число уже введено! while (counter 10) int next; // Очередное число cin >> next; if (next > max) max = next; // Обновить max counter - counter +1; // Считать все числа } cout << max; } Инварианты Возможно, кто-то задумается, всегда ли данный алгоритм най- дет самое большое число. Для доказательства корректности алго- ритмов используют понятие инварианта, т.е. некоторого утверж- дения, которое справедливо в определенных точках алгоритма. Рассмотрим утверждение: «В переменной max находится са- мое большое из всех введенных чисел». Вернемся к записи алго- ритма, просмотрим ее и подчеркнем все инструкции, перед выпол- нением которых это утверждение обязательно будет истинно (комментарии опущены). U int max;
238 Приложение cin » max; int counter ~ 1; while (counter <= IQ) { int next; cin >> next; if (next > max) max = next; counter - counter + 1; ) cout << max; Как видно, оно истинно и перед инструкцией вывода «cout « шах», а значит, алгоритм выведет самое большое число. Алгоритмический язык: массивы Есть программы, в которых нужно хранить десятки и сотни чисел одновременно. Такое количество отдельных переменных превратит программу в монстра, и здесь реше- нием проблемы являются массивы. Элементы массива можно трактовать как пронумерован- ные переменные. Все такие переменные носят одно имя, но имеют разные номера. В языке C++ номерами служат целые неотрицательные числа, например, элементы массива с име- нем А называются А[0], А[1], А[2]... Если в массиве всего п элементов, последний элемент обозначается А[п ”1]. Подобно простым переменным массивы бывают целые и ве- щественные, и их нужно объявлять в инструкциях int и float, U__________________________________________________ float А[100]; // Вещественный массив из 100 элементов int days[7]; // Целый массив из 7 элементов Сделаем несколько упражнений с массивами. 1. Объявить целый массив и заполнить нулями его элементы. U______________________________________________ int х[10 0] ; int i = 0;
Введение в программирование 239 while (i < 100) { x[i] - 0; i = i + 1 ; 2. Заполнить массив натуральными числами: 1, 2,3,100. U_____________________________________________ x[i] ~ i + 1; // программа та же, но вместо // ”x[ij = 0;“ стоит эта строка 3. Заполнить массив четными числами 0, 2, 4, ...,198. ___и__________________________________________ х [ .1 j - i * 2; // программа та же, но вместо // ”x[i] ~ 0;стоит эта строка 4. Объявить вещественный массив у из 6 элементов и за- полнить его числами, введенными с клавиатуры. ___и_______________________________________________ float у[6]; int i - 0; while (i < 6) { сin >> yfij; i - i + 1 ; } 5. Просуммировать числа из массива у и вывести сумму на экран. и_______________________________________________ float у[6] - {2, 4, 3, 5, 8, 1); float summa - 0; int i -- 0 ; while (i < 6) { summa - summa + y[i]; i -- i + 1 ; cout summa;
240 Приложение В последнем примере начальные значения массива у были заданы в инструкции объявления. Это можно сделать при помощи списка инициализации в фигурных скобках, и________________________________________________ [float у[б] = {2, 4, 3, 5,~8, 1}; 4.2. Поиск наибольшего числа в массиве Задача. Задан целый массив М из 10 элементов: М[0],М[1], М[9]. Найти набольшее число в массиве и его номер. Решение. В основу решения положим алгоритм поиска мак- симального среди чисел, вводимых с клавиатуры. Отличие в том, что мы будем не вводить числа с клавиатуры, а брать их из элемен- тов массива М. Счетчик counter послужит нам для выделения эле- мента массива. U_______________________________________________________ /* Поиск наибольшего числа в массиве */ { // Объявление и заполнение массива int М[10] - {2, 4, 20, 12, 1, 3, 14, 16, 7, 5}; int max ~ М[0]; //В max - начальный элемент int counter = 1; // Номер очередного элемента while (counter < 10) { if (М[counter] > max) { // Обновить самое большое число max ~ М[counter]; } // Изменить номер очередного элемента counter = counter + 1; } cout << max; } Этот алгоритм получен из базового при помощи минимальных переделок, но он не отвечает на вопрос, где находится наибольшее число в массиве. Попробуем сохранять в переменной max не наи- большее значение, а его номер, т.е. номер того элемента массива, который это значение содержит. Если в шах находится номер эле- мента, то содержимое элемента всегда доступно как М[тах].
Введение в программирование 241 и /* Поиск наибольшего г числа в массиве, версия 2 */ // Объявление и заполнение int М[10] = {2, 4, 20, 12, массива 1, 3, 14, 16, 7, 5); int max - 0; // В max - номер начального элемента int counter = i; // Номер очередного элемента while (counter < 10) { if (М[counter] > M[max]) { // Обновить самое большое число max ~ counter; } // Изменить номер очередного элемента counter = counter + 1; } cout « max; // Вывести номер максимального числа cout << M[max]; // Вывести максимальное число } Как видим, алгоритм почти не изменился, но стал точно отве- чать на вопрос задачи. 4.3. Поиск заданного числа в массиве В практике компьютерной обработки данных часто необходи- мо найти заданное число среди множества хранимых в массиве чи- сел. Иными словами, нужно решить следующую задачу. Задача. Есть массив из п чисел: М[0], М[1], М[2],М[п - 1] и некоторое число х. Найти номер того элемента массива, который содержит число х, или убедиться, что такого элемента в массиве нет. Решение. Поскольку числа в массиве не упорядочены, нам не остается ничего, как перебирать последовательно все элементы мас- сива М и сравнивать их с искомым числом х. Для перебора элемен- тов массива используем переменную I. Сравнение с х будем делать в цикле, который выполняется, пока истинны два условия: а) номер i очередного элемента не превысил п; б) очередной элемент Лф] не равен х. Решим задачу поиска для п = 10. Перед началом выполнения алгоритма в массиве М будут находиться числа, среди которых ве- дется поиск, а в переменной х — искомое число.
242 Приложение и______________________________________________________ /* Поиск заданного числа в массиве */ { // Объявление и заполнение массива int М[10] - {2, 4, 20, 12, 1, 3, 14, 16, 7, 5} ; int х - 14; // Искомое число int i = 0; // Номер очередного элемента массива while ((i < 10) && (M[i] х)) i - 1 + 1; // Перейти к следующему элементу cout << i; // Вывести результат поиска } Если алгоритм найдет число, он выведет его номер, если не найдет, выведет число 10 — размер массива. Размер массива не может быть номером никакого элемента массива, поэтому вполне подходит на роль индикатора неудачи поиска. Алгоритмический язык: константы В предыдущем примере мы трижды сталкивались с чис- лом 10: а) при объявлении массива — int М[10]; б) при организации цикла — while ((i < 10)...; в) при анализе вывода — если напечатано 10, число не найдено. Для массивов типична ситуация, когда их размер много раз встречается в программе. Если потребуется изменить раз- мер массива, мы должны будем сделать изменения в разных местах программы, а это чревато ошибками, т.к. можно что- то упустить. Кроме того, число 10 безлико и ничего не гово- рит тому, кто читает программу и пытается ее понять. Напрашивается следующее решение: объявить целую пе- ременную, например, size, присвоить ей значение 10, и далее использовать ее в инструкциях программы: и___________________________________________________ int size ~ 10; int М I size] - . . . ; while ((i < size) && (M[iJ x)).
Введение в программирование 243 Препятствие в том, что размер массива в инструкции объяв- ления не может быть переменным, для этой цели пригодны лишь постоянные значения. На наше счастье, в языке C++ мож- но объявить не только переменные, но и «постоянные». Постоянная величина или константа объявляется как переменная, но со словом const и всегда получает значение при объявлении (инициализируется). и_____________________________________________________ const int SIZE = 10; Значение константы нельзя менять в ходе выполнения программы. Инструкцию, подобную SIZE = 15, не пропус- тит компилятор. Чтобы константы были заметнее в программе, их имена принято записывать прописными буквами, хотя язык C++ этого не требует. Теперь алгоритм поиска заданного числа будет выглядеть так. и___________________________________________________ Поиск заданного числа в массиве */ const int SIZE = 10; // Объявление и заполнение массива int М[10] = {2, 4, 20, 12, 1, 3, 14, 16, 7, 5}; int х - 14; // Искомое число int i = 0; // Номер очередного элемента массива while ((i < SIZE) && (M[i] 1= x)) 1=1^1; // Перейти к следующему элементу cout « i; // Вывести результат поиска 4.4. Поиск с порогом Процедура поиска выполняется столь часто, что ее стараются сделать предельно быстрой. В предыдущем алгоритме на каждом шаге цикла выполнялось три элементарных операции — два сравнения и одно сложение. Сейчас мы увидим алгоритм, в котором тот же ре- зультат достигается всего за две операции, т.е. в 1,5 раза быстрее! В этом алгоритме мы воспользуемся для хранения п чисел мас- сивом из (л + 1) элементов и в последний элемент заранее поместим
244 1 филожение искомое число х. Теперь в условии повторения цикла достаточно про- верять лишь совпадение M[i] сх, т.к. число х обязательно будет найде- но в массиве. По окончании цикла остается только уточнить, где имен- но оно было найдено. Если в последнем элементе массива, то это не настоящее х, а искусственное и первоначально его в массиве не было. Этот алгоритм носит название поиска с порогом, т.к. мы ста- вим в массиве тот порог, о который обязательно споткнется алго- ритм повторения. В записи алгоритма мы опустили строки, V_____________________________________________________ const int n = 10; int M[n] - {2, 4, 20, 12, 1, 3, 14, 16, 7, 0); int x = 14; которые не относятся к процедуре поиска, а нужны лишь для про- верки алгоритма на компьютере. Алгоритмический язык: функции Чтобы выделить алгоритм решения какой-нибудь зада- чи из его окружения, в языке С+4- применяют функции. За- дача функции — получить данные, выполнить над ними ка- кие-то действия и вернуть результат в ту программу, которая вызвала функцию. Функция имеет заголовок и тело. В заголовке задают тип результата, имя функции и список параметров — данных, ко- торые функция получает из вызывающей ее программы. Для поиска с порогом тип результата — целый, подходящим име- нем может быть «BoundarySearch », а параметрами являются: М — массив, п — размер массива, х — искомое число.
Введение в программирование 245 Вот как выглядит такой заголовок. U__________________________________________ int BoundarySearch (int M[], int n, int x); Перед каждым параметром указывают его тип, а между параметрами ставят запятую. Если параметр — массив, за его именем стоят квадратные скобки. Телом функции является алгоритм в уже привычном нам виде. Вот как выглядит функция целиком. U_________________________________________________ /* Поиск с порогом */ int BoundarySearch (int M[], int n, int x) { M[n - 1] = x; // Ставим порог int i ~ 0; while (M[i] != x) i ~ i + 1 ; return i; } В связи с появлением функций список доступных инст- рукций пополняется инструкцией return, которая вычисля- ет стоящее в ней выражение и возвращает результат той про- грамме, которая вызвала функцию. Согласитесь, что распоряжаться найденным значением должен не алгоритм поиска, а тот алгоритм, который пору- чил ему эту работу, поэтому мы изъяли из алгоритма поиска инструкцию вывода и заменили ее инструкцией возврата. Посмотрим, как теперь выглядит программа проверки функции Boundary Search. и__________________________________________________ void main() { const int N = 10; int m[N] = {2,4,20,12,1,3,14,16,7,0}; int x = 14; // Вызов функции поиска с порогом int i = BoundarySearch(m, N, x); cout « i; }
246 11риложение Две последние строки можно заменить одной U_____________________________________ cout << BoundarySearch(m, N, x) ; т.к. хранить найденное значение в данной программе незачем. Функция main() Теперь, когда мы познакомились с функциями, можно сказать, что любая программа на C++ содержит функцию с именем main( ), с которой всегда начинается выполнение программы. Все другие функции, если они есть, прямо или косвенно вызываются из функции main(). Несмотря на остроумие поиска с порогом, скорость его не дос- таточна для работы с большими массивами данных. Если числа рас- положены в массиве в порядке возрастания (или убывания), мож- но предложить значительно более быстрый способ поиска. 4.5. Двоичный поиск Разделим область поиска (первоначально это весь массив) по- полам и сравним искомое число х с числом из середины области. Если число из середины больше, чем х, то дальше искать надо только в первой половине массива, если меньше — только во вто- рой, а если равно, значит число уже найдено и поиск нужно пре- кратить. Оформим эту идею в виде функции Binary Search () — двоичного поиска. Поскольку функция решает ту же задачу, что nBoundarySearch( ), параметры ее точно такие же: М — массив, п — размер массива, х — искомое число. Для выполнения двоичного поиска нужны следующие пере- менные: left — левая граница поиска; right — правая граница по- иска; middle — середина области поиска. U______________________________________________________ /* Двоичный поиск */ int BinarySearch (int M[], int n, int x) int left - 0; int right - n - 1;
Введение в программирование 247 // Искать, пока в области поиска есть хоть один // элемент while (left <= right) { // Середина отрезка int middle (left + right) / 2; if (x < M [middle]) // Оставить для поиска левую половину right - middle - 1; if (x > M[middle]) // Оставить для поиска правую половину left = middle + 1; if (x Mfmiddle]) return middle; // Ура, нашли! } // Вернуть n в знак того, что числа х в массиве нет return п; } Программа, которая готовит массив и вызывает функцию BinarySearch(), м&лъ отличается от той, что проверяла функцию BoundarySearchf ). и______________________________________________________ void main () <. const int n = 10; // Числа в массиве упорядочены int M[n] = {1,2,3,4,5,7,12,14,16,20}; int X = 14; // Вызов функции двоичного поиска int i - BinarySearch(М, n, х) ; cout << i; Сложность двоичного поиска В процессе выполнения цикла область поиска уменьшается вдвое на каждом шаге цикла, пока не станет пустой или пока чис- ло х не будет найдено. Интересно ответить на вопрос, сколько раз может повториться цикл в данном алгоритме? Иными словами, какова продолжительность поиска в худшем случае?
248 Приложение Если в массиве содержится п чисел, то делить область поиска пополам можно не более, чем log2 п раз. Это позволяет оценить вре- менную сложность алгоритма как Т = const • log2n, где const — константа, не зависящая от п. Для сравнения заметим, что оценка временной сложности любого из алгоритмов последовательного поиска Т = const • п, т.е. линейная функция от п. По этой причине последовательный по- иск иногда называют линейным, а двоичный — логарифмическим. 4.6. Задачи 1. Как изменить алгоритм поиска наибольшего из введенных чисел, чтобы он находил наименьшее число? 2. Подсчитайте количество наименьших чисел в массиве. 3. Какова временная сложность алгоритма поиска наибольшего числа в массиве? 4. Как применить поиск с порогом, если в массиве нет свободных элементов? 5. Проверьте, есть ли в массиве два одинаковых числа. 6. Задан массив из п элементов. Измените порядок чисел в мас- сиве на обратный. 7. Задан массив с нулевыми и ненулевыми элементами. Сдвинь- те все ненулевые элементы влево так, чтобы они располагались там плотно. Так же плотно справа должны располагаться ну- левые элементы. Например, дано: {0, 4, 3, 0, 7}, должно полу- читься: {4, 3, 7, О, О,}. 8. Задан массив с положительными и отрицательными числами. Расположите все отрицательные числа левее положительных. Например, дано: {2, -4, 3, -3,1, 7, -5}, должно получиться: {- 4,-3,—5, 2,3, 1, 7}.
Введение в программирование 249 5. Алгоритмы сортировки Эффективность двоичного поиска убедительно свидетельству- ет в пользу порядка, хотя бы в массивах. В этом разделе мы рас- смотрим несколько алгоритмов упорядочения массивов. 5.1. Обменная сортировка Задан массив из п чисел, например: 50 40 10 60 30 20. Пройдем вдоль массива, сравнивая на каждом шаге пару сосед- них чисел: первое со вторым, второе с третьим и т.д. Если левое число пары окажется больше правого, поменяем их местами. Получим: 40 10 50 30 20 60. Полного порядка пока не получилось, но самое большое число в массиве попало на свое законное место. Проделаем то же самое еще раз: 10 40 30 20 5060. Теперь уже два числа в массиве: 50 и 60 занимают свои места. Нетрудно понять, что после (и - 1)-го прохождения, а, возможно, и раньше, все числа в массиве встанут на свои места, и он будет пол- ностью упорядочен: 10 30 20 40 50 60. 10 20 30 40 50 60. Запишем этот алгоритм в виде функции ChangeSort — обмен- ная сортировка, с двумя параметрами: М — массив и п — размер массива. Заголовок функции будет таким и___________________________________________________ void ChangeSort(int М[], int n) Слово void в заголовке функции говорит о том, что никакого значения функция ChangeSort в вызывающую программу не воз- вращает. Ее работа состоит в перегруппировке чисел в массиве М. и______________________________________________________ /* Обменная сортировка */ void ChangeSort (int М[], int n) // Установить в 1 счетчик прохождений
250 Приложение int Running = 1; while (Running < n - 1) { /* Пройти массив, сравнивая соседние числа */ Running = Running + 1; } ) Детализируем псевдоинструкцию, /* Пройти массив, сравнивая соседние числа */, реализовав ее в виде функции с заголовком void Run(int М[], int len). Параметр len означает не размер массива, как в функции ChangeSort( ), а длину той его части, которую нужно проходить, и_________________________________________________________ /* Пройти массив, сравнивая соседние числа */ void Run(int М[], int len) { // Переменная i нужна, чтобы пройти массив int i ~ 0; while (i < (len) { if (M[ij > M[i + 1]) /* Поменять местами M[i] и M[i + 1] */ i = i + 1; ) } Поменять местами два элемента массива и вообще значение двух любых переменных можно в три присваивания. При обмене значений третья переменная играет роль перевалочного пункта. V { int х = M[i]; М [ i ] - M[i + 11; M [ i -ь 1 ] “ x; ) Встроим этот алгоритм в функцию Run( ) и получим ее окон- чательный вариант.
Введение в программирование 251 и /* Пройти массив, сравнивая соседние числа */ void Run (int X [j , int len) int i ~ 0; while (i < len) if (Mil] > Mil - 1]) i int x M [ i 1 ; M [ i J - M[i + 11; M [ i + 1 ] = x; } i i + 1; } 1 Мы закончили работу над функцией Run(), но еще не закон- чили над функцией ChangeSort(), Заменим псевдоинструкцию /* Пройти массив, сравнивая соседние числа */ инструкцией вы- зова функции Run( ). Передадим ей массив М и длину прохода п — running, которая тем меньше, чем больше счетчик прохожде- ний running. U___________________________________________________ /* Обменная сортировка */ void ChangeSort (int М[], int n) •i // Установить в 1 счетчик прохождений int running - 1; while (running < n - 1) // Длина прохода уменьшается с каждым // прохождением Run(M, n - running); // Увеличить на 1 счетчик прохождений running = running -t 1 ; } Для проверки работы алгоритма напишем главную програм- му, которая:
252 11риложение 1) подготовит массив; 2) вызовет функцию сортировки; 3) выведет упорядоченный массив на экран. U_________________________________________________ void main() { // Подготовить массив const int n - 10; int M[n] = {2,4,20,12, 1, 3, 14, 16, 7, 5}; // Вызвать функцию сортировки ChangeSort (М, n) ; // Вывести элементы массива для проверки int i = 0; while (i < n) { cout << M[i]; i = i + 1 ; } } Алгоритмический язык: форматирование вывода Главная программа из предыдущего примера выведет элементы массива в таком виде: 12345712141620. Такой вывод невозможно прочесть. К счастью, помимо чисел инструкция вывода способна выводить строки — по- следовательности любых символов, заключенные в двойные кавычки. Например, инструкция cout << “Hello!”; выведет на экран слово Hello! без кавычек. Чередуя вывод чисел и вывод строк, можно придать ин- формации на экране нужный вид. Например, еще одна инст- рукция вывода в цикле и_________________________________________________ while (i < n) { cout << M[i];
Введение в программирование 253 cout << " ”; // Разделитель между числами i - 1 -ь 1; } сделает результат вывода читабельным: 12 34 5 7 12 14 1620. Если несколько инструкций вывода следуют подряд, их можно заменить одной, например, вместо двух инструкций и_______________ cout << M[i]; cout << " ” ; можно написать одну U_________________________________________________ | cout << M[i] << ” Каждая инструкция вывода начинает располагать данные на экране с того места, где закончила вывод пре- дыдущая инструкция. Иными словами, данные на экране появляются в текущей позиции курсора. Принудительно перевести курсор в начало следующей строки можно, вы- ведя специальную константу endl (сокращение от tend of line»). U_________________________________________________ cout << endl; Например, пожелав вывести содержимое массива в стол- бик, мы должны написать: U_____________________________________________________ while (1 < n) { cout << M[i] << endl; i = i + 1; } 5.2. Сортировка слиянием Сортировка слиянием — это более быстрый способ сортиров- ки, чем обменная. Чтобы лучше понять смысл сортировки слияни- ем, отдельно рассмотрим алгоритм слияния.
254 Приложение Задача. Даны два упорядоченных массива А и В. Слить их в третий массив С, тоже упорядоченный. Решение. Сравним первые элементы сливаемых массивов: А[0] и В[0]. Меньший из них поместим в начало массива С и «вы- черкнем» из исходного массива. Снова сравним два первых, еще не вычеркнутых элемента массивов А и В, меньший перепишем в С и вычеркнем и т.д., пока один из исходных массивов не исчер- пается. Тогда перепишем в С остаток другого массива, и слияние закончено. Для простоты условимся, что размер сливаемых массивов А и В одинаков и равен п, размер массива С соответственно равен 2п. Более точно все это можно записать в виде следующего ал- горитма, и_____________________________________________________ /* Слияние упорядоченных массивов */ void Merge(int А[] , int В [ ] , int С[]f int п) { int ia = 0; // Первый не вычеркнутый элемент из А int ib = 0; // Первый не вычеркнутый элемент из В int ic - 0; // Очередной элемент из С while (ia < и && ib < п) { // В обоих массивах еще есть элементы if (A[ia] < B[ib]) { С [ic] = A[ia]; ia - ia + 1; } else { C [ic'l - B[ib] ; ib ~ ib + 1; } ic = ic + 1; } if (ia n) // Если элементы массива А исчерпаны while (ib < n) // переписать в С остаток в { C[ic] -• В [ ib] ;
Введение в программирование 255 ib -- ib + 1; i с - i с -h x • } if (ib == n) // Если элементы массива В исчерпаны while (ia < n) / / переписать в С остаток А { C l i c ] = A[iaj; i a - i a + 1; i c - i c + 1; } Массив С будет полностью упорядочен, т.к. числа, которые попали туда раньше, меньше тех, что попали туда позже. Слияние выполнено за один просмотр исходных массивов, по- этому затраченное время можно оценить функцией Т = const ‘ п, где п — общее количество элементов в массивах А и В. Теперь вернемся к сортировке слиянием. Даже в неупорядо- ченном массиве можно обнаружить упорядоченные участки. Оче- видна истинность того, что любой массив состоит из упорядочен- ных участков единичной длины. Алгоритм сортировки слиянием следующий: 1. Разобьем сортируемый массив на две половины — А1 и В1, каждую из которых станем рассматривать как массив, состо- ящий из упорядоченных участков единичной длины. 2. Соединим их, применяя алгоритм слияния к упорядоченным участкам; один участок пары будем брать из массива А1, а другой — из массива В1. Результаты слияния каждой пары участков будем заносить попеременно в два новых массива — А2 и В2. В новых массивах упорядоченные участки будут иметь длину, равную сумме длин исходных участков, т.е. 2. 3. Сольем теперь упорядоченные участки массивов А2 и В2, по- переменно записывая результаты в освободившиеся массивы А1 и В1. Длина упорядоченных участков снова удвоится и ста- нет равной 4. 4. Будем повторять слияние массивов, каждый раз меняя роля- ми пары массивов (А1, Б1) и (А2, В2), пока длина упорядочен- ного участка не сравняется с длиной исходного массива. В этот момент сортировку можно считать законченной.
256 Приложение Запишем алгоритм в виде функции MergeSort. и__________________________ /* Сортировка слиянием */ void MergeSort (int М[], int n) { /* Разбить массив М на две половины: А1 и В1 */ // D - длина упорядоченного участка, вначале ~ 1 int D - 1; while (D < n) { /* Слить массивы Al и Bl в массивы А2 и В2 */ // Удвоить длину упорядоченного участка D = D * 2; /* Поменять ролями пары массивов */ } } Алгоритм сортировки слиянием не детализирован здесь до той степени, чтобы выполнить его на компьютере. Его полную детали- зацию и проверку вы должны сделать самостоятельно. 5.3. Сравнение двух алгоритмов сортировки Мы познакомились с двумя алгоритмами сортировки: обменной и слиянием. Каковы преимущества и недостатки каждого из них? Обменная сортировка, несомненно, проще. К тому же она не нуждается в дополнительной памяти — кроме сортируемого мас- сива требуется лишь несколько переменных, и их количество не зависит от объема сортируемых данных. Сортировка же слиянием требует удвоенного по сравнению с исходными данными объема памяти — для входных и для выходных массивов. Оценим теперь время сортировки, а значит и быстродействие реализующих эти алгоритмы программ. Пусть исходный массив состоит из п элементов. Обменная сортировка требует п -1 просмот- ров массива. Первый просмотр охватывает массив целиком, вто- рой — на 1 элемент меньше, третий — на два и т.д., в среднем про- смотру подвергается половина массива или п / 2 элементов. Время, затраченное на один просмотр, можно оценить как = const t • п.
Введение в программирование 257 Полное время обменной сортировки Тобмен = Т (П~1) = COIlSt • 71 (71 ~ 1). Временные затраты алгоритмов имеет смысл обсуждать толь- ко для больших п, поэтому слагаемыми с младшими степенями п можно пренебречь. Т * - Т. (п - 1) = const х * п2. обмен 1 v 7 обмен Теперь проанализируем сортировку слиянием. Она тоже состо- ит из отдельных фаз, в которых входные массивы сливаются и об- разуют выходные массивы. Время выполнения одной фазы можно оценить как Т2 = const2 • п, потому, что слияние требует однократного просмотра всех входных данных, а объем сливаемых данных от фазы к фазе не меняется и со- ставляет ровно п элементов. Константа в формуле характеризует количество операций в алгоритме слияния и не зависит от /г. Остается оценить, сколько фаз потребуется для полной сорти- ровки массива. Первоначальная длина упорядоченных отрезков считается равной 1, и каждая фаза сортировки эту длину удваивает. Сколько нужно удвоений, чтобы довести длину отрезка от 1 до л, иными словами, какова должна быть степень числа 2, чтобы 2х превысило л? Ответ: ближайшее сверху целое к двоичному лога- рифму от л. Итак, временная сложность сортировки слиянием Т = const -n-lo^n. слияния слияния ° 2 Чтобы лучше понять разницу между временной сложностью двух алгоритмов, обратимся к примеру. Пусть на каком-то компьютере два алгоритма сортируют массив из 1 тыс. элементов за одинаковое время, например, за 1 с. Что будет, если взять для сортировки массив не из тысячи, а из миллиона элементов? Легко посчитать, что время сортировки слиянием увеличится в 2000 раз и составит примерно 16 мин. Время же обменной сортировки уве- личится в 1 млн. раз, что составит около 280 ч. После такого рас- чета не стоит и пытаться упорядочивать большие массивы при помощи обменной сортировки, но для небольших массивов ее при- менение оправдано.
258 Приложение 5.4. Задачи 1. Что нужно изменить в алгоритме обменной сортировки, чтобы первыми занимали свои места не большие числа, а маленькие? 2. Улучшите алгоритм обменной сортировки так, чтобы работа прекращалась, если прохождение массива не вызвало ни од- ного обмена. Скажется ли улучшение на временной сложнос- ти алгоритма? 3. Разработайте алгоритм слияния массивов А и В с упорядочен- ными участками длины d. 4. Разработайте алгоритм слияния двух массивов с упорядочен- ными участками произвольной длины. Как определить конец упорядоченного участка? 5. Изменится ли временная сложность алгоритма слияния, если сливать не по два, а по три массива? 6. Рекурсивные алгоритмы 6.1. Простая рекурсия В процессе детализации алгоритма может оказаться, что какая- то его часть сводится к выполнению этого же алгоритма, но над мень- шим объемом данных. Такой алгоритм называется рекурсивным. Вспомним алгоритм двоичного поиска. 1. Разделить область поиска (первоначально это весь массив) попо- лам и сравнить искомое число х с числом из середины области. 2. Если число из середины больше, чем х, то выполнить двоич- ный поиск в первой половине массива. 3. Если число из середины меньше, чем х, то выполнить двоич- ный поиск во второй половине массива. 4. Если число из середины равно х, поиск нужно прекратить. Предположим, мы решили запрограммировать его в форме функции, и______________________________________________________ | int Binary Search (int M[] , int: x, int left, int right) ~] которой передается весь массив М, искомое число х, левая left и правая right границы области поиска. Областью поиска считаем часть массива, которая начинается с элемента left и заканчивается
Введение в программирование 259 элементом right. Чтобы найти место i числа х в массиве М из п эле- ментов, надо написать такую инструкцию: и_____________________________________________________ [~int i - BinarySearch (М, х, 0, n - 1) ;_____________^0 Запишем алгоритм функции BinarySearchf) в рекурсивной форме. и_________________________________________________________ int Binarysearch (int М[], int xz int left, int right) { if (left >= right) // Если область поиска пуста return -1; // возвращаем -1 в знак того, // что числа х в ней нет int middle ~ (left + right) / 2; if (x M [middle 1 ) // Число найдено return middle; if (x < M[middle]) //Ищем слева return BinarySearch(M, x, left, middle-1); else //Ищем справа return BinarySearch(M, x, middle+1, right); } Сравнивая прежнюю и новую форму алгоритма, мы замечаем, что в рекурсивном алгоритме нет оператора цикла. Рекурсию мож- но рассматривать как альтернативу инструкции цикла, т.е. как еще один способ организовать повторение в программе. Не совсем точно, но наглядно работу рекурсивного алгоритма можно изобразить в виде последовательного запуска все новых и но- вых копий одного и того же алгоритма. На самом деле копируются только данные (параметры и внутренние переменные функции), но не программный код.
260 Приложение Подобно бесконечным циклам, при неправильном написании программы возможны бесконечные самовызовы алгоритма. В на- шем примере этого не происходит, так как область поиска с каж- дым вызовом сокращается не менее чем вдвое, а это не может про- должаться бесконечно. Количество запущенных и работающих «копий» алгоритма называется глубиной рекурсии. Для алгоритма двоичного поиска максимальная глубина рекурсии равна логарифму двоичному от п. Придумывая рекурсивные алгоритмы, надо следить, чтобы глуби- на рекурсии не была большой, поскольку для выполнения таких алгоритмов понадобится много памяти. Мы рассмотрели самый простой вид рекурсии, когда в процес- се своего выполнения алгоритм вызывает себя ровно один раз (хотя в алгоритме BinarySearch( 7 два оператора процедуры, выполняет- ся всегда лишь один из них). Такая простая рекурсия легко заме- няется инструкцией повторения, и нам она понадобилась лишь для знакомства с этим приемом программирования. 6.2. Ханойские башни Программистам давно известна следующая задача. Задача. Имеется три стержня: 1-й, 2-й и 3-й. На первом стержне находится пирамида из п дисков, внизу — самый боль- шой, на нем — меньший, затем еще меньший и т.д., наверху — самый маленький. Требуется переместить все диски с 1-го стержня на 3-й, перекладывая их по одному таким образом, чтобы ни на каком стержне меньший диск не оказался ниже большего. Говоря точ- нее, нужно указать необходимую последовательность переме- щений дисков. Решение. Искомый алгоритм должен переместить пирамиду с первого стержня на третий, соблюдая известные из условия пра- вила. Поскольку алгоритм будет рекурсивным, нужно заранее по- заботиться об имени функции и параметрах. Назовем функцию Напоу, а параметрами сделаем три числа: п — количество дисков в пирамиде, sour — номер стержня-источника и dest — номер стержня-приемника. и______________________________________________________ Г void Hanoy (int n, int sour, int dest); I
Введение в программирование 261 Чтобы переместить пирамиду из п дисков со стержня sour на стержень dest? используя в качестве промежуточного третий стер- жень middle, достаточно сделать следующее: а) переместить пирамиду из п -1 верхних дисков с sour на middle', б) перенести один оставшийся диск с sour на dest*, в) переместить пирамиду из п - 1 дисков с middle на dest. Разумеется, все эти действия имеют смысл, если п > 0. Что делать, когда п == 0? Ответ: ничего; для переноса пирамиды из 0 дисков ничего перекладывать не нужно. Поскольку мы говорили о перекладывании дисков не с перво- го стержня на третий, а со стержня sour на стержень dest, не лиш- ним будет уметь вычислять номер промежуточного стержня. Для этого годится простая формула: middle = 6 ~ sour - dest. И наконец, как заставить компьютер выполнить пункт б), т.е. переложить один диск с sour на dest? Компьютер — не робот и сам перекладывать диски не будет. Ему достаточно вывести на экран или принтер пару чисел: номер стержня-источника и номер стержня-приемника. Последователь- ность таких пар, как запись шахматной партии, покажет порядок перекладывания дисков. Теперь можно записать решение, и /* VO Ханойские башни*/ sour, int dest) id Hanoy (int n, int if (n > 0) { int middle ~ 6 - sour - dest; Hanoy(n - 1, sour , middle); cout: << sour << ’’ " << dest; Hanoy (n -- 1, middle, dest); } } Чтобы переложить п дисков с первого стержня на третий, в главной программе надо выполнить инструкцию вызова функции, и________________________________________________________ | Напоу (п,~1, 3); |
262 11риложение 6.3. Быстрая обменная сортировка В предыдущем разделе мы рассмотрели два алгоритма сорти- ровки: обменной и слиянием. Один из них работал быстро, а дру- гой — экономно, т.е. не требовал дополнительной памяти помимо сортируемого массива. К. Хоор (С. Ноаге) предложил алгоритм, который работает так же быстро, как сортировка слиянием, и так же экономно, как об- менная сортировка. Он получил широкое распространение и был назван быстрой обменной сортировкой или сортировкой Хоора. Суть алгоритма в том, что мы наудачу выбираем одно из чи- сел сортируемого массива и назначаем его разделителем. Затем перестраиваем элементы массива так, что все числа меньше раз- делителя располагаются до него, а все числа больше разделите- ля — после. Этот процесс называется частичной сортировкой, и мы увидим, что ее можно выполнить за один просмотр массива. Далее таким же образом сортируем сначала первую, а потом вто- рую часть массива. Теперь займемся частичной сортировкой. Пусть задан массив М: 40 80 30 50 60 10 20. Назначим какое-нибудь число разделителем, например, первое. Будем двигаться от начала массива к центру, пока не найдем число больше разделителя, это — 80. Будем двигаться от конца массива к центру, пока не найдем число меньше разделителя, это — 20: 40180 30 50 601020|. Поменяем их местами 40|20 30 50 60 10 801 и продолжим движение 40 20 30,50 60 10180. Будем так поступать, 40 20 30,10 60 50(80 пока левая отметка не зайдет за правую, это послужит сигналом к остановке. 40 20 30 10(160 50 80 Остается поменять число над бывшей правой отметкой (она теперь стоит левее) местами с разделителем 10 20 30 4060 50 80 и частичная сортировка закончена. Оформим этот алгоритм в виде функции с именем PartSort и параметрами: М — сортируемый массив, left — левая граница области сортировки, right — правая граница области сортировки.
Введение в программирование 263 Возвращать функция будет место числа-разделителя, которое оно займет по окончании частичной сортировки. и_______________________________________________________ /* Частичная сортировка */ int PartSort (int М[], int left, int right) t // Выбрать в качестве разделителя самое левое // число int separ ~ left; while (left <- right) { // Сдвинуть левую отметку while (M[left] <= M[separ]) left - left + 1; // Сдвинуть правую отметку while (Mfright] >- Mfsepar]) right = right - 1; if (left < right) // Совершить обмен Mfleft] <=> M[right]; } // Совершить обмен M[separ] <=> M[right]; return right; } Вы, конечно, заметили «незаконную» операцию «<=>» в на- шем алгоритме. Мы использовали ее как условное обозначение для трех присваиваний, которые нужно выполнить, чтобы две перемен- ные обменялись значениями. Теперь напишем рекурсивный алгоритм сортировки Хоора. В качестве параметров зададим массив и границы сортируемой об- ласти. U______________________________________________________ /* Сортировка Хоора*/ void HoareSort (int М[], int left, int right) if (left <= right) // Если область не пуста { // Выполнить частичную сортировку int middle - PartSort(М, left, right);
264 Приложение // Сортировать левую половину HoareSort (М, left, middle - 1); // Сортировать правую половину HoareSort (М, middle + 1, right); Как видите, алгоритм не только быстрый, не только эконом- ный, но и простой. Этим объясняется его широкое практическое применение. Что касается сортировки слиянием, ее мы изучали не зря. Из трех известных нам сортировок она единственная пригод- на для упорядочения не только массивов, но и файлов. 6.4. Задачи 1. Предложите рекурсивный алгоритм суммирования массива из п чисел. Какова его глубина рекурсии? 2. Понятию факториала можно дать рекурсивное определение: а) 01=1; б) п! = (п ~ 1)! • п, если п > 0. Основываясь на этом определении, предложите рекурсивный алгоритм вычисления факториала. 3. Оцените временную сложность алгоритма Hanoy( ) для пира- миды из пдисков. 4. Оцените временную сложность быстрой обменной сортировки, предположив, что после частичной сортировки число-разде- литель всегда попадает: а) точно в середину сортируемой области; б) точно в начало сортируемой области. 7. Символическая арифметика В этом разделе мы рассмотрим позиционные системы счисле- ния и алгоритмы арифметических операций: сложения, вычита- ния, умножения, деления. В практическом отношении это даст нам возможность производить вычисления с неограниченной точнос- тью и над числами любой длины.
Введение в программирование 265 7.1. Позиционные системы счисления Любое целое число А можно записать в виде многочлена А = ап-Рп ' + -2,Р',’2+ ...+ а2 р2 + а1-р1 +а0-р°, (1) где р — положительное целое число, называемое основанием сис- темы счисления; ап _р ап 2,..., а2, а{) — целые неотрицательные числа, непре- вышающие р ~ 1. Если для изображения каждого числа из диапазона [0, п — 1] выделить особый символ, то слово ап^ап 2... а2 ах а0 из таких сим- волов будет записью числа А в системе счисления с основанием р. Символы, изображающие числа от 0 дор - 1, называют цифрами. Например, пусть р = 10. Номер четвертого года нынешнего тысячелетия можно представить в виде многочлена 2 • 103 + 0 • 102 + 0 • 101 + 4 • 10°. Значит, в позиционной системе счисления с основанием 10 этот номер запишется в виде слова «2004». 7.2. Образы чисел Задача 1. Дано целое число п. Распечатать составляющие его десятичные цифры в столбик. Для начала выделим младшую цифру. Это будет остаток от деления п на 10. В самом деле, все слагаемые многочлена (1), кро- ме последнего а0, делятся на 10 без остатка, а последнее слагае- мое само равно остатку, т.к. меньше 10, по определению. Итак, aQ == п % 10. Вторая цифра числа п равна младшей цифре числа (л/10). Поэтому, чтобы выделить вторую цифру, надо разделить п нацело на 10 и от результата взять остаток от деления на 10: п = л/10; а{ = п % 10. Так же следует поступать для третьей цифры, для четвертой и прочих, пока п будет оставаться больше 0. Когда п станет нулем, следует остановиться, т.к. дальнейшие повторения ничего, кроме нулей, не дадут. Запишем алгоритм печати числа в столбик.
266 Приложение u const int P - 10; int n; cin » n; while (n > 0) { cout << n % P << endl; n = n / P; } Задача 2. Дано целое число п. Распечатать цифры его двоич- ной записи в столбик. Решение этой задачи отличается от предыдущей всего одним числом. В первой строке надо написать U _________ const int Р = 2; Фактически мы разработали алгоритм получения цифр числа в любой системе счисления. Остается только не выводить цифры на экран, а сохранять их, например, в массиве. Оформим алгоритм в виде функции Image, и____________________________ int Image(Int n, int p, int img[], int size) { int i - 0; while (i < size) { img[i] - n % p; i = i + 1; n. - n / p; } return i; } Функция имеет четыре параметра: п — число, р — основание системы счисления, img — массив, куда надо складывать цифры числа, size — размер массива. Мы изменили условие повторения: вместо (п > 0) написали (i < size). Это сделано для того, чтобы гарантированно заполнить нуля- ми все старшие разряды образа.
Введение в программирование 267 7.3. Величина чисел В предыдущем разделе мы по величине числа определяли его изображение в заданной системе счисления. Теперь решим обрат- ную задачу: по изображению числа и основанию системы счисле- ния определим величину числа. Чтобы это сделать, достаточно про- сто просуммировать члены формулы (1). Будем полагать, что изображение находится в целом массиве длины size, в нулевом элементе массива расположено а0, в первом элементе а{ и т.д. Все элементы, начиная с n-го, естественно, со- держат нули. Оформим алгоритм в виде функции Value( ). 4 int Value(int р, int img[], int size) int val = 0; int к = 1; int i = 0; while (i < size) { val - val + img[i] * k; к = к * p; i ~ i + 1; } return val; Функция получает основание системы счисленияр, массив img и его размер size. Возвращает функция найденную величину числа. Имея пару таких функций, как Image( ) и Valuef ), можно лег- ко переводить образ числа в одной системе счисления в образ того же числа в другой системе счисления. Для удобства оформим алго- ритм перевода в виде функции Convert( ). 4___________________________________________________________ void Convert(int sour, int dest, int img[], int size) r Image(Value(sour, img, size), dest, img, size); } Первый параметр функции — основание исходной системы счисления, второй параметр — основание целевой системы счисле- ния, третий параметр — массив с образом числа, четвертый — раз-
268 11риложение мер массива. Точно так же как Image() функция возвращает раз- мер нового образа, который содержится в том же массиве img. 7.4. Печать изображения числа Полезно уметь выводить образ числа на экран. Для этого нуж- но просмотреть массив с образом в направлении от конца к началу и в таком порядке вывести на экран элементы массива. и Функция Printf ) получает: img — массив с образом и size — размер образа. Все обстоит прекрасно, пока мы не печатаем образы чисел в си- стеме счисления с основанием больше десяти. В таких системах для записи одноразрядных чисел принято использовать не только десятичные цифры, но и латинские буквы. Например, цифрами ше- стнадцатиричной системы служат: 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, А, В, С, D, Е, F. Десятичное число 255 в шестнадцатиричной системе выглядит как FF, наша же функция напечатает: 1515. Чтобы ис- править это упущение, дополним код функции Print(). U void Printf int img[], int size) { int i = size * 1; while (i >= = 0) { if (img! :ii < io) cout « img[i]; else if (img[i] == 10) cout << "A"; else if (img[i] == 11) cout « "B"; else if (img[ij =- 12)
Введение в программирование 269 cout << "C"; else if (imgfij -= 13) cout << “D"; else if (iragtij — 14) cout « "E"; else if (img[i] 15) cout < < •' F " ; else cout << и * и . i = i - 1 ; f } Здесь мы воспользовались вложенными условными операто- рами для перебора всех альтернатив, но отступили от правила от- ступов, т.к. все альтернативы равноправны, и это равноправие вы- ражается выровненным по вертикали кодом. Заметьте, что мы обеспечили вывод образов только для сис- тем с основанием, не превышающим 16. Цифры больше 15 на печа- ти появятся в виде звездочек. Предложенное решение не кажется изящным, но когда вы изучите больше возможностей языка, вы сможете написать более красивую программу. 7.5. Символическое сложение Предположим, у нас есть образы двух чисел в некоторой сис- теме счисления, и мы хотим их сложить, т.е. получить образ сум- мы этих чисел в той же системе счисления. Первое решение состоит в том, чтобы определить величину первого числа, определить величину второго числа, сложить их и получить изображение суммы. Это решение использует уже го- товые алгоритмы и потому выглядит просто. и_________________________________________________________ void Add(int р, int а[] , int b[] f int sum[] , int size) t int valA ~ Value (p, a, size); int valB = Value(p, b, size); Image (va 1A + valB, p, sum) ;
270 Приложение Второе решение заключается в том, чтобы непосредственно сложить изображения «в столбик» так, как нас учили на уроках арифметики. Будем складывать образы поразрядно, перенося еди- ницу в старший разряд, если сумма превышает или равна основа- нию системы счисления. U____________________________________________________________ void Add(int р, int а[], int b[], int sum[J, int size) { int carry = 0; // Перенос в старший разряд int i - 0; while ( i < size) { int s - afi] + b[i] + carry; sum[i] -- s % p; carry ” s / p; i " i + 1; } } Второе решение позволяет складывать числа любой длины и этим выгодно отличается от первого. 7.6. Символическое умножение Что делать, если мы хотим перемножить два числа, заданные своими образами? Простое сведение умножения к сложению (123x456 ^ 123 4- 4- 123 4- 123 + ... + 123) не работает, т.к. сомножители могут быть большими. Попробуем запрограммировать алгоритм умножения «в столбик». Вот пример: * 123 456 + 738 615 492 56088 Иными словами, 56088 - 123 * б + 1230 * 5 + 12300 * 4. В общем, алгоритм умножения чисел А и В можно сформули- ровать так. 1) Проходим все разряды числа В, начиная с младшего.
Введение в программирование 271 2) На каждом шаге умножаем число А на очередной разряд чис- ла В, т.е. вычисляем частичное произведение, назовем его С. 3) Частичное произведение С добавляем в полное произведение, назовем его PROD, 4) Перед выполнением следующего шага умножаем число А нар — основание системы счисления. Учитывая, что образы чисел мы храним в целых массивах, запишем алгоритм в виде функции Multy() с таким же набором параметров, как у функции Add(). Заметим, что в Multy() четвер- тый параметр означает не сумму образов, как в Add( )ь а их произ- ведение. V_________________________________________________________ const int SIZE - 10; void Muity(int p, int a[], int b[], int prod[], int size) { /* Очистить будущее произведение prod */ int i - 0; while (i < size) { /* Получить частичное произведение */ /* Добавить частичное произведение в полное */ /* Сдвинуть первый сомножитель на один разряд, т.е. умножить на р * / i = i + 1; } } Как видно, для достижения цели нам надо разработать не- сколько частных алгоритмов. Начнем с алгоритма получения частичного произведения, ко- торый заключается в умножении образа img на одноразрядное чис- ло q. Для этого пройдем все разряды образа и умножим каждый разряд на число q с переносом «десятков» в старший разряд, как мы это делали при сложении образов. и________________________________________________________ /* Получить частичное произведение */ void PartProd(int p, int q, int img[1, int pprod[J, int size) int carry =0; // Перенос в старший разряд
272 Приложение V———J———__ while (i < size) { int s = img[i] * q; // Остаток пишем в текущий разряд pprod[i] - s % р; // Частное переносим в следующий разряд carry - з / р; i -- i + 1 ; } } Параметрами функции PartProd( ) являются: р — основание системы счисления, q — число, на которое надо умножить образ, img — умножаемый образ, pprod — частичное произведение, size — размер образа. Алгоритм добавления частичного произведения к полному еще больше напоминает сложение образов и отличается от него тем, что не создает новый образ, а изменяет старый. и ___________ /* Добавить частичное произведение в полное */ void AddProd(int р, int a[j , int sum[] , int size) { int carry = 0; int i = 0; while( i < size) { int s = a[i] + sum[i] + carry; sum[i] = s % p; carry - s / p; i = i + 1; } J Умножение образа на основание системы счисления равнознач- но сдвигу образа на один разряд в сторону старших разрядов. В ос- вободившийся младший разряд вписываем ноль, и_____________________________________________________ /* Сдвинуть образ на один разряд */ void Shift(int р, int img[], int size) { int i ~ size - 1; while (i > 0) { img[i] - img[i - I];
Введение в программирование 273 i _ i _ } img[0] “0; //в младший разряд вносим 0 } И, наконец, очистка образа заключается в том, что все разря- ды образа img заполняются нулями. и_______________________________________________________ /* Очистить образ */ void Clear(int img[], int size) { int i - 0; while (i < size) { img [ i ] -- 0; i = i + 1; } } Подставим вызовы функций в исходный алгоритм. и__________________________________________________ const int SIZE - 10; void Muity(int p, int a [ ] , int b[], int prod[], int size) { // Счистить будущее произведение prod Clear(prod, size); int 1 = 0; while (i < size) { int c[SIZE]; // Получить частичное произведение PartProd(p, b[i], a, c, size); // Добавить частичное произведение в полное AddProd(p, с, prod, size); // Сдвинуть первый сомножитель на один разряд Shift(р, a, size); i = i + 1 ; г
274 Приложение Обратите внимание на константу SIZP9 объявленную за пре- делами функции Multy( ). Благодаря такому местоположению она может быть использована не только для объявления массива с внутри функции Multyf )9 но и для объявления массивов — аргу- ментов Malty ( ). и________________________________________________________ void main() { int a[SIZE], b[SIZE], clSIZE], p - 10; Image(20, p, a, SIZE); Image(10, p, b, SIZE); Multy(p, a, b, c, SIZE); Print(c, SIZE); } 7.7. Задачи 1. Функция Print() печатает все ведущие нули числа, что при- водит к появлению на экране большого количества ненужных символов. Дополните код функции Print() так, чтобы веду- щие нули не печатались. 2. Запрограммируйте алгоритм вычитания образов в системе счисления с основанием р. 3. Запрограммируйте алгоритм деления образов в системе счис- ления с основанием р. 4. Дано положительное вещественное число меньше единицы. Выведите на экран старший разряд этого числа, записанного в р-ичной системе счисления. 5. Предложите способ представления образов вещественных по- ложительных чисел, меньших единицы. Используйте для это- го операцию не деления, а умножения на основание системы счисления.
Литература 1. Полное описание современного состояния языка можно най- ти в книге Бьярна Страуструпа «Язык программирования C++, спец. изд.».— М., СПб.: «ИздательствоБИНОМ» — «Невский диалект», 2001 г.— 1099 с. 2. Для тех, кто любит изучать предмет по нескольким книгам и уже знаком с языком С, подойдет книга Герберта Шилдта «Самоучитель C++».— СПб.: БХВ-Петербург, 1999 г.— 650 с. 3. Очерк истории языка и описание его важнейших механизмов есть в книге Бьярна Страуструпа «Дизайн и эволюция C++».— М.: ДМК Пресс, 2000 г.— 448 с. (оригинал издан в 1994 г.). 4. Ценные советы по практическому применению стандартной библиотеки шаблонов можно найти в книге Скотта Мейерса «Эффективное использование STL. Библиотека программиста». — СПб.: Питер, 2002.— 224 с. 5. Тем, кто решил стать настоящим профессионалом в програм- мировании на C++, необходимо прочесть книгу Герба Саттера «Решение сложных задач на C++».— М.: Издательский дом «Вильямс», 2002.— 400с.
Содержание Предисловие..........................................3 Часть 1. Основы программирования...................... 1. Введение в C++.................................... 1.1. Что такое C++................................ 1.2. Простейшая программа......................... 1.3. Консольные программы в Visual C++ ........... 1.4. Организация повторений....................... 1.5. У с ловная инструкция........................ 1.6. Инструкция цикла for......................... 1.7. Задачи....................................... 2. Массивы.......................................... 2.1. Одномерные массивы........................... 2.2. Двумерные массивы............................ 2.3. Сортировка и поиск в массивах................ 2.4. Задачи....................................... 3. Указатели........................................ 3.1. Адресный тип данных ......................... 3.2. Операции new и delete ....................... 3.3. Разыменование и взятие адреса................ 3.4. Связь между массивами и указателями.......... 3.5. Массивы в свободной памяти................... 3.6. Двумерные массивы в свободной памяти......... 3.7. Задачи....................................... 4. Функции.......................................... 4.1. Параметры функции............................ 4.2. Выходные параметры функции .................. 4.3. Ссылки....................................... 4.4. Возврат ссылки .............................. 4.5. Одномерные массивы как параметры ............ 4.6. Двумерные массивы как параметры.............. 4.7. Задачи....................................... 5. Строки........................................... 5.1. Встроенный тип char.......................... 5.2. Строки символов .............................
277 5.3. Строковые библиотечные функции ..............32 5.4. Задачи.......................................34 6. Структуры и объединения........................ 35 6.1. Структуры....................................35 6.2. Списки.......................................36 6.3. Объявление структур .........................38 6.4. Битовые поля.................................39 6.5. Объединения..................................39 6.6. Задачи.......................................40 7. Дополнительные сведения о функциях...............41 7.1. Параметры по умолчанию.......................41 7.2. Произвольное число параметров................41 7.3. Неиспользуемые параметры.....................43 7.4. Перегруженные функции .......................44 7.5. Указатель на функцию.........................44 7.6. Спецификатор inline .........................46 7.7. Макросы......................................46 7.8. Задачи..................................... 48 8. Потоковый ввод и вывод...........................49 8.1. Разновидности ввода и вывода.................49 8.2. Открытие и закрытие потока...................49 8.3. Ввод и вывод символов........................50 8.4. Ввод и вывод строк...........................51 8.5. Ввод и вывод записей ........................52 8.6. Управление указателем файла..................52 8.7. Состояние потока.............................53 8.8. Форматированный вывод........................54 8.9. Форматированный ввод.........................55 8.10. Другие функции форматного ввода и вывода....56 8.11. Примеры .................................. 56 8.12. Файловый ввод-вывод.........................58 8.13. Задачи.................................... 58 9. Уточнение понятий языка..........................59 9.1. Объявление, определение, инициализация ......59 9.2. Область действия и время жизни ..............60 9.3. Типы.........................................61 9.4. Производные типы ............................61 9.5. Числовые константы...........................63 9.6. Именные константы............................64 9.7. Перечисление.................................64 9.8. Порядок вычисления выражений ................66
278 Содержание 10. Операции и операторы............................67 10.1. Сводка операций.............................67 10.2. Перечень инструкций.........................68 10.3. Инструкция выражения........................69 10.4. Инструкция switch...........................70 10.5. Инструкции break и continue.................71 10.6. ИнструкцИя и метки..........................71 10.7. Директив^ препроцессора .................. 72 Часть 2. 06ъектцо.ОриентирОВанное программирование...75 1. Классы.............................................. 1.1. Классы и объекты........................... 75 1.2. Инкапсуляция ................................76 1.3. Конструктор .................................77 1.4. Деструктор ..................................78 1.5. Указатели на объекты.........................79 1.6. Константные методы...........................79 1.7. Операции new и delete .......................79 1.8. Пример класса — список в свободной памяти....80 1.9. Задачи..................................... 82 2. Производные классы............................. 82 2.1. Наследование.................................82 2.2. Уровни доступа ..............................83 2.3. Виртуальные функции .........................84 2.4. Реализация виртуальных функций...............87 2.5. Полиморфизм .................................88 2.6. Список инициализации конструктора............89 2.7. Задачи..................................... 90 3. Дополнительные сведения о классах................91 3.1. Классы, структуры и объединения..............91 3.2. Присваивание и инициализация объектов .......91 3.3. Передача объектов функциям ..................93 3.4. Массивы объектов.............................95 3.5. Дружественные функции и классы ..............95 3.6. Статические элементы.........................97 3.7. Константы и типы в объявлении класса.........98 3.8. Вложенные классы ........................ 99 3.9. Указатели на члены класса.................. 100 3.10. Задачи.................................... 101 4. Перегрузка операторов............................ 102 4.1. Общие принципы...............................102
279 4.2. Перегрузка бинарных операторов.............. 103 4.3. Перегрузка операторов помещения и извлечения из потока ...........................104 4.4. Перегрузка унарных операторов............... 105 4.5. Оператор присваивания ...................... 107 4.6. Оператор индекса массива.....................108 4.7. «Умные» указатели .......................... 109 4.8. Задачи.......................................111 5. Распределение памяти........................... 112 5.1. Распределение памяти и инициализация объекта.112 5.2. Дополнительные параметры оператора new.......113 5.3. Освобождение памяти .........................114 5.4. Выделение памяти для массива ............... 116 5.5. Нехватка памяти............................. 116 5.6 Перегрузка глобальных операторов.............116 5.7. Задачи...................................... 117 6. Обработка исключений............................ 117 6.1. Создание и перехват исключений............ 117 6.2. Обработка различных исключений ............. 119 6.3. Стандартная обработка исключения.............121 6.4. Повторная обработка исключения ............. 122 6.5. Спецификация исключений в функции .......... 122 6.6. Стандартные исключения.......................123 6.7. Исключения, выбрасываемые оператором new..... 123 6.8. Исключения в конструкторах...................124 6.9. Исключения в деструкторах....................125 6.10. Исключения или управляющие конструкции? ... 126 6.11. Задачи..................................... 127 7. Множественное наследование...................... 127 7.1. Базовые классы.............................. 127 7.2. Представление объектов в памяти............. 128 7.3. Множественное наследование с общим предком...129 7.4. Виртуальные базовые классы ................. 130 7.5. Почему наследование называется виртуальным... 131 7.6. Виртуальные базовые классы и виртуальные функции ... 131 7.7. Задачи...................................... 133 8. Преобразования типов............................ 133 8.1. Идентификация типа во время исполнения...... 133 8.2. Операторdynamiccast......................... 133 8.3. Другие преобразователи типов ................136 8.4. Функции преобразования объектов..............139
280 Содержание 8.5. Задачи......................................... 140 9. Пространства имен................................ 141 9.1. Определение пространства имен ................. 141 9.2. Доступ к членам пространства................... 141 9.3. Различие между объявлением и директивой ....... 142 9.4. Прочие особенности............................. 143 9.5. Спецификаторы сборки........................ 145 9.6. Задачи..........................................145 Часть 3. Стандартная библиотека шаблонов.............. 146 1. Шаблоны.......................................... 146 1.1. Обобщенные функции (шаблоны функций)............146 1.2. Параметры шаблонов.......................•..... 147 1.3. Специализация шаблонов......................... 148 1.4. Выведение аргументов шаблона функции........... 149 1.5. Обобщенные классы...............................150 1.6. Методы композиции ..............................152 1.7. Отношения порядка и функциональные объекты..154 1.8. Задачи..........................................156 2. Контейнеры и итераторы............................. 157 2.1. Структура библиотеки............................157 2.2. Последовательные контейнеры.................... 158 2.3. Контейнер vector ...............................159 2.4. Итераторы ..................................... 160 2.5. Обзор методов вектора.......................... 162 2.6. Функции контейнеров deque и list............... 165 2.7. Адаптеры контейнеров........................... 166 2.8. Задачи..........................................166 3. Ассоциативные контейнеры. Строки................... 167 3.1. Ассоциативные массивы...........................167 3.2. Операции с ассоциативными массивами ........... 171 3.3. Множества и множества с дубликатами.............172 3.4. Строки ........................................ 173 3.5. Задачи..........................................177 4. Последовательные алгоритмы......................... 177 4.1. Немодифицирующие последовательные алгоритмы ... 178 4.2. Модифицирующие последовательные алгоритмы ......184 4.3. Задачи......................................... 193 5. Сортировка и прочие алгоритмы...................... 194 5.1. Алгоритмы, связанные с сортировкой..............194 5.2. Прочие алгоритмы................................199
281 5.3. Задачи.......................................202 6. Библиотека потоков................................ 202 6.1. Организация библиотеки потоков...............202 6.2. Вывод........................................203 6.3. Ввод.........................................205 6.4. Состояние потока.............................208 6.5. Итераторы потоков............................209 6.6. Задачи.......................................210 7. Форматирование ввода и вывода ................. 211 7.1. Флаги форматирования.........................211 7.2. Форматирующие функции-члены..................213 7.3. Манипуляторы ввода и вывода..................214 7.4. Файловые потоки .............................216 7.5. Строковые потоки.............................219 7.6. Задачи.......................................219 Приложение. Введение в программирование.............220 1. Основные этапы решения задачи на ЭВМ.............220 1.1. Постановка задачи............................220 1.2. Проектирование программы ....................221 1.3. Разработка алгоритма ...................... 221 1.4. Кодирование................................ 222 1.5. Отладка и тестирование программы.............222 1.6. Задачи.......................................223 2« Элементарные алгоритмы...........................223 2.1. Последовательность...........................223 2.2. Выбор........................................224 2.3. Повторение...................................225 2.4. Комбинация элементарных алгоритмов...........225 2.5. Другие инструкции............................226 2.6. Пример: ввод и сложение чисел................228 2.7. Задачи.......................................230 3. Разработка алгоритма........................... 231 3.1. Пошаговая детализация алгоритма..............231 3.2. Разработка алгоритма для вычисления синуса ..231 3.3. Разработка алгоритма подсчета простых чисел..234 3.4. Задачи.......................................236 4. Алгоритмы поиска................................ 236 4.1. Поиск наибольшего среди вводимых чисел ......237 4.2. Поиск наибольшего числа в массиве............240 4.3. Поиск заданного числа в массиве..............241
Навчальне видання В. М. Бондарев Програмування на C++ Навчальний пособник Редактор: Драган Н. О. Комп’ютерна верстка та дизайн: Столяренко А. В. Художник обкладинки: Побойко В. П. Шдп. до друку 07.09.2004 Формат 60 х 90 */16 Друк ризограф!чний Патр офсетний Умов. друк. арк. 17,75 Зам. № Щна догов!рна. Тираж 300 прим. Видавництво «Компатя СМ1Т». 61166, м. Харк1в, просп. Ленша, 14. Тел.: 8-(057)-717-54-94, 702-08-16 Факс: 8-(057)-714-23-66 E-mail: book@smit.kharkov.ua Св1доцтво про внесения суб’екта видавничо! справи до державного реестру видавщв, вигот1вник1в i розповсюджувач1в видавничо! продукци ДК № 435 в!д 26.04.2001. Друк — ФОП Васильева Н.В. 61166, м. Харюв, просп. Летна, 14. Тел.: 8-(057)-702-13-07