Текст
                    Министерство образования Республики Беларусь
Учреждение образования «Витебский государственный
университет имени П.М.Машерова»
С. П. Кунцевич
Языки С и С+н
Практикум по программированию
Витебск
Издательство УО «ВГУ им. П.М.Машерова»
2004

ББК 32.973 УДК 681.3 К 91 Печатается по решению Редакционно-издательского совета учреждения образова- ния «Витебский государственный университет имени ПМ.Машерова» Автор: старший преподаватель кафедры прикладной математики и механики УО «ВГУ им. ПМ.Машерова» С.П.Кунцевич Рецензент: зав. каф. информатики и информационных технологий УО «ВГУ им. ПМ.Машерова», канд. физ.-мат. наук, доцент А.И.Бочкин Кунцевич С.П. К 91 Языки С и C++: Практикум по программированию. - Витебск: Издательство УО «ВГУ им. П.М.Машерова», 2004. - 64 с. Пособие содержит обзор синтаксиса языков программирования С и C++. Теоретический материал сопровождается примерами и вариантами заданий для проведения практических занятий по программированию. Пособие предназначено для студентов, начинающих изучение язы- ков программирования С и C++. Для лучшего понимания материала по- собия желательно предварительное знакомство с одним из языков про- граммирования, а также с основными принципами объектно-ориентиро- ванного программирования. ББК 32.973 УДК 681.3 © Кунцевич С.П., 2004 © УО «ВГУ им. П.М.Машерова», 2004 2
СОДЕРЖАНИЕ Введение......................................................4 §1 . Синтаксические основы языка С............................5 Лабораторная работа №1...................................11 §2 . Операторы ветвления.....................................12 Лабораторная работа №2...................................14 §3 . Операторы повторения....................................16 Лабораторная работа №3...................................18 §4 . Директивы препроцессора.................................19 §5 . Массивы.................................................22 Лабораторная работа №4...................................23 §6 . Указатели...............................................25 §7 . Функции.................................................27 Лабораторная работа №5...................................29 §8 . Строки..................................................30 Лабораторная работа №6...................................32 §9 . Структуры...............................................33 Лабораторная работа №7...................................34 §10 . Ввод-вывод в языке С...................................35 §11 . Ввод-вывод в языке C++.................................38 Лабораторная работа №8...................................40 §12 . Динамическое распределение памяти......................41 Лабораторная работа №9...................................43 §13 . Линейные списки........................................44 Лабораторная работа №10..................................46 §14 . Объектно-ориентированное программирование..............47 Лабораторная работа №11..................................50 §15 . Конструкторы и деструкторы.............................51 §16 . Перегрузка операторов..................................54 §17 . Исключения.............................................56 Лабораторная работа №12..................................58 §18 . Шаблоны................................................59 Лабораторная работа №13..................................60 §19 . Полиморфизм и виртуальные функции......................61 Лабораторная работа №14..................................64 3
Введение Язык С - это язык программирования общего назначения, отличаю- щийся эффективностью, экономичностью и переносимостью. Первая вер- сия этого языка была разработана Деннисом Ричи и Кеном Томпсоном в начале 1970-х гг. в научно-исследовательской фирме Bell Telephone Labo- ratories для программирования в операционной системе UNIX, а в 1975 го- ду с его помощью была переписана и вся ОС UNIX для ЭВМ PDP-11. В принятом в 1989 году стандарте ANSI С определено только лишь 32 ключевых слова, из них почти половина связана с описанием переменных и констант. Язык С не содержит встроенных средств для выполнения вво- да-вывода, распределения памяти, манипуляций с экраном или управления процессами. Это компенсируется наличием стандартных библиотек, в ко- торых реализовано большое количество функций. Строгое определение языка С делает его независимым от любых дета- лей операционной системы или машины. В то же время программисты мо- гут добавить в библиотеку специфические функции, чтобы более эффек- тивно использовать конкретные системные или технические особенности. Язык С налагает незначительные ограничения на многие действия. В большинстве случаев это можно отнести к его достоинствам, однако, это требует от программистов хорошего знания языка для понимания того, как будут выполняться их программы. В начале 1980-х гг. Бьерн Страуструп добавил к языку С несколько дополнений, призванных упростить разработку больших и очень больших программ. Получившийся язык был назван C++. Большая часть новых воз- можностей C++ заключаются в поддержке объектно-ориентированного программирования. Язык C++ имеет семантику, намного более сложную по сравнению с языком С, однако, это ведет к большей надежности про- грамм и заставляет программистов быть более “дисциплинированными”. Язык C++ является надмножеством языка С, т.е. большинство про- грамм на С можно компилировать в среде C++, однако компилировать программы C++ в среде С, при наличии в них каких-либо специфических для C++ конструкций, нельзя. В подобных случаях в пособии делаются со- ответствующие замечания. 4
§1. Синтаксические основы языка С Типы данных В языке С имеется пять базовых типов данных: char (символьный, 8- разрядный), int (целочисленный, 16- или 32-разрядный), float (вещест- венный с одинарной точностью, 32-разрядный), double (вещественный с двойной точностью, 64-разрядный) и void (пустой). В язык C++ было до- бавлено еще два базовых типа: bool (логический, 8-разрядный) и wchar_t (расширенный символьный, 16-разрядный). Несмотря на название, величины символьного типа используются для работы с короткими 8-разрядными целыми числами, которые могут пред- ставлять коды символов. Тип void используется специальным образом, например, при описа- нии функций без параметров и/или без возвращаемого значения. С помощью модификаторов знака (signed или unsigned) и модифи- каторов длины (short или long) из некоторых базовых могут быть полу- чены дополнительные типы данных. Так, например, long double - веще- ственный тип с расширенной точностью (80-разрядный), a unsigned int (или просто unsigned) - беззнаковый целочисленный тип. Константы Почти в каждой программе присутствуют константы - фиксирован- ные значения, которые не могут изменяться во время работы программы. Константы могут быть любых типов: символьные, целочисленные и т.д. Символьные константы представляют собой одиночные символы, за- ключённые в одинарные кавычки, например, ' А'. Значением символьной константы является целое число - код символа. Для записи символов, ко- торые невозможно ввести с клавиатуры (например, возврат каретки), и не- которых печатаемых символов, используют специальные escape-последо- вательности, начинающиеся с обратного слэша, например, ’ \п* - переход на новую строку '\t' - горизонтальная табуляция ' \ ' - одиночная кавычка ’ \" ’ - двойная кавычка ' \\' - обратный слэш 5
’\х??’ - символ с шестнадцатеричным кодом ?? ' \???' - символ с восьмеричным кодом ??? Целочисленные константы могут записываться не только в десятич- ной, но и в восьмеричной и шестнадцатеричной системе счисления. Для этого используются префиксы 0 и Ох: 123 - десятичная константа 0123 - восьмеричная константа 0x123 - шестнадцатеричная константа Тип целочисленной константы (int, unsigned int и т.д.) определя- ется компилятором автоматически по её значению. При необходимости, тип константы можно указать принудительно с помощью суффиксов и и L: 123U - беззнаковое целое число 12 3L - длинное целое число 123UL - беззнаковое длинное целое число Дробные константы по умолчанию имеют тип double. Указав суф- фиксы L или F можно получить типы long double или float: 123. - константа типа double 12.3L - константа типа long double 1.23F - константа типа float Объявление переменных Перед использованием все переменные должны быть объявлены с по- мощью записи вида: имя типа список переменных; Список переменных должен содержать один или несколько иденти- фикаторов, разделённых запятыми. Идентификаторы записываются по об- щепринятым правилам: они могут содержать только латинские буквы и цифры и должны начинаться с буквы. При записи идентификаторов важен регистр символов, т.е. var и Var - это разные идентификаторы. Не допус- кается совпадение идентификаторов с ключевыми словами. В зависимости от места, в котором объявляются переменные, они на- зываются глобальными (объявляются вне функций), локальными (объявля- ются внутри функций) или формальными параметрами (объявляются при описании параметров функции). Во время объявления глобальных и локальных переменных им можно придать начальное значение, т.е. выполнить их инициализацию. Глобаль- ные переменные инициализируются один раз при запуске программы, ло- 6
кальные переменные инициализируются каждый раз при входе в блок, где они были объявлены. Если при объявлении переменных инициализацион- ное значение не указано, то глобальные переменные инициализируются нулём, а локальные переменные будут иметь неопределённое значение. Пример. Объявление и инициализация переменных int а, Ь=0, с; unsigned char Char; double f=2.5; Операции и операторы Язык С содержит необычно большой набор операций. Многие опера- ции С соответствуют машинным командам, и поэтому допускают прямую трансляцию в машинный код, а разнообразие операций позволяет выби- рать их различные наборы для минимизации результирующего кода. Для обозначения операций используются специальные символы или комбинации символов - операторы. Те операторы, которые записываются несколькими символами, не должны разделяться пробельными символами. Арифметические операторы +, -, *, / используются в языке С точно так же, как и в большинстве других языков программирования, и могут применяться ко всем базовым типам, кроме void. Только с целочисленны- ми операндами допускается использование арифметических операторов % (нахождение остатка), ++ (увеличение на 1) и — (уменьшение на 1). При использовании операторов увеличения и уменьшения на 1 в вы- ражениях различают две формы записи. При префиксной записи (++а или —а) значением выражений будет служить новое значение переменной, при постфиксной записи (а++ или а—) - старое значение. Битовые операторы ~ (инверсия), & (и), | (или), л (исключающее или), « (сдвиг влево), » (сдвиг вправо) используются для манипуляции отдельными битами целочисленных операндов и полностью соответствуют машинным командам. Как известно, операторы присваивания используются для изменения значений переменных. Кроме простого оператора присваивания (=), в языке С имеется десять составных операторов присваивания ( +=, -=, *=, /=, %=, &=, | =, л=, «=, »= ), совмещающих с присваиванием выполнение соответствующих бинарных арифметических и битовых операций. 7
В отличие от других языков программирования, в языке С допускает- ся использование операторов присваивания внутри выражений. Пример. Использование операторов присваивания а=Ь=с=0; то же, что и с=0; Ь=с; а=Ь; а%=Ь; то же, что и а=а%Ь; а=Ь+=с; то же, что и b=b+c; а=Ь; В дальнейшем будет рассказано про логические операторы, операто- ры отношений и другие операторы языков С и C++. Если в выражении смешиваются несколько операций, то порядок вы- полнения определяется их приоритетом. Наиболее высокий приоритет имеют унарные операции (++, —, ~ и т.д.). Если в выражении присутству- ют несколько операций с одинаковым приоритетом, то унарные операции выполняются справа налево, а большинство бинарных (за исключением, например, присваивания) - слева направо. Преобразование типов В языке С преобразование типов может выполняться двумя способа- ми: неявно (т.е. автоматически) или явно. Неявное преобразование типов выполняется в тех ситуациях, когда величины одного типа смешиваются с величинами другого типа. Когда подобное происходит при выполнении присваивания, то значе- ние выражения справа от знака “=” преобразовывается к типу переменной слева. В отличие от других языков программирования, в языке С разреша- ется автоматическое преобразование типов с потерей точности, например, из типа int к типу char или из типа double к типу int. Когда в арифметическом выражении смешиваются два операнда раз- ных типов, то происходит автоматическое преобразование типа операнда с меньшей точностью к типу операнда с большей точностью, например, из типа char к типу int или из типа int к типу double. В отличие от других языков программирования, результат арифмети- ческого выражения всегда имеет тот же тип, что и оба входящих в него операнда. Так, например, если делимое и делитель имеют целый тип, то и частное также будет целым. Поэтому, например, значение частного 22/7 будет равно 3, а площадь треугольника, вычисленная по формуле l/2*a*h, всегда будет равна нулю, вне зависимости от значений а и h. 8
В случае, когда неявное преобразование типов не дает нужный ре- зультат, можно прибегнуть к оператору явного преобразования типов: (имя типа) выражение Пример. Вычисление приближённого значения числа тс. double pi=(double)22/7; Структура программы Все С-программы состоят из одной или нескольких функций. В каж- дой программе обязательно должна присутствовать функция main, которая первой получает управление и определяет все дальнейшие действия. Пример. Простая программа на языке С #include <stdio.h> int main() { puts("Hello, world!"); return 0; } Первая строчка программы является указанием (“директивой”) под- ключить заголовочный файл stdio.h. Заголовочные файлы облегчают ис- пользование в программах стандартных функций, типов и констант. В подключаемом в примере заголовочном файле stdio.h содержатся опи- сания всех констант, типов и функций, используемых для организации файлового ввода/вывода в языке С, в том числе и описание функции puts, которая отвечает за вывод строки символов на экран. В языке C++ принято использовать другую, объектно-ориентирован- ную модель ввода/вывода. В частности, в заголовочном файле iostream.h описаны объекты cin и cout, которые используются для ор- ганизации ввода/вывода: #include <iostream.h> int main() { int x; cout « "Введите число X"; cin » x; cout « "Квадрат числа X равен " « x*x « ’\n’; return 0; } 9
Комментарии В языке С все комментарии начинаются с пары символов /* и за- канчиваются парой */. Компилятор игнорирует любой текст, заключён- ный между этими комбинациями символов. /* Обычно комментарии используются, если сделать пояснение к фрагменту программы. */ Комментарии воспринимаются компилятором как пробельные сим- волы и могут находиться в любом месте программы, где допустимо ис- пользование символов-разделителей. В языке C++ добавлена возможность использования однострочных комментариев, начинающихся с пары символов / /. // Компилятор C++ игнорирует любой текст // от символов // до конца строки Математические функции В стандарте языка С определено 22 математические функции, которые описаны в заголовочном файле math .h, в том числе: sin, cos, tan - тригонометрические функции; as in, acos, atan - обратные тригонометрические функции; sinh, cosh, tanh - гиперболические функции; exp - экспонента; log, loglO - натуральный и десятичный логарифмы; sqrt - квадратный корень; pow - степень числа; f abs - модуль числа. Почти все из этих функций имеют один аргумент (у функции pow - два аргумента) - вещественное число двойной точности (double) и воз- вращают значение этого же типа. Кроме того, в заголовочном файле math. h описан ряд часто употреб- ляемых математических констант, например: М_Е = 2.71828182845904523536 - числое М_Р1 = 3.14159265358979323846 — число 71 M_SQRT2 = 1.41421356237309504880 -д/2 10
Пример. Вычисление объёма шара #include <math.h> V=4*M_PI*pow(г,3)/3; Лабораторная работа №1 1. Найдите радиус окружности, описанной около правильного N- угольника со стороной а. 2. Дан периметр равностороннего треугольника. Найдите его площадь. 3. Даны длины внешней и внутренней границ кольца. Вычислите его площадь. 4. Даны длины сторон треугольника. Вычислите его площадь. 5. Найдите радиус окружности, вписанной в правильный А'-угольник со стороной а. 6. Даны длины катета и гипотенузы прямоугольного треугольника. Вычислите его площадь. 7. Даны длины стороны и диагонали ромба. Вычислите его площадь. 8. Дана длина диагонали и площадь ромба. Вычислите его периметр. 9. Даны длины диагоналей и площадь параллелограмма. Найдите его периметр. 10. Найдите площадь правильного А'-угольника со стороной а. 11. Даны координаты трёх вершин параллелограмма. Найдите коор- динаты четвёртой вершины. 12. Даны координаты двух вершин квадрата. Вычислите его площадь. 13. Даны координаты двух вершин куба. Найдите площадь его по- верхности. 14. Даны координаты вершин треугольника. Вычислите его периметр. 15. Найдите угол между двумя векторами в трехмерном пространстве. 16. Заданы координаты концов отрезка. Найдите уравнение середин- ного перпендикуляра к нему. 17. Даны координаты двух вершин квадрата. Вычислите координаты остальных вершин. 18. Найдите координаты точки пересечения двух прямых. 19. Даны координаты двух вершин куба. Вычислите его объём. 20. Даны координаты двух вершин квадрата. Найдите радиус вписан- ной в него окружности. 11
§2. Операторы ветвления Язык С содержит полный набор операторов структурного программи- рования: составной оператор, операторы ветвления, операторы повторения и операторы передачи управления. Составной оператор {} служит для объединения нескольких операто- ров в единый блок. Наиболее часто составной оператор используется внут- ри операторов ветвления или повторения, а также для ограничения области видимости вспомогательных переменных. Пример. Поменять местами значения двух целых переменных { int temp=a; a=b; b=temp; } В языке С знак является неотъемлемой частью оператора и не должен пропускаться даже перед закрывающей фигурной скобкой. Большинство структурных операторов основываются на проверке ус- ловий, в зависимости от истинности которых и выполняются те или иные действия. В языке С отсутствует специальный тип для хранения истины и лжи, а вместо него используются числовые значения. Лжи соответствует нуль, а истине - любое значение, отличное от нуля. В языке C++ появился специальный тип bool, который может принимать только два значения: false (0) или true (1). Для определения соотношения между операндами в языке С исполь- зуются операторы отношения == (равно), != (не равно), > (больше), >= (больше или равно), < (меньше), <= (меньше или равно) и логические опе- раторы ! (отрицание), && (логическое И), | | (логическое ИЛИ). Все операторы отношения и логические операторы возвращают целые значения 0 (ложь) или 1 (истина). Оператор ветвления if позволяет, в зависимости от некоторого ус- ловия, выполнить один из двух альтернативных операторов или наборов операторов. Полная форма записи оператора if выглядит так: if (выражение) оператор!; else оператор2; или, с использованием составных операторов, так: if (выражение) { последовательность операторов; } else { последовательность операторов; } 12
Так же, как и в других языках программирования, допускается краткая форма записи оператора if, не содержащая блока else. В круглых скобках в операторе if записывается выражение, играю- щее роль условия. Если его значение не равно нулю, то выполняется опе- ратор, записанный после if, если равно нулю - выполняется оператор, за- писанный после else. Пример. Поиск большего из двух чисел if (a>b) max=a; else max=b; Для замены конструкции if (выражение!) выражение2; else выражениеЗ; можно использовать условный оператор ? :, имеющий три операнда: выражение! ? выражение2 : выражениеЗ; Оператор ?: работает следующим образом: сначала вычисляется выражение!, если оно не равно нулю, то вычисляется выражениеЗ, в про- тивном случае - выражениеЗ. Вся конструкция получает значение вычис- ленного выражения. Пример. Поиск меньшего из двух чисел a<b ? min=a : min=b; или min = a<b ? а : b; Оператор выбора switch позволяет осуществить выбор выполняемо- го набора операторов, основываясь на результатах сравнения значения со списком целых или символьных констант. Полная форма записи этого опера- тора имеет вид: switch (выражение) { case константа!: последовательность операторов; break; case константа2: последовательность операторов; break; default: последовательность операторов; } При совпадении значения выражения с константой, выполняется со- ответствующая ей последовательность операторов. Если значение выраже- ния не совпадает ни с одной из перечисленных констант, то выполняется набор операторов, находящийся после default. 13
Раздел default может отсутствовать, в этом случае при отсутствии совпадения ничего не происходит. Пример. Фрагмент калькулятора switch (action) { case c=a+b; break; case c=a-b; break; case '*': c=a*b; break; case c=a/b; break; } Если какая-либо последовательность операторов не завершается опе- ратором break, то продолжается выполнение следующего набора опера- торов и т.д. до тех пор, пока не встретится оператор break или пока не за- кончится оператор switch. Благодаря этому, например, можно выполнять один и тот же набор действий для разных значений констант. Пример. Фонетический анализ switch (letter) { case 'а': case 'е': case 'o': case 'у': case 'ю': case 'я' : cout « letter « default: cout « letter « } case 'ё': case 'и': case 'ы': case 'o': " - гласная буква\п"; break; " - не гласная буква\п"; Лабораторная работа №2 В заданиях 1 10 необходимо определить взаимное расположение гео- метрических фигур на плоскости. 1. Точка и треугольник (возможные варианты расположения: точка лежит внутри, снаружи или на треугольнике). 2. Две окружности (возможные варианты расположения: пересекают- ся, касаются, не имеют общих точек). 3. Точка и прямоугольник со сторонами, параллельными осям коор- динат (возможные варианты расположения: точка лежит внутри, снаружи или на прямоугольнике). 4. Окружность и прямая (возможные варианты расположения: пересе- каются, касаются, не имеют общих точек). 5. Две прямые (возможные варианты расположения: пересекаются, перпендикулярны, параллельны, совпадают). 14
6. Точка и окружность (возможные варианты расположения: точка лежит внутри, снаружи или на окружности). 7. Два отрезка (возможные варианты расположения: пересекаются или не пересекаются). 8. Прямая и прямоугольник со сторонами, параллельными осям коор- динат (возможные варианты: пересекаются или не пересекаются). 9. Два отрезка на прямой (возможные варианты расположения: не имеют общих точек, имеют одну общую точку или общий отрезок). 10. Два прямоугольника со сторонами, параллельными осям коорди- нат (возможные варианты расположения: не имеют общих точек, имеют одну или бесконечное множество общих точек). 11. Даны координаты вершин треугольника. Определите его тип (ост- роугольный, прямоугольный, тупоугольный). 12. Даны три числа. Определите, можно ли из них составить арифме- тическую прогрессию. 13. Даны три числа. Определите, можно ли из них составить геомет- рическую прогрессию. 14. Даны длины сторон треугольника. Определите его тип (равносто- ронний, равнобедренный или ни тот, ни другой). 15. Определите, пройдёт ли кирпич со сторонами а, Ь, с в прямоуголь- ное отверстие со сторонами т,п. 16. Даны длины четырёх отрезков. Определите, могут ли они являться сторонами равнобокой трапеции. 17. Даны длины четырёх отрезков. Определите, могут ли они являться сторонами параллелограмма. 18. Даны координаты трёх точек. Определите, могут ли они являться вершинами прямоугольника. 19. Даны координаты четырёх точек. Определите, могут ли они яв- ляться вершинами параллелограмма. 20. Даны координаты трёх точек. Определите, могут ли они принад- лежать одной сфере. 15
§3. Операторы повторения Оператор повторения с предусловием while имеет следующую фор- му записи: while (выражение) оператор; или, с использованием составного оператора: while (выражение) {последовательность операторов;} Выражение, записываемое в круглых скобках, играет роль условия продолжения цикла. Цикл выполняется, пока это выражение не равно ну- лю (истинно). Когда выражение становится равным нулю, выполняются операторы, следующие за циклом. Цикл не выполняется ни разу, если при первой проверке выражение сразу равно нулю. Пример. Алгоритм Евклида while (a!=b) if (a>b) a-=b; else Ь-=а; Оператор повторения с постусловием do while имеет вид: do оператор; while (выражение); или, с использованием составного оператора: do { последовательность операторов; } while (выражение); В отличие от оператора while, в операторе do while проверка ус- ловия продолжения цикла происходит в конце, после выполнения тела цикла. Таким образом, в операторе do while тело цикла всегда выпол- няется хотя бы один раз. Пример. Ввод числа из диапазона [1; 10] do { cout « "Введите число N (l<=N<=10)"; cin » N; } while (N<1 || N>10); Наиболее мощным и гибким в языке С является оператор повторения for. Его полная форма записи имеет вид: for (выражение!; выражение2; выражениеЗ) оператор; или, с использованием составного оператора: for (выражение!; выражение2; выражениеЗ) { последовательность операторов; } 16
Оператор for полностью соответствует следующей конструкции, содержащей оператор while: выражение!; while (выражение2) { последовательность операторов; выражениеЗ; } Выражение! выполняет инициализацию цикла и обычно содержит команду присваивания. Выражение2 играет роль условия продолжения цикла. ВыражениеЗ обычно содержит команду изменения переменной на каждой итерации цикла. Каждое из этих выражений, в свою очередь, мо- жет состоять из нескольких, разделённых оператором последовательного выполнения, или отсутствовать. Пример. Вычисление суммы первых 100 натуральных чисел for ( i=l, S=0; i<=100; i++ ) S+=i; или for ( i=l, S=0; i<=100; S+=i, i++ ) ; или for ( i=l, S=0; i<=100; S+=i++ ) ; В языке C++ допускается выражение! заменять на команду объявле- ния и инициализации локальной переменной цикла: S=0; for (int i=l; i<=100; i++) S+=i; Во всех операторах повторения внутри тела цикла могут использо- ваться операторы передачи управления break и continue. По оператору break прекращается выполнение тела цикла и осуще- ствляется переход к следующей за циклом команде. Пример. Поиск первого простого числа в диапазоне [А;В] for (i=A; i<=B; i++) if (prostoe(i)) break; По оператору continue пропускается оставшаяся часть тела цикла и осуществляется переход к следующей итерации. Пример. Поиск простых чисел-близнецов в диапазоне [А;В] for (i=A; i<=B-2; i++) { if (Iprostoe (i)) continue; if (prostoe (i+2) ) cout « i i+2 « ’ \n’; } К операторам передачи управления также относится оператор перехо- да goto, который позволяет передать управление другому участку кода. Однако в большинстве случаев его использование не рекомендуется. 17
Лабораторная работа №3 В заданиях 1 10 необходимо с точностью h вычислить приближенное значение некоторой функции в точке х. Вычисления реализуйте с помощью всех видов операторов повторения (while, do while, for). Считайте, что требуемая точность достигнута, если очередное слагаемое по модулю не превосходит h. Сравните с результатом, полученным с помощью соответствующих функций стандартной библиотеки. со / = 0 ^arctgx-^C-1)^ i = 0 со S.COSX-^f-l) 9. In 1 +х 1 - X 61+х У / 1 = 0 со О 1 8. ch .г / j (2iy i = 0 со 1о(г4^~Х(_1)'('+1)? i = 0 11. Найдите количество цифр в заданном целом числе. 12. Найдите наименьший простой делитель натурального числа. 13. Проверьте, является ли заданное натуральное число симметрич- ным (одинаково пишется слева направо и справа налево). 14. Разложите заданное натуральное число на простые множители. 15. Найдите сумму делителей заданного натурального числа. 16. Найдите все простые делители заданного натурального числа. 17. Найдите количество делителей заданного натурального числа. 18. Найдите сумму цифр заданного целого числа. 19. Найдите наибольший простой делитель натурального числа. 20. “Вычеркните” из заданного целого числа самую большую цифру. 18
§4. Директивы препроцессора Одной из особенностей языка С, отличающей его от других языков программирования, является наличие препроцессора, который выполняет обработку исходного текста программы перед компиляцией. Благодаря препроцессору возможно значительно упростить разработку и модифика- цию программы. Например, препроцессор может выполнить замену фраг- ментов текста в программе, вставить в программу содержимое других фай- лов, отбросить часть программы и т.д. Управление работой препроцессора осуществляется с помощью раз- личных директив. Все директивы препроцессора начинаются со знака #. Каждая директива должна располагаться в отдельной строке. Директивы могут находиться в любом месте программы, но их действие распростра- няется только на остаток файла от того места, где они появились. Наиболее часто в программах используется директива #include <имя файла> или #include "имя_файла" Эта директива служит указанием препроцессору включить в текст программы содержимое указанного файла. Если имя включаемого файла указано в угловых скобках, то поиск файла осуществляется в стандартных каталогах для хранения заголовочных файлов, если в кавычках - сначала в каталоге с программой и только потом - в стандартных каталогах. Директива #define идентификатор последовательность символов служит указанием препроцессору заменить все вхождения идентификатора в тексте программы на указанную последовательность символов (макро- подстановку). Определённый этой директивой идентификатор затем мож- но использовать в тексте программы или внутри других макроподстановок. Пример. Сумма и произведение N чисел #define N 10 #define FOR for(int i=l;i<=N;i++) int S=O,P=1; FOR S+=i; /* for(i=l;i<=10;i++) S+=i; */ FOR P*=i; /* for (i=l;i<=10;i++) P*=i; */ cout « "Сумма чисел от 1 до " « N « " = " « S « ". Их произведение равно " « Р « ’\п’; 19
Директива #define часто используется для описания различных констант. Например, с её помощью в заголовочном файле math. h описа- ны все математические константы: #define М_Е 2.71828182845904523536 Директива #undef идентификатор отменяет макроопределение, данное директивой #define указаному идентификатору. Так можно ограничить действие макроподстановки на ту часть программы, где она действительно нужна. Директива #define может определять макроподстановку с пара- ментами'. #define идентификатор(параметры) макроподстановка Препроцессор, выполняя замену такого идентификатора, будет под- ставлять вместо параметров, встречающихся в макроподстановке, реаль- ные выражения. Пример. Большее из двух #define max(a,b) ( (а)> (b)? (а) : (b)) В макроподстановках необходимо использовать скобки для того, что- бы сохранить верный порядок действий и после выполнения замены. В противном случае можно получить неожиданный результат. Наибольшее количество директив препроцессора относится к дирек- тивам условной компиляции. Эти директивы позволяют разрешить и/или запретить компиляцию фрагментов программы, что широко используется при разработке различных версий одной и той же программы. Простейший блок условной компиляции имеет вид: #if константное выражение текст #endif Выражение, записываемое после директивы #if, играет роль условия и вычисляется до компиляции, т.е. оно должно содержать только константы и ранее определённые идентификаторы. Если условие истинно, то текст между #if и #endif обрабатывается препроцессором, в противном случае - пропускается. Вместо директивы #if, блок условной компиляции может начинать- ся с директив #ifdef или #ifndef. В этом случае в условном выраже- 20
ния должен быть записан единственный идентификатор. Текст внутри блока условной компиляции будет обрабатываться препроцессором, если этот идентификатор был ранее определён с помощью директивы #def ine, или не определён, соответственно. Подобные директивы часто используются в заголовочных файлах для предотвращения их многократной обработки препроцессором. Пример. Использование директив условной компиляции #ifndef _unique_name #define _unique_name /*этот текст обрабатывается не более одного раза*/ #endif Блок условной компиляции может быть расширен с помощью дирек- тивы #else, которая позволяет задать для компиляции альтернативный вариант последовательности операторов: #if константное выражение последовательность операторов #else альтернативная последовательность операторов #endif При необходимости использовать несколько вложенных директив #else #if, запись может быть сокращена с помощью директивы #elif: /* полная запись */ /* краткая запись */ # if условие #if условие текст текст # else #elif условие_2 # if условие 2 текст 2 текст 2 #else #else текст 3 текст_3 #endif #endif #endif Директива #error сообщение об ошибке используется для отладки. Когда препроцессор встречает эту директиву, он прекращает компиляцию и выдаёт указанное сообщение об ошибке. Пример. Проверка наличия макроопределения #ifndef N #error Значение N не определено #endif 21
§5. Массивы Как известно, массивом называют совокупность переменных одного типа, к которым обращаются с помощью общего имени. Доступ к каждому конкретному элементу массива осуществляется с помощью его индекса, указываемого в квадратных скобках [ ]. Одномерные массивы объявляются следующим образом: тип имя [размер]; Первый элемент любого массива имеет индекс 0. Например, если мас- сив объявлен следующим образом: double А [3 ]; то в нём три элемента: А [ 0 ], А [ 1 ] и А [ 2 ]. Для обработки одномерных массивов удобнее всего использовать оператор повторения for: for (i=0; i<N; i++) оператор; Пример. Ввод массива с клавиатуры for (i=0, sum=0; i<N; i++) cin » A[i]; Пример. Сумма элементов массива из N элементов for (i=0, sum=0; i<N; i++) sum+=A[i]; Пример. Является ли массив “симметричным ”? for (i=0, j=N-l, k=l; i<j; i++, j--) if (A[i]!=A[j]) k=0; if (k) cout « "массив симметричный"; else cout « "массив не симметричный"; В языке С полностью отсутствует какие-либо средства, предотвра- щающие выход индексов элементов массива за допустимые границы, что при неаккуратном программировании часто приводит к ошибкам. При объявлении массивов можно выполнить их инициализацию, для чего соответствующие значения элементов перечисляются в фигурных скобках после знака “=”. Если значений указано меньше, чем элементов, то оставшиеся элементы инициализируются нулём. double pi[5] = {3, 3.1, 3.14, 3.141, 3.1415}; int sto[100] = {1}; /* и 99 нулей */ Если при инициализации массива не указать количество элементов, то оно берётся равным количеству перечисленных значений: char z[] = {0, 1, 2, 3, 4, 5}; /* 6 элементов */ 22
В языке С допускается использование многомерных массивов: тип имя [размеры]...[размер2][размер!]; В памяти многомерные массивы располагаются непрерывно. Напри- мер, двумерный массив float R[10][20]; содержащий 200 элементов, в памяти представляется с помощью десяти подряд расположенных одномерных массивов, в каждом из которых по 20 элементов: в первом будут элементы от R [ 0 ] [ 0 ] до R [ 0 ] [ 19 ], во втором - от R [ 1 ] [0] до R [ 1 ] [19],в последнем - от R [ 9 ] [0] до R [ 9 ] [19]. Многомерные массивы инициализируются так же, как и одномерные, однако, для удобства, лучше добавить фигурные скобки вокруг каждого измерения, например, так: int stepen2[6][2] = { {0,1}, {1,2}, {2,4}, {3,8}, {4,16}, {5,32} }; Размеры массивов удобно определять с помощью директивы препро- цессора #define. Лабораторная работа №4 1. Найдите сумму элементов одномерного массива, находящихся ме- жду элементами с наибольшим и наименьшим значениями. 2. Без использования вспомогательного массива переставьте элементы одномерного массива таким образом, чтобы сначала шли отрицательные элементы, а затем положительные (с сохранением порядка). 3. Найдите самую длинную последовательность элементов одномер- ного массива, образующую арифметическую прогрессию. 4. Обменяйте значения элементов одномерного массива с наибольши- ми и наименьшими значениями. 5. Найдите самую длинную ненулевую последовательность в одно- мерном массиве. 6. Без использования вспомогательного массива уплотните одномер- ный массив, исключив из него нулевые элементы и сдвинув влево все ос- тальные, не нарушая их исходный порядок. 7. Найдите самую длинную последовательность элементов одномер- ного массива, образующую геометрическую прогрессию. 8. Найдите количество различных элементов одномерного массива. 23
9. Определите, можно ли из одного одномерного массива вычёркива- нием элементов получить другой. 10. Переставьте элементы одномерного массива в порядке возраста- ния количества их делителей. 11. Проверьте, является ли квадратный массив симметричным относи- тельно (а) главной или (б) побочной диагонали. 12. Найдите (а) сумму и (б) разность двух квадратных матриц. 13. Отобразите квадратный массив относительно (а) главной и (б) по- бочной диагонали. 14. Проверьте, содержит ли двумерный массив седловой элемент (наибольший в строке и наименьший в столбце). 15. В двумерном массиве найдите (а) строку и (б) столбец с наиболь- шей суммой элементов. 16. Проверьте, является ли двумерный массив симметричным относи- тельно (а) вертикальной или (б) горизонтальной оси. 17. Найдите произведение двух квадратных матриц. 18. Отобразите двумерный массив относительно (а) вертикальной и (б) горизонтальной оси. 19. Проверьте, имеются ли в двумерном массиве одинаковые (а) стро- ки или (б) столбцы. 20. В двумерном массиве найдите (а) строку и (б) столбец с наиболь- шим количеством нулевых элементов. 24
§6. Указатели В языке С активно используются указатели - переменные, содержа- щие адреса памяти, распределяемой для размещения других переменных. Кроме работы с динамически распределяемой памятью, указатели помо- гают эффективно работать с массивами, строками, широко используются при передаче параметров функциям и т.д. Объявление переменной-указателя имеет вид: базовый тип *имя; При объявлении указателя должен быть задан базовый тип (тип пере- менной, адрес которой будет содержать указатель), а имени указателя должна предшествовать звёздочка (или несколько звездочек для мно- жественного перенаправления). В одном объявлении можно смешивать простые переменные и переменные-указатели, например, так: char s, *z, S[10] ; Для получения адреса переменной в языке С используется оператор взятия адреса &: z = & s; Чтобы показать, что указатель не содержит адрес какой-либо пере- менной, ему обычно присваивают значение NULL (нулевой указатель). Оператор разыменования указателя * используется для доступа к пе- ременной, адрес которой содержится в указателе: * z = ’ q ’ ; С переменной, полученной с помощью разыменования указателя, можно совершать все операции, допустимые для базового типа. Если, по каким-то причинам, базовый тип указателя не соответствует типу пере- менной, на которую он указывает, необходимо прибегнуть к явному пре- образованию типа указателя: *(double*)z = 2.3; В языке С существует тесная связь между указателями и массивами. В частности, имя массива без указания квадратных скобок рассматривается как адрес его нулевого элемента, и, наоборот, при использовании квадрат- ных скобок с указателем, он рассматривается как одномерный массив: S то же, что и &S[0] z [0] то же, что и *z 25
При выполнении над указателями арифметических операций, их зна- чения изменяются в предположении, что указатели содержат адреса эле- ментов массивов базового типа. Изменение указателя на единицу соответствует переходу к следую- щему (предыдущему) элементу одномерного массива: z++; /* перейти к следующему элементу массива */ z ——; /* перейти к предыдущему элементу массива */ Аналогичные действия выполняются, если к указателю прибавить (отнять) целое число: S+5 /* адрес элемента S[5] */ *(S+5) /* сам элемент S[5] */ Разность между двумя указателями равна целому числу - разности между соответствующими индексами элементов массива. С указателями возможно использование и операторов отношения, сравнивающих соответствующие адреса объектов в памяти. Например, элементу массива с большим индексом соответствует больший адрес в па- мяти, а нулевой указатель (null) меньше любого из указателей. Использование указателей позволяет более эффективно записывать многие алгоритмы по обработке массивов. Пример. Сумма элементов массива int sum=0, *u=A+N; while (A<u--) sum+=*u; cout « "Сумма элементов массива " « sum « ’\n’; Пример. Поиск наибольшего элемента массива int *max=A, *u=A+N; while (A<u--) if (*max<*u) max=u; cout « "Наибольший элемент массива = " « *max « "\nEro индекс: " « max-A « ’\n’; В этих примерах массивы просматриваются с помощью указателя и, начальное значение которого равно адресу элемента массива, следующего за последним. Благодаря оператору уменьшения на 1, при проверке усло- вия выполнения цикла указатель сдвигается на предыдущий элемент мас- сива, значение которого затем суммируется (в первом примере) или срав- нивается (во втором). Как видно, использование указателя во втором при- мере позволяет обойтись одной переменной max для определения и значе- ния, и индекса наибольшего элемента массива. 26
§7. Функции Описание функций имеет следующий вид: возвращаемый тип имя функции (список параметров) { последовательность операторов; } Функция может возвращать значения любого допустимого типа. Если функция не должна возвращать значения, то в качестве возвращаемого ти- па должен быть указан тип void. Вложенность функций не допускается. В списке параметров функции необходимо указывать тип для каждого используемого функцией формального параметра. void MyFunction (int a, int b, int c) { } Все аргументы передаются в функцию по значению, путём копирова- ния в формальный параметр. Т.е. формальные параметры можно произ- вольно модифицировать, при этом значение переменной, используемой при вызове функции, остаётся неизменным. Если необходимо передать в функцию аргумент по ссылке, то в качестве формальных параметров сле- дует использовать указатели, а в функцию передавать адрес аргумента. Пример. Функция для обмена значений двух переменных void xchg (int * a, int * b) { int temp=*a; *a = *b; *b = temp; } xchg (&x, &y) ; /* передача параметров по ссылке */ Немедленный выход из функции и возврат значения осуществляется с помощью оператора return. Например, основная функция программы (main) обычно возвращает целочисленное значение: return 0; Если в теле функции отсутствует оператор return, то работа функции за- канчивается после выполнения последнего её оператора. В языке С отсутствует возможность передачи массивов в функцию по значению. Вместо этого по значению передаётся только указатель на мас- сив, следовательно, функция имеет доступ ко всем его элементам. Формальный параметр для передачи указателя на массив может быть описан несколькими способами: тип имя [размер] /* как массив */ тип имя [] /* как безразмерный массив */ тип * имя /* как указатель */ 27
Наиболее часто используется последняя форма записи, как соответствую- щая действительному способу передачи аргумента. Пример. Обнуление массива void Zero (int * mas, int N) { int i; for (i=0; i<N; i++) *mas++=0; } int A [ 10]; Zero (A, 10); /* передача массива как указателя */ В языке C++ есть несколько дополнений, касающихся описаний функ- ций и передачи параметров. Во-первых, передача параметров по ссылке может осуществляться неявно. Пример. Функция для обмена значений двух переменных void xchg (int & a, int & b) { int temp=a; a = b; b = temp; } xchg (x, у); /* неявная передача по ссылке */ Во-вторых, в языке C++ разрешается использовать несколько функ- ций с одинаковыми именами, отличающиеся набором аргументов: void xchg (double & a, double & b) { double temp=a; a = b; b = temp; } void xchg (char & a, char & b) { char temp=a; a = b; b = temp; } Компилятор автоматически генерирует вызов той или иной функции, основываясь на типе и количестве передаваемых аргументов. Подобный механизм принято называть перегрузкой функций. В-третьих, для формальных параметров при описании функций в язы- ке C++ разрешается указывать значения по умолчанию, которые автомати- чески будут использоваться компилятором: double koren (double х, double stepen=2) { return pow(x,double(1)/stepen); } y=koren(x); /* корень 2-й степени */ z=koren(x,3); /* корень 3-й степени */ В четвёртых, в языке С допускается вызов функций до её описания. В этом случае компилятор генерирует вызов, основываясь на типах фактиче- ских параметров, и предполагает, что функция возвращает целочисленное значение. В языке C++ вызов функции до её описания является ошибкой. И в языке С, и в языке C++ широко применяются предварительные описания функций, называемые прототипами. Прототип функции отлича- 28
ется от её описания отсутствием тела функции и обычно помещается в на- чале программы, а само описание - в конце (или даже в отдельном файле). Использование прототипов позволяет избежать ошибок, связанных с несо- ответствием типов и количества параметров, передаваемых в функции. Обычно прототипы функций помещают в заголовочные файлы, кото- рые потом подключают к основной программе директивой include. Так, в заголовочных файлах, поставляемых с компилятором, содержатся прото- типы всех стандартных функций. Пример. Использование прототипа функции /★ ========================= основная программа */ #include <iostream.h> #include "nod.h" int main() { int a,b; cout « "a=?"; cin » a; cout « "b=?"; cin » b; cout « "НОД(а,Ь)=" « nod(a,b); return 0; } /* ==================== заголовочный файл nod.h */ int nod (int a, int b); /* =========== файл с пользовательской функцией */ int nod (int a, int b) { while (a!=b) if (a>b) a-=b; else b-=a; return a; } Для корректной компоновки исполнимого файла необходимо, чтобы все файлы с пользовательскими функциями были включены в проект. Лабораторная работа №5 Выполните задания 1 10 лабораторной работы № 4, при этом функции для ввода, вывода и обработки массивов разместите в отдель- ном файле. Внутри функций весь доступ к элементам массивов осуществ- ляйте с помощью указателей. Подготовьте соответствующий заголовочный файл. 29
§8. Строки Одним из частых применений одномерных массивов в языке С явля- ются строки, которые представляются как символьный массив произволь- ной длины, оканчивающийся символом с кодом 0. При объявлении символьных массивов следует задавать размер, дос- таточный для хранения всех символов строки (с учётом предполагаемых преобразований) и завершающего нулевого символа. Инициализация строк может выполняться с помощью строковых кон- стант - последовательностей символов, заключённых в двойные кавычки: char s [10] = "Строка"; Данная запись соответствует следующему объявлению массива: char s [10] = { ’С’, ’т’, ’ р’, ’о’, ’к’, ’а’, 0 }; В строковых константах можно использовать те же escape-последова- тельности, что и в символьных константах. Нулевой символ добавляется в конец строковой константы автоматически. Поддержка операций над строками (вернее, над символьными масси- вами) обеспечивается с помощью большого количества библиотечных функций, прототипы которых содержатся в заголовочном файле string.h. Перечислим основные из них: size_t strlen(char * s) - возвращает длину строки s. Используемый в этой и многих других функциях языка С тип size_t является синонимом для беззнакового целого типа и обычно обозначает размер переменных в памяти. char * strcpy(char * si, char * s2) — копирует последова- тельность символов из строки s2 в строку si. char * s treat (char * si, char * s2) - копирует последова- тельность символов из строки s2 в конец строки si. char * strncpy(char * si, char * s2, size_t n) — функция копирует не более и первых символов из строки s2 в строку si. char * strncat(char * si, char * s2, size_t n) — к строке si присоединяет не более п первых символов из строки s2. Для предотвращения выхода за пределы массивов при работе функ- ций strepy, strcat, strnepy и strncat, необходимо, чтобы под целе- вую строку был зарезервирован достаточный объём памяти. 30
Пример. “Компьютерная грамотность - вторая грамотность ” char s[100]="Компьютерная грамотность"; strcat (s," - вторая "); strncat (s,s + 13,11); Значение указателя, возвращаемое функциями strcpy, strcat, strncpy и strncat, совпадает с первым аргументом и может быть ис- пользовано для организации последовательных вызовов, например, так: strncat(strcat (s," - вторая "),s+13,11); Использование указателей при обработке строк позволяет создавать компактные и эффективные программы. Например, стандартная функция strlen может быть реализована следующим образом: size_t Strlen (char * s) { size_t len=0; if (s) while (*s++) len++; return len; } int strcmp(char * si, char * s2) - выполняет посимвольное сравнение строк si и s2. Если строки совпадают, то возвращаемое значе- ние равно нулю. Если строки различаются, то возвращаемое значение рав- но разности кодов первой пары не совпадающих символов. int strncmp(char * si, char * s2, size_t n) — выполняет сравнение не более п символов строк si и s2. char * str str (char * si, char * s2) - возвращает указатель на первое вхождение строки s2 в строку si. char * strchr(char * s, char с) - возвращает указатель на первое вхождение символа с в строке s. char * strrchr(char * s, char с) - возвращает указатель на последнее вхождение символа с в строке s Функции str str, strchr и strrchr возвращают нулевое значение указателя, если строка не содержит искомые подстроку или символ. Пример. Заменить в строке s все символы 'v' на 'w' char * v; while (v=strchr(s,’v’)) *v=’w’; В заголовочном файле stdlib.h содержится описание ещё несколь- ких функций, работающих со строками: int atoi (char * s) - возвращает целое число, записанное в стро- ке s (или нуль, если строка s не является записью числа). 31
double atof (char * s) - возвращает дробное число, записанное в строке 5 (или нуль, если строка s не является записью числа). char * itoa(int n, char * s, int r) — в строке сформирует запись целого числа п в системе счисления с основанием г. Под строку s должен быть зарезервирован достаточный объём памяти. Для ввода строк с клавиатуры можно воспользоваться описанной в за- головочном файле stdio.h функцией gets (char * s), которая заносит введённую пользователем строку в символьный массив s. Под строку s должен быть зарезервирован достаточный объём памяти. Вывод строки на экран может осуществляться с помощью уже упоми- навшейся функции puts (char * s). Лабораторная работа №6 В заданиях 1 10 необходимо реализовать аналоги стандартных функций для работы со строками. Подготовьте программу, демонстри- рующую совпадение результатов их работы со стандартными. 1. strcpy 2. strncpy 3. strcat 4. strncat 5. strcmp 6. strncmp 7. strchr 8. strrchr 9. atoi 10. itoa 11. Уплотните строку, удалив лишние пробелы. 12. Выведите на экран самое длинное слово в данной строке. 13. Проверить, является ли данная строка палиндромом, т.е. читается ли она одинаково слева направо и справа налево. Пробелы не учитывать. 14. Проверьте, можно ли вычёркиванием букв из одного слова полу- чить другое. 15. Замените в строке все вхождения одной подстроки на другую. 16. Проверьте баланс скобок “(” и “)” в строке. 17. Строка является фрагментом программы на языке С. Выведите на экран все содержащиеся в ней комментарии. 18. Подсчитайте количество различных символов в строке. 19. Выведете на экран все слова строки, если они отделяются друг от друга произвольным количеством пробелов или знаков препинания. 20. Дана строка, представляющая собой корректное арифметическое выражение, содержащее только имена величин, константы, скобки и знаки арифметических действий (плюс и минус). Раскройте скобки. 32
§9. Структуры Как и массивы, структуры представляют собой совокупность пере- менных, объединенных общим именем. Однако, элементы структуры мо- гут иметь различные типы, а доступ к ним осуществляется по имени. При описании структуры указываются имя типа структуры, описыва- ются входящие в неё элементы, а также список имён объявляемых струк- турных переменных и указателей: struct имя типа {описание элементов} список; Входящие в структуру элементы описываются, как и обыкновенные переменные. Одну из частей описания структуры (имя типа, описание эле- ментов или список переменных) можно опускать: struct Complex { double Re, Im; }; struct Complex a, *b; struct { double xl, yl, x2, y2; } linel, line2; В языке C++ ключевое слово struct при объявлении переменных разрешается опускать. В языке С для подобного сокращения записи ис- пользуется оператор typedef, определяющий синоним для описания типа: typedef struct { double Re, Im; } Complex; Complex a, *b; Инициализация структур выполняется путём перечисления значений входящих в структуру элементов также, как при инициализации массивов: Complex zero={0,0}, опе={1,0}; Для доступа к элементам структуры используется оператор . (точка): а.Re = 2.3; а.Im = -4.5; В языке С имеется специальный оператор -> для доступа к элементам структуры с помощью указателя на неё: b->Re = 6.7 вместо (*b).Re = 6.7 Структурные переменные могут передаваться в функции как по зна- чению, так и по ссылке, а также возвращаться из функций. Пример. Среднее арифметическое двух комплексных чисел Complex Average (Complex * A, Complex * В) { Complex С={ (A->Re+B->Re)/2, (A->Im+B->Im)/2 }; return C; } 33
Лабораторная работа №7 При решении задач 1 10 используйте структурный тип данных, опи- сание которого, а также прототипы функций для ввода, вывода и обра- ботки данных этого типа разместите в заголовочном файле, а сами функции - в отдельном файле. 1. Найдите векторное произведение двух векторов в трехмерном про- странстве. 2. Найдите произведение двух матриц 2x2. 3. Найдите смешанное произведение трех векторов в трехмерном про- странстве. 4. Даны два отсчета времени (час:минута:секунда). Определить про- межуток времени между ними. 5. Найдите угол между двумя векторами в трехмерном пространстве. 6. Найдите координаты точки в сферической системе координат, если известны ее координаты в прямоугольной декартовой системе координат. 7. Найдите координаты точки пересечения двух прямых, уравнения которых заданы в общем виде. 8. Найдите расстояние между двумя окружностями (наименьшее из возможных расстояний между двумя точками фигур). 9. Даны два прямоугольника со сторонами, параллельными осям ко- ординат. Найдите площадь их пересечения. 10. Найдите частное двух комплексных чисел. 34
§10. Ввод-вывод в языке С Система ввода-вывода языка С предназначена для работы с последо- вательностями символов, называемых потоками. Потоку может соответст- вовать как дисковый файл, так и принтер, терминал и т.п. Понятие “поток” позволяет разрабатывать гибкий и эффективный ввод-вывод, который не зависит от используемых файлов или встроенного оборудования. Двоичные потоки однозначно соответствуют последовательности символов на внешнем устройстве. В отличие от них, при работе с тексто- выми потоками происходит определённое преобразование некоторых сим- волов, например, символа конца строки. Из-за такого преобразования чис- ло прочитанных или записанных символов может не совпадать с числом символов во внешнем устройстве. Функции для работы с потоками описаны в заголовочном файле stdio.h. Все эти функции используют указатель на структуру FILE. Эта структура создаётся автоматически и содержит различную информацию о потоке (например, указатели на буферы чтения и записи, позиция файлово- го курсора, атрибуты состояния файла и т.п.). FILE * fopen (char * name, char * mode) — открывает файл, создаёт и заполняет структуру file и возвращает указатель на неё. В слу- чае ошибки функция возвращает нулевое значение указателя (null). Строка mode описывает режим работы с открываемым файлом и мо- жет состоять из символов г (для чтения), w (для записи), а (для дополне- ния), + (расширенный режим), b (двоичный режим), t (текстовый режим). Пример. Открытие текстового файла для чтения FILE * f; if ((f = fopen("readme.txt", "rt")) == NULL) { puts("Ошибка"); return 1; } int fclose (FILE * f) - используется для закрытия файла, ранее открытого с помощью fopen. Функция сохраняет в файл данные, находя- щиеся в дисковом буфере и выполняет операцию системного уровня по за- крытию файла. В случае нормального завершения функция возвращает значение 0, в случае ошибки - EOF. int feof (FILE * f) - возвращает ненулевое значение, если была попытка чтения после конца файла. 35
int f getc (FILE * f) - считывает один символ из файла. int fputc(int с, FILE * f) - записывает один символ в файл. char * fgets(char * s, int n, FILE * f) — из файла/считы- вает строку (не более и символов) и заносит её в символьный массив s. int fputs (char * s, FILE * f) - выводит строку в файл. Функции fgetc, fputc, fputs в случае ошибки возвращают значе- ние EOF, а функция fgets - нулевое значение указателя. Программе доступны два предопределённых файловых указателя, описанных в заголовочном файле stdio.h: stdin (стандартный ввод) и stdout (стандартный вывод). Эти указатели неявно используются в функциях консольного ввода-вывода: int getchar ()-считывает один символ из файла stdin. int putchar (int с) - выводит один символ в файл stdout. char * gets (char * s) - считывает строку из файла stdin, при этом удаляет введённый символ конца строки. Под символьный массив s должно быть зарезервировано достаточное количество памяти. int puts (char * s) - выводит строку и дополнительный символ конца строки в файл stdout. Пример. Посимвольный вывод текстового файла на экран do { int Char=fgetc(f); if (Char!=EOF) putchar(Char); } while (!feof(f)); В языке С имеются функции, выполняющие форматированный ввод- вывод, т.е. они могут считывать и выводить данные в различных форматах, которыми можно управлять. Для форматированного вывода используются три функции: int fprintf(FILE * f, char * format, . . .) int printf( char * format, . . .) int sprintf(char * s, char * format, . . .) Эти функции используются для вывода базовых типов данных в файл (fprintf), стандартный поток вывода stdout (printf) или символьный массив (sprintf). Все они возвращают количество выведенных символов. Строка format должна состоять из символов, непосредственно выво- димых в поток, и управляющих последовательностей, вместо которых под- ставляется текстовое представление соответствующих аргументов. 36
Управляющие последовательности состоят из % и символа формата: % с - отдельный символ % i, %d - знаковое целое десятичное число %и - беззнаковое целое десятичное число % о - беззнаковое целое восьмеричное число %х, %х - беззнаковое целое шестнадцатеричное число %f, %е, %Е, %g, %G - вещественное число %s - строка символов Между знаком % и символом формата могут находится дополнитель- ные модификаторы формата, задающие минимальную ширину поля вы- вода, выравнивание, точность и т.п. Пример. Вывод корней квадратного уравнения с точностью 3 знака printf("xl=%.3f, x2=%.3f", xl, x2) ; /* xl=2.718, x2=-3.141 */ Для форматированного ввода используются три функции: int fscanf(FILE * f, char * format, . . .) int scanf( char * format, . . .) int sscanf(char * s, char * format, . . .) Эти функции используются для ввода базовых типов данных из файла (fscanf), стандартного потока ввода stdin (scanf) или символьного массива (sscanf). Все они возвращают количество введённых значений. Форматная строка описывает порядок чтения значений из потока и может содержать как управляющие последовательности, сообщающие о типе читаемых данных, так и обычные символы, которые необходимо от- бросить при чтении из потока. Если указанный в форматной строке символ не найден, то выполнение функций прекращается. Символам-разделителям (пробел, табуляция и т.п.) в потоке может соответствовать любое количе- ство (в т.ч. и ноль) подобных символов. Остальные аргументы функций должны являться адресами перемен- ных, в которые и будут заноситься прочитанные значения. Пример. Чтение двух значений переменных scanf("xl=%d, x2=%d", &xl, &х2); Необходимо следить за тем, чтобы управляющие последовательности, указанные в форматной строке, полностью соответствовали количеству и типу остальных аргументов перечисленных функций. 37
§11. Ввод-вывод в языке C++ Для организации потокового ввода-вывода в языке C++ используется объектно-ориентированный подход. В заголовочном файле iostream.h описано несколько классов, используемых для этих целей. Класс ios содержит ряд свойств и методов, необходимых для управ- ления потоковым вводом-выводом. Например, для определения состояния потока можно использовать методы good (), fail () или eof (), которые возвращают ненулевое значение при отсутствии ошибок, при наличии ошибок или при попытке чтения после конца файла, соответственно. Класс istream обеспечивает форматированный ввод базовых типов данных из потока с помощью оператора », а также содержит методы для неформатированного ввода данных, например: get (char & с) - считывает один символ из потока. getline(char * s, int n) - считывает из потока строку (не бо- лее п символов). read (char * s, int n) - считывает из потока ровно п символов. Для ввода данных со стандартного потока ввода используется объект cin, являющийся экземпляром класса istream: char s [100]; cin.getline (s,10 0); Класс ©stream обеспечивает форматированный вывод базовых типов данных в поток с помощью оператора «, а также содержит методы для неформатированного вывода данных, например: put (char с) - выводит один символ в поток. write (char * s, int n) - выводит в поток п символов. Для вывода данных в стандартный поток вывода используется объект cout, являющийся экземпляром класса ©stream: cout.write(s,strlen(s) ) ; Класс iostream объединяет в себе возможности, предоставляемые классами istreamn ©stream по вводу-выводу данных. От класса iostream порождён класс fstream, отвечающий за вы- полнение файлового ввода-вывода и описанный в заголовочном файле fstream.h. В этом классе доступны следующие методы: 38
open (char * name, int mode) — открыть файл. Число mode зада- ёт режим работы с файлом и может принимать значения ios: : in (для чтения), ios : : out (для записи), ios : : арр (для дополнения) и т.д.; close () - закрыть файл. Пример. Ввод массива из файла fstream f; f.open("с:\\student\\file.dat", ios::in); int N, A [10]; f » N; for (int i=0; i<N; i++) f » A[i]; f.close (); Форматирование вывода в языке C++ может выполняться разными способами, например, с помощью соответствующих методов класса ios: width (int w) - установить ширину поля вывода. precision (int р) - задать количество знаков после запятой. fill (char с) - задать символ-заполнитель setf(int f) - установить флаги форматирования и т.д. Флаги форматирования управляют основанием системы счисления для выводимых целых чисел (ios : : dec, ios : : oct, ios : :hex), выравни- ванием (ios : : left, ios : : right), способом вывода вещественных чисел (ios : : scientific, ios : : fixed) и т.д. Пример. Вывод шестнадцатиричного числа из шести цифр cout.setf(ios::hex | ios::right | ios::uppercase); cout.fill (’0’); cout.width(6); cout « 2748; /* 000ABC */ Другим способом форматирования вывода в C++ является использо- вание специальных объектов - манипуляторов, описанных в заголовочном файле iomanip. h: dec, oct, hex — установить флаги ios : : dec, ios : : oct, ios : : hex setw (int w) - установить ширину поля вывода. setfill (char с) - установить символ-заполнитель. setprecision (int р) - задать количество знаков после запятой setiosflags (int f) - установить флаги форматирования Пример. Вывод шестнадцатиричного числа из шести цифр cout « setiosflags(ios::right | ios::uppercase) « hex « setfill(’O’) « setw(6) « 2748; 39
Лабораторная работа №8 Выполните задания 1 10 двумя способами: с использованием для ра- боты с файлами функций языка С и объектов языка C++. 1. Имеется текстовый файл с результатами марафона в виде: стартовый номер Фамилия И.О. чч:мм:сс Найдите победителя марафона. 2. Имеется текстовый файл с программой конференции в виде: чч.мм Фамилия И.О. Название доклада Найдите название самого продолжительного доклада. 3. Имеется текстовый файл со списком дождливых дней за год в виде: день месяц количество осадков Найдите самый дождливый месяц. 4. Имеется текстовый файл с курсами обмена валют в банках города: "Название банка" Покупка: ????. Продажа: ????. Найдите банк с самым выгодным курсом покупки (продажи) валюты. 5. Имеется текстовый файл с протоколом работы в сети Internet: № Дата Начало Время Получено Отправлено Найдите общее время работы в сети и объём переданной информации. 6. Имеется текстовый файл, описывающий направление и силу ветра за год в виде: день месяц направление ветра сила ветра Найдите преобладающее направление ветра в каждом месяце. 7. Имеется текстовый файл, описывающий банковские операции по вкладу в виде: дата операция сумма Найдите остаток вклада. 8. Имеется текстовый файл с результатами сессии в виде: Фамилия И.О. 0ценка1 0ценка2 ОценкаЗ 0ценка4 Найдите студента с наименьшим средним баллом по итогам сессии. 9. Имеется текстовый файл с распечаткой телефонных разговоров: дата время номер продолжительность Найдите общую стоимость разговоров. 10. Дан текстовый файл, содержащий дамп двоичного файла в виде: 0000: 23 69 6Е 63 6С 75 64 65 | 20 22 69 6F 73 74 72 65 #include "iostre Восстановите исходный файл. 40
§12. Динамическое распределение памяти К динамическому распределению памяти прибегают тогда, когда не- возможно заранее определить количество и размер переменных, с которы- ми должна работать программа. В этом случае память для переменных по мере надобности выделяется из специальной области памяти (“кучи”) и ос- вобождается, когда необходимость в ней исчезает. Таким образом, по мере работы программы одна и та же область памяти может быть использована для хранения различной информации. Функции для выделения и освобождения динамически распределяе- мой памяти описаны в заголовочном файле stdlib .h. void * malloc (size_t size) - выделяет область памяти для раз- мещения динамической переменной размером size байт. Для определения размера памяти, занимаемой переменными, может быть использован опе- ратор sizeof. void * calloc(size_t n, size_t size) — выделяет область памяти для размещения массива из п элементов по size байтов каждый. void * realloc (void * pointer, size_t size) — изменяет размер области памяти, выделенной для хранения динамической перемен- ной, расположенной по адресу pointer (при этом переменная может быть перенесена по другому адресу). void free (void * pointer) - освобождает область памяти по ад- ресу pointer, выделенную ранее вызовами malloc, calloc или realloc. Функции calloc, malloc, realloc возвращают нетипизированный указатель (void *), содержащий адрес динамической переменной. В слу- чае ошибки, возвращаемое значение равно нулевому указателю (null). Преобразование нетипизированного указателя к указателю требуемо- го типа в языке С может осуществляться автоматически. Однако в языке C++ подобное преобразование необходимо выполнять явно. Пример. Выделение памяти под динамическую переменную double * а = (double *) malloc(sizeof(double)); Пример. Выделение памяти под динамический массив int * b = (int *) calloc(N,sizeof(int)); Пример. Увеличение размера массива в два раза b = (int *) realloc(b,2*N*sizeof(int)); 41
Для работы с динамически распределяемой памятью в языке C++ бы- ло введено два специальных оператора. Оператор new используется для динамического распределения памя- ти. В зависимости от формы записи, этот оператор может выделять память под динамическую переменную, выделять память и инициализировать ди- намическую переменную, выделять память под динамический массив: указатель = new тип; указатель = new тип (значение); указатель = new тип [размер]; Оператор new автоматически вычисляет требуемый размер памяти и сразу возвращает указатель нужного типа. Для освобождения памяти, занимаемой динамической переменной или динамическим массивом, созданными с помощью оператора new, ис- пользуется оператор delete: delete указатель на переменную; delete [] указатель на массив; Пример. Создание двумерного динамического массива int ** а = new int*[N]; for(int i=0;i<N;i++) a[i]=new int [M]; Пример. Удаление двумерного динамического массива из памяти for(int i=0;i<N;i++) delete [] a[i]; delete [] a; 42
Лабораторная работа №9 Выполните задания 1 10 двумя способами: с использованием функций языка С и операторов языка C++ для работы с динамически распределяе- мой памятью. 1. Дана произвольная строка, слова в которой разделены пробелами. Напечатать все различные слова в строке. 2. Постройте первые N строк треугольника Паскаля. 3. Данную числовую последовательность разбейте на упорядоченные подпоследовательности. 4. Дана произвольная строка, слова в которой разделены пробелами. Напечатать все слова этой строки в порядке возрастания их длины. 5. Данную числовую последовательность разбейте на подпоследова- тельности, которые являются арифметическими прогрессиями. 6. Постройте первые N “строк Фибоначчи” - последовательность из строк, каждая из которых получена конкатенацией двух предыдущих. 7. Из данной числовой последовательности выделите все ненулевые подпоследовательности. 8. Дана произвольная строка, слова в которой разделены пробелами. Напечатать все слова, которые встречаются в исходной строке ровно один раз. 9. Данную числовую последовательность разбейте на подпоследова- тельности, которые являются геометрическими прогрессиями. 10. Дана произвольная строка, слова в которой разделены пробелами. Напечатать все слова строки в алфавитном порядке. Выполните задания 11 20 лабораторной работы № 4 двумя способа- ми: с использованием функций языка С и операторов языка C++ для ра- боты с динамически распределяемой памятью. 43
§13. Линейные списки Динамическое распределение памяти часто используется при связном представлении различных структур данных (не путать со структурами в языке С), таких как линейные списки, кольцевые списки, деревья и т.д. Элементы линейного списка расположены, в некотором смысле, по- следовательно. Над линейными списками можно выполнять такие опера- ции, как доступ к элементу с целью проверки или модификации, вставка нового элемента, удаление элемента и т.д. Линейные списки, в которых операции вставки и удаления элементов чаще всего выполняются на краях, получили специальные названия: стек (операции вставки и удаления обычно выполняются только на одном из концов списка), очередь (операции вставки обычно выполняются на одном из концов списка, а удаления - на втором) или дек (операции вставки и удаления выполняются на обоих концах списка). При связном представлении линейного списка его элементы описы- ваются с помощью структур, содержащих информационную часть (непо- средственно данные) и адресную часть (ссылки на предыдущий и/или сле- дующий элементы). Необходимое количество ссылок (одна или две) опре- деляется по тем операциям, которые должны выполняться над элементами списка. Так, стек или очередь обычно представляются с помощью одно- связного списка, а дек - двусвязного: struct Stack {double Data; struct Stack * Next; }; struct Queue {double Data; struct Queue * Next; }; struct Deq {double Data; struct Deq * Prev, * Next; }; Возможные способы организации стека (а), очереди (б) и дека (в) с помощью этих структур показаны на рисунке. а) Тор ► Data Data Next Next б) In ► Data Data 0 Next в) Left ► Data Data 0 Left / / Right Right / 44
Доступ к структурам осуществляется с помощью переменных-указате- лей, ссылающихся на концы списков: struct Stack * Top; struct Queue * In, * Out; struct Deq * Left, * Right; Концы списка обозначаются с помощью специального значения ука- зателя, чаще всего - нулевого указателя null. Чтобы избежать потерь памяти при выполнении операций над элемен- тами связных структур данных, необходимо строго следить за последова- тельностью вызовов функций динамического распределения памяти и из- менением ссылок. Пример. Добавление элемента в очередь Queue * temp = (Queue *) malice (sizeof(Queue)); temp -> Data = 3.1415; temp -> Next = NULL; In -> Next = temp; In = temp; Пример. Удаление элемента из очереди Queue * temp = Out; Out = Out -> Next; free(temp); Просмотр всех элементов линейного списка выполняется, начиная с одного из концов, путём последовательного изменения вспомогательной переменной-указателя до достижения второго конца списка. В качестве ус- ловия окончания цикла удобно использовать нулевое значение указателя. Пример. Сумма всех элементов очереди Queue * temp = Out; double Sum = 0; while (temp) { Sum += temp -> Data; temp = temp -> Next; } 45
Лабораторная работа №10 Выполните задания 1 10 тремя способами: с использованием стека, очереди и дека. Описание соответствующих типов и набор всех необходимых для их обработки функций (добавление, извлечение элемента и т.д.) разместите в отдельных файлах. Подготовьте соответствующие заголовочные фай- лы. 1. Даны две упорядоченные последовательности. Сформируйте из них третью, так же упорядоченную. 2. Даны две последовательности. Проверьте, можно ли одну из них получить вычеркиванием некоторых элементов другой. 3. Из заданной последовательности символов сформируйте новую, в которой из нескольких одинаковых подряд идущих символов оставлен только один. 4. Из заданной числовой последовательности сформируйте новую, со- держащую все элементы исходной, кроме нулевых. 5. Из заданной числовой последовательности сформируйте новую, в которой сначала идут все отрицательные элементы исходной, а затем - все положительные (с сохранением первоначального порядка). 6. Получите самую длинную не содержащую нулей подпоследова- тельность заданной числовой последовательности. 7. Из заданной числовой последовательности сформируйте новую, не содержащую элементов исходной с максимальным по модулю значением. 8. Проверьте, является ли заданная последовательность символов симметричной. 9. Сформируйте числовую последовательность, в которой наибольшие и наименьшие элементы исходной переставлены местами. 10. Проверьте, является ли данная числовая последовательность упо- рядоченной. 46
§14. Объектно-ориентированное программирование Основное отличие языка программирования C++ от языка С заключа- ется в поддержке объектно-ориентированного программирования (ООП). Как известно, объектами принято называть конструкция языка про- граммирования, объединяющие в себе и данные, и код для их обработки. В языке C++ реализация объектов носит название “классы”. Описание классов При описании класса необходимо определить данные, необходимые для представления объекта, и множество действий для работы с этим объ- ектом. Описание класса похоже на описание структуры и имеет три раздела: закрытый (private), защищённый (protected) и открытый (public). Доступ к членам класса, описанным в разделах private и protected, возможен только из функций-членов описываемого класса. Члены класса, описанные в разделе public, доступны из любой функции программы. Подобный механизм позволяет предохранить объект от неправильного ис- пользования. Пример. Описание класса для реализации одномерного массива class Vector { protected: int N; double mas[10]; public: void SetLength(int); int GetLength(); void Set (int,double) ; double Get(int); }; После описания класса, его имя можно использовать для создания пе- ременных-объектов : Vector vect; Для доступа к членам класса также, как и для доступа к элементам структур, используются операторы . и ->. Пример. Вывод массива на экран for(i=0;i<vect.GetLength();i++) cout«vect.Get(i); 47
Для определения функций-членов класса необходимо с помощью опе- ратора области видимости : : указать, к какому классу они относятся: void Vector :: SetLength(int value) { if (value<=10) N = value; } Обычно описание класса размещается в заголовочном файле, а опре- деление функций-членов класса - в отдельном файле. Внутри функций-членов класса идентификаторы относятся к тому объекту, для которого функции были вызваны. На этот объект внутри функций-членов класса можно также ссылаться с помощью неявно переда- ваемого указателя this: void Vector :: SetLength(int value) { if (value<=10) this->N = value; } Наследование классов В языке C++ разрешается при описании одного класса использовать ранее описанный класс. Общая форма записи при наследовании имеет вид: class производный класс: [доступ] базовый класс { // тело производного класса }; Производный класс включает в себя все члены базового класса и до- бавляет новые, специфические для производного класса. Тем самым воз- можно избежать многократного переписывания текста при описании схо- жих классов. Необязательный параметр доступ определяет, к какому раз- делу производного класса будут относиться открытые и защищённые чле- ны базового класса: к исходным разделам (public), к защищённому (pro- tected) или к закрытому (private). При наследовании проявляется разница между закрытыми (private) и защищёнными (protected) членами класса. Закрытые члены базового класса всегда остаются закрытыми в производном классе и недоступны из его функций. В отличие от них, защищённые члены базового класса дос- тупны функциям-членам производного класса. Пример. Массив с сортировкой class SortedVector: public Vector { public: void Sort() ; }; 48
В языке C++ допускается множественное наследование, когда произ- водный класс наследует атрибуты нескольких базовых классов одновре- менно: class производный класс: [доступ] базовый класс1, [доступ] базовый_класс2, [доступ] базовый_классЫ { // тело производного класса }; Если в нескольких базовых классах есть элементы с одинаковым име- нем, то при попытке доступа к ним возникает неоднозначность. Для устра- нения этой неоднозначности необходимо с помощью оператора области видимости : : указать конкретный базовый класс, чей элемент необходи- мо использовать. Так же следует поступать и в случае, когда в производ- ном классе описаны (перегружены) функции с теми же именами, что и в базовом классе. Дружественные функции Функция, не являющаяся членом класса, может иметь доступ к его за- крытым и защищённым членам, если при его описании она объявлена как дружественная: class имя класса { // тело класса friend дружественная функция(параметры); }; Чаще всего дружественные функции используются, когда одна и та же функция должна использовать закрытые члены двух различных классов: class class2; class classl { // тело первого класса friend void func(classl, class2); }; class class2 { // тело второго класса friend void func (classl, class2); }; void func(classl cl, class2 c2) { // тело дружественной функции }; 49
Для успешной компиляции необходимо, чтобы класс class2 был объявлен до первого появления использующей его дружественной функ- ции fun с. Поэтому до объявления класса classl идет предварительное (пустое) объявление класса class2. Функции, объявленные дружественными для базового класса, не бу- дут являться таковыми для производного класса. Лабораторная работа №11 В задачах 1 10 необходимо разработать заголовочный файл, содер- жащий описание иерархии из двух классов, включающих функции для вво- да-вывода параметров объектов и некоторую заданную функцию. Опре- деление функций-членов класса разместите в отдельном файле. Составьте программу, демонстрирующую работу с каждым из под- готовленных классов. 1. Базовый класс - круг; производный класс - кольцо; функция - пло- щадь. 2. Базовый класс - сфера; производный класс - цилиндр; функция - объём. 3. Базовый класс - треугольник; производный класс - трапеция; функ- ция - площадь. 4. Базовый класс - цилиндр; производный класс - конус; функция - объём. 5. Базовый класс - сфера; производный класс - цилиндр; функция - площадь поверхности. 6. Базовый класс - круг; производный класс - сектор; функция - пло- щадь. 7. Базовый класс - куб; производный класс - прямоугольный паралле- лепипед; функция - объём. 8. Базовый класс - прямоугольник; производный класс - трапеция; функция - площадь. 9. Базовый класс - сфера; производный класс - конус; функция - объ- ём. 10. Базовый класс - цилиндр; производный класс - конус; функция - площадь поверхности. 50
§15. Конструкторы и деструкторы Конструкторы и деструкторы - это функции-члены класса, которые имеют особый смысл. Конструкторы Конструкторы определяют, каким образом будет создан новый объ- ект, как будет проводиться распределение памяти и инициализация объек- та. Вызов конструктора выполняется автоматически при создании объекта. Имя функции-конструктора должно совпадать с именем класса, а тип воз- вращаемого значения не указывается. имя класса() { // тело конструктора } Пример. Конструктор, заполняющий вектор нулями Vector() { N=10; for (int i=0;i<N;i++) m[i]=0; } Перед вызовом конструктора объекта производного класса автомати- чески вызываются конструкторы всех его базовых классов в том порядке, в котором классы были описаны. Деструкторы Деструкторы описывают действия, которые необходимо выполнить для корректного уничтожения объектов класса, созданных перед этим кон- структором (например, для освобождения динамически распределямой па- мяти, используемой объектом). Имя функции-деструктора совпадает с именем класса, перед которым стоит знак тильда имя_класса () { // тело деструктора } Вызов деструктора происходит при явном уничтожении объекта с по- мощью оператора delete или при неявном уничтожении объекта, когда заканчивается блок кода, в котором был описан объект. Пример. Вектор с динамически распределяемой памятью class Vector { protected: int N; double * mas; public: Vector(); Vector(); // размер массива // указатель на массив // конструктор // деструктор 51
Vector :: Vector() // конструктор { N=10; mas=new double[N]; } Vector :: -Vector() // деструктор { delete [] mas; } При уничтожении объекта производного класса сначала вызывается его деструктор, а потом деструкторы всех базовых классов. Параметризованные конструкторы В языке C++ допускается использование параметризованных конст- рукторов, т.е. конструкторов, которым можно передавать аргументы для уточнения действий по созданию и инициализации объектов. Создание объекта с помощью параметризованного конструктора в общем виде записывается так: имя класса имя объекта (параметры); Пример. Конструктор для создания вектора нужной размерности Vector :: Vector(int size) { N=size; mas=new double[N]; } На конструкторы распространяются все правила языка C++ по описа- нию функций, т.е. у одного класса может быть несколько конструкторов с разными наборами параметров, а также допускается использование значе- ний параметров по умолчанию, например, так: Vector :: Vector(int size=10) { N=size; mas=new double[N]; } Vector a (5); // вектор из пяти элементов Vector b; // вектор из десяти элементов Если какие-либо параметры необходимо передать конструктору базо- вого класса, то его вызов должен явно указываться между заголовком и те- лом конструктора производного класса: имя производного класса (параметры) : имя базового класса (параметры) { // тело конструктора производного класса } Пример. Вызов конструктора базового класса SortedVector(int size) : Vector(size) { } Аналогично может выполняться инициализация переменных и объек- тов, являющихся членами класса. 52
Конструктор копирования Конструктор копирования - особый вид конструктора, который ис- пользуется при инициализации одного объекта другим (например, при соз- дании нового объекта, при передаче объекта в функцию или из функции). По умолчанию в этих случаях выполняется побитовое копирование, т.е. в новом объекте создаётся точная копия исходного объекта. В некото- рых случаях простого копирования значений недостаточно. Например, ес- ли исходный объект использует динамически распределяемую память, то вновь создаваемый объект будет содержать указатель на ту же самую об- ласть памяти, а деструкторы при уничтожении объектов будут пытаться освободить её несколько раз. Для решения подобных проблем и использу- ется конструктор копирования, который вызывается компилятором при необходимости выполнить инициализацию одного объекта другим. Конструктор копирования описывается как параметризованный кон- структор с единственным аргументом - ссылкой на объект, с помощью ко- торого необходимо выполнить инициализацию вновь создаваемого объек- та. Общая форма записи конструктора копирования имеет вид: имя класса (const имя класса ^объект) { // тело конструктора копирования } Пример. Конструктор копирования для вектора vector :: vector (const vector sold) { N=old.N; mas=new double[N]; for(int i=0;i<N;i++) mas[i]=old.mas[i]; } Конструктор копирования вызывается только при инициализации од- ного объекта другим. При выполнении операций присваивания всё равно по умолчанию выполняется побитовое копирование объектов. 53
§16. Перегрузка операторов Перегрузка операторов - это механизм языка C++, благодаря которо- му пользовательские классы становятся такой же частью языка, что и стандартные типы данных. Благодаря перегрузке большинство операторов языка можно применять к объектам пользовательских классов. Для всех остальных типов данных действие операторов сохраняется без изменения. Для перегрузки оператора описывается функция следующего вида: возвращаемый тип operator # (список аргументов) Вместо символа # подставляется перегружаемый оператор, такой как +, - ит.д. При перегрузке унарного оператора аргументом функции должен яв- ляться единственный объект пользовательского класса, при перегрузке би- нарного оператора таковым должен являться хотя бы один из двух аргу- ментов. Если функция-оператор должна обращаться к закрытым или за- щищённым членам классов, она должна быть объявлена при описании этих классов как дружественная. Пример. Перегрузка оператора - (изменение знака). Vector operator - (Vector v) { for(int i=0;i<z.N;i++) v.mas[i] = -v.mas[i]; return v; } В этом примере изменяется и возвращается копия объекта, переданная в функцию, а исходный объект остаётся без изменений. При описании бинарных функций-операторов, следует учитывать все возможные случаи их использования. Пример. Перегрузка оператора * // умножение вектора на число Vector operator * (Vector v, double c) { for(int i=0;i<v.N;i++) v.mas[i] *= c; return v; } // умножение числа на вектор Vector operator * (double c, Vector v) { for(int i=0;i<v.N;i++) v.mas[i] *= c; return v; } 54
Если функция-оператор должна модифицировать передаваемый ей объект, то необходимо использовать передачу аргумента по ссылке. Пример. Перегрузка оператора ++ Vector operator ++ (Vector & v) { for(int i=0;i<v.N;i++) v.mas[i]++; return v; } Унарные операторы, а также бинарные операторы, у которых первый операнд является объектом пользовательского класса, могут быть функ- циями-членами этого класса. В этом случае в функцию неявно, с помощью указателя this, передаётся первый операнд бинарного оператора или единственный операнд унарного оператора. Некоторые операторы, напри- мер, = или [], всегда должны описываться как функции-члены пользова- тельского класса. Пример. Перегрузка бииариого оператора [ ] double Vector::operator[](int i) { if (i>=0 && i<N) return mas[i]; else return 0; } Перегрузка операторов « и » позволяет использовать их для вы- полнения операций ввода-вывода с объектами пользовательских классов. В общем виде соответствующие функции записываются так: ostream& operator « (ostream &S, тип объект) { S « ... return S; } istream& operator » (istream &S, тип ^объект) { S » ... return S; } Внутри этих функций для выполнения ввода-вывода необходимо ис- пользовать переменные-потоки, переданные по ссылке, а не фиксирован- ные объекты cin и cout. Также эти функции должны возвращать ссыл- ку на исходный поток, чтобы было возможно записать несколько операций ввода-вывода в одну цепочку. 55
§17. Исключения Исключительными ситуациями или исключениями принято называть нарушения в нормальном ходе работы программы, вызванные ошибками в данных, которыми оперирует программа (внутренние исключения), а также сбоями в работе аппаратного обеспечения компьютера или операционной системы (внешние исключения). О некоторых внешних исключениях программа может узнать через специальные значения, возвращаемые стандартными функциями. Эти, а также все внутренние исключения могут быть предусмотрены и соответст- вующим образом обработаны, например, с помощью условных операторов. Пример. Обработка исключительных ситуаций при чтении файла if ((f=fopen("massiv.dat","rt"))==NULL) puts("Ошибка при открытии файла"); else if (fscanf(f,"%d",&N)==EOF) puts("Ошибка при чтении файла"); else if (N<1 || N>100) puts("Неверное число N"); else { /* ... и т.д. */ } Подобный способ обработки исключительных ситуаций излишне пе- регружает программу. Кроме того, затруднительной становится обработка исключительных ситуаций, возникших внутри вызываемых функций. Язык программирования C++ содержит специальную конструкцию try . . . catch, которая позволяет упорядочить обработку исключитель- ных ситуаций. В общем виде эта конструкция записывается так: try { // операторы, при выполнении которых возможно // возникновение исключительных ситуаций } catch (тип1 переменная) { // обработка исключений первого типа } catch (тип2 переменная) { // обработка исключений второго типа } // и т.д. catch (...) { // обработка оставшихся исключений } 56
Блоки catch служат для обработки исключительных ситуаций, вы- явленных внутри блока try. Для перехода к тому или иному блоку catch необходимо выполнить оператор throw выражение; Оператор throw может располагаться не только непосредственно в блоке try, но и внутри любой вызываемой в этом блоке функции. Тип выражения, указанного в операторе throw, определяет, какой именно бу- дет вызван блок catch, а само значение выражения используется для инициализации переменной, описанной в заголовке этого блока. Послед- ним должен идти блок catch (...), который служит для обработки ис- ключений любого типа. Если блок catch (...) отсутствует, а тип выра- жения в операторе throw не соответствует ни одному из блоков catch, то происходит аварийное завершение работы программы. После обработки исключительной ситуации, управление передаётся операторам, следующим за всей конструкцией. То же происходит и после выполнения блока try, если в нём ни разу не был выполнен оператор throw. Пример. Обработка исключительных ситуаций при чтении файла. try { f.open("massiv.dat", ios::in); if (f.failO) throw 100; f » N; if (f.failO) throw 200; if (N<1 || N>100) throw "неверное значение N"; // и т.д. } catch (int code) { cout « "Ошибка ввода-вывода, код " « code; } catch (char * str) { cout « "Ошибка: " « str; } catch (...) { cout « "Неизвестная ошибка"; } 57
Лабораторная работа №12 В задачах 1 10 необходимо разработать заголовочный файл, содер- жащий описание соответствующего класса, включающего все необходи- мые для решения задачи данные и операторы («, » и т.д.). Определение функций-членов класса разместите в отдельном файле. Предусмотрите обработку исключительных ситуаций. Подготовьте программу, демонстрирующую работу с подготовлен- ным классом. 1. Найдите сумму и произведение двух комплексных чисел. 2. Найдите сумму и векторное произведение двух векторов в трехмер- ном пространстве. 3. Найдите сумму и произведение двух матриц 2x2. 4. Найдите сумму и разность двух многозначных целых чисел. 5. Найдите сумму и произведение двух несократимых рациональных дробей. 6. Найдите разность и частное двух комплексных чисел. 7. Найдите разность и скалярное произведение двух векторов в трех- мерном пространстве. 8. Найдите разность и частное двух матриц 2x2. 9. Найдите произведение двух многозначных целых чисел. 10. Найдите разность и частное двух несократимых рациональных дробей. 58
§18. Шаблоны Многие алгоритмы логически одинаковы вне зависимости от типа данных, с которыми они оперируют. Например, алгоритм сортировки од- номерного массива не зависит от типа его элементов и записывается оди- наково и для целых, и для дробных чисел. В языке C++ подобные алгоритмы могут быть описаны с помощью шаблонов функций или шаблонов классов. Типы данных, которыми опе- рируют функция или класс, являются параметрами шаблонов. Формальные типы параметров перечисляются перед описанием функции или класса с помощью ключевого слово template: template <class тип1, class тип2, ...> Конкретная версия функции автоматически создаётся компилятором, когда он встречает вызов функции. При этом формальные типы парамет- ров заменяются компилятором на типы фактических аргументов функции. Пример. Шаблон функции для обмена значений двух переменных template <class Tip> void swap(Tip &x, Tip &y) { Tip z=x; x=y; y=z; } int a=5, b=6; double x=1.4, y=3.7; swap(a,b); // вызов функции swap(int,int); swap(x,y); // вызов функции swap(double,double); swap(a,x); // ошибка: вызов swap(int,double); При необходимости создать специфическую версию функции для не- которых типов данных, шаблон можно перегрузить явным образом: void swap (char * a, char * b) { char * temp=new char[strlen(a)+1]; strcpy(temp,a); strcpy(a,b); strcpy(b,temp); delete [] temp; } Описание шаблона класса выполняется аналогично, т.е. все необхо- димые алгоритмы должны работать с формальными типами данных. Фак- тические типы данных нужно указать при создании экземпляров класса: имя класса<список фактических типов> имя объекта; В качестве фактических типов можно указывать любые стандартные типы и пользовательские классы, для которых описаны все используемые в шаблоне функции и операции. 59
Поскольку функции-члены шаблона класса также являются шаблона- ми, то при их описании вне класса необходимо использование ключевого слова template: template <class формальный тип> возвр тип имя класса<формальный тип>::функция { } Пример. Шаблон класса “окружность” template <class Tip> class Circle { public: Tip x,y,R; Tip Length() { return 2*M_PI*R; } Tip Square(); }; template <class Tip> Tip Circle<Tip>::Square() { return M_PI*R*R; } Circle<float> A; Circle<double> B; Лабораторная работа №13 При решении задач 1 10 используйте самостоятельно разработан- ный шаблон класса, включающий описание всех необходимых операторов («, » и т.д.) и функций. Распределение памяти для хранения массивов должно осуществляться динамически в конструкторе. 1. Найдите наибольшее, наименьшее и среднее арифметическое значе- ний элементов массива. 2. Упорядочьте массив по возрастанию и убыванию методом выбора. 3. Найдите сумму и разность двух матриц NxM. 4. Найдите наибольшее и наименьшее значение элементов стека. 5. Упорядочьте массив по возрастанию и убыванию методом вставки. 6. Найдите сумму, разность и скалярное произведение двух векторов в А-мерном пространстве. 7. Вычислите степень, произведение квадратных матриц NxN. 8. Упорядочьте массив по возрастанию и убыванию методом обмена. 9. Проверьте, является ли последовательность элементов, хранящаяся в деке, симметричной. 10. Найдите периметр и площадь выпуклого многоугольника. 60
§19. Полиморфизм и виртуальные функции Одним из важнейших принципов объектно-ориентированного про- граммирования является полиморфизм, согласно которому в объектах должна быть реализована возможность использования единого интерфейса для выполнения различных действий, причём выбор нужного действия должен зависеть от конкретной ситуации. Подобный выбор может осуще- ствляться как во время компиляции программы (раннее связывание), так и во время её выполнения (позднее связывание). Рассмотрим работу механизма раннего связывания на примере сле- дующих двух классов: class Classi { public: void funcX (); void callX() { funcX (); } } cl; class Class2: public Classi { public: void funcX (); } c2 ; void Classi::funcX () { cout « 'вызов Classi::funcX'; } void Class2::funcX () { cout « 'вызов Class2::funcX'; } В простых случаях раннее связывание обеспечивает требуемое пове- дение программы: cl.funcX(); // вызов Classi::funcX с2.funcX(); // вызов Class2::funcX Несовершенство раннего связывания проявляется при косвенном вы- зове перегружаемой функции: cl.callX(); // вызов Classi::funcX с2.callX(); // опять вызов Classi::funcX Для разрешения подобных конфликтов необходимо использовать позднее связывание, которое реализуется в языке C++ с помощью вирту- альных функций. Вызов виртуальной функции во время исполнения программы осуще- ствляется на основе специальной таблицы виртуальных функций, которая заполняется отдельно для каждого класса и содержит ссылки на все вирту- альные функции класса. 61
Для обозначения виртуальной функции служит ключевое слово virtual. При переопределении виртуальной функции в производном классе повторное использование ключевого слова virtual необязатель- но, однако количество и типы формальных параметров и возвращаемого значения должны полностью соответствовать исходному определению. class Classl { public: virtual void funcX (); void callX() { funcX (); } } cl; class Class2: public Classl { public: void funcX (); } c2 ; cl.callX(); // вызов Classl::funcX c2.callX (); // а сейчас - вызов Class2::funcX Позднее связывание и виртуальные функции чаще всего используются при работе с указателями на объекты базового класса. Напомним, что в языке C++ не разрешается смешивать указатели на переменные разных ти- пов, однако в данном случае мы имеем дело с исключением: переменной- указателю на объект базового класса разрешается присваивать адрес объ- екта производного класса. Однако с помощью такого указателя можно об- ращаться лишь к тем элементам производного класса, которые были унас- ледованы от базового класса. При вызове виртуальных функций с помощью переменной-указателя на объект они вызываются в соответствии с типом объекта, на который ссылается указатель. Для остальных функций работает механизм раннего связывания, и они вызываются в соответствии с базовым типом указателя: class Classl { public: virtual void funcX (); void funcY(); } cl; class Class2: public Classl { public: void funcX(); void funcY(); void funcZ(); } c2 ; Classl * pl = &c2; pl->funcX(); // вызов Class2::funcX () pl->funcY(); // вызов Classl::funcY() pl->funcZ(); // ошибка 62
Дальнейшим развитием концепции виртуальных функций являются чисто виртуальные функции. Их использование оправдано тогда, когда в производных классах по смыслу задачи обязательно должна выполняться перегрузка виртуальных функций, например, если базовый класс не может содержать какую-либо конкретную реализацию такой функции. Для описания чисто виртуальных функций используется следующий синтаксис: virtual возвращаемый тип функция (параметры) = 0; Класс, содержащий хотя бы одну чисто виртуальную функцию, назы- вается абстрактным. Использование в программе объектов абстрактных классов не допускается. Использование объектов классов, порождённых от абстрактных, возможно при условии, что в них переопределены все чисто виртуальные функции. Пример. Вычисление массы тела class Telo { public: double го; virtual double v() = 0; double m() { return ro*v(); } }; class Kub: public Telo { public: double a; double v() { return a*a*a; } }; 63
Лабораторная работа №14 Дан текстовый файл, содержащий описание последовательности объ- ектов двух классов в следующем виде: N // количество объектов <тип> <константы> // описание 1-го объекта <тип> <константы> // описание 2-го объекта <тип> <константы> // описание N-ro объекта Составьте программу, которая будет формировать еще один тексто- вый файл с такой же структурой, в котором исходные объекты будут пере- числены в порядке возрастания значений некоторой функции класса. При решении задачи необходимо разработать заголовочный файл, со- держащий описание иерархии из трёх классов: базового и двух производ- ных. В базовом (абстрактном) классе требуемая функция должна быть объявлена как чисто виртуальная. Для организации файлового ввода-вывода в каждом производном классе реализуйте операторы « и ». Для хранения объектов в памяти используйте массив из указателей на объекты базового класса. Варианты заданий соответствуют л. р. № 11. 64