Вы находитесь на странице: 1из 449

В.Г.

Давыдов

Программирование
и основы
алгоритмизации
Рекомендовано УМО по образованию в области радиотехники,
электроники и биомедицинской техники и автоматизации
в качестве учебного пособия для студентов высших
учебных заведений, обучающихся по специальности
«Управление и информатика в технических системах»

Москва «Высшая школа» 2003 '


УДК 004.4
ББК 32.965
Д 13
Рецензенты:
кафедра «Систем управления и информатики» Санкт-Петербургского государст­
венного института точной механики и оптики (технического университета) (зав. ка­
федрой, д-р техн. наук, проф. В.В. Григорьев);
канд. техн. наук, доц. Санкт-Петербургского электротехнического университета
«ЛЭТИ» СВ. Власенко

Давыдов, В.Г.
Д 13 Программирование и основы алгоритмизации: Учеб. посо-
бие/В.Г. Давыдов. — М.: Высш. шк., 2003. — 447 е.: ил.
ISBN 5-06-004432-7
Учебное пособие написано в соответствии с разработанной с участием автора
примерной программой курса «Программирование и основы алгоритмизации», утвер­
жденной Министерством образования Российской Федерации для подготовки бакалав­
ров и специалистов по направлениям 5502 и 6519 «Автоматизация и управление».
Его цель состоит в поэтапном формировании у студентов следующих слоев зна­
ний и умений — знание основных понятий программирования (слой 1), знание базо­
вого языка программирования C++ (слой 2) и умение решать задачи на ЭВМ (слой 3).
Для удобства преподавателей и студентов приведено по 20 вариантов контроль­
ных заданий по основным разделам курса, заданий на выполнение программных
проектов и приведены тестовые экзаменационные вопросы. Прилагаемый к учебно­
му пособию компакт-диск содержит описание ПМ-ассемблера, его интегрированную
среду программирования, полные тексты демонстрационных программ автора и др.
Для студентов высших учебных заведений, обучающихся по специальности
210100 — «Управление и информатика в технических системах».
УДК 004.4
ББК 32.965
Учебное издание
Давыдов Владимир Григорьевич
ПРОГРАММИРОВАНИЕ И ОСНОВЫ АЛГОРИТМИЗАЦИИ
Редактор ТВ. Рысева, Художник ТС. Лошаков
Лицензия и д № 06236 от 09.П.01
Изд. № РЕНТ-183. Подп в печать 14.08.03. Формат 60x88Vi6. Бум. газетная. Гарнитура «Тайме».
Печать офсетная. Объем 27,44 усл. печ. л., 27,94 усл. кр.-отг.. Тираж 3000 экз. Заказ № 3184.
ФГУП «Издательство «Высшая школа», 127994, Москва, ГСП-4, Неглинная ул., 29/14.
Тел.: (095) 200-04-56. E-mail: mfo@v-shkola.ru http://www.v-shkola.ru
Отдел реализации: (095) 200-07-69, 200-59-39, факс: (095) 200-03-01. E-mail: sales@v-shkola.ru
Отдел «книга-почтой»: (095) 200-33-36. E-mail: bookpost@v-shkola.ru
Отпечатано в ФГУП ордена «Знак Почета» Смоленской областной типографии им. В.И. Смирнова.
214000, г. Смоленск, пр-т им. Ю. Гагарина, 2.

ISBN 5-06-004432-7 © ФГУП «Издательство «Высшая школа», 2003


Оригинал-макет данного издания является собственностью издательства «Высшая
школа», и его репродуцирование (воспроизведение) любым способом без согласия издатель­
ства запрещается.
ПРЕДИСЛОВИЕ

Учебное пособие обеспечивает курс "Программирование и ос­


новы алгоритмизации" и соответствует разработанной с участием
автора примерной программе этого курса, рекомендованной Мини­
стерством образования для подготовки бакалавров и специалистов
по направлениям 5502 и 6519 "Автоматизация и управление". Посо­
бие ориентировано на студентов, начинающих изучение курса про­
граммирования "с нуля", но может быть полезным и преподавателям
высших учебных заведений.
Учебное пособие состоит из введения, двух частей и приложе­
ний. Во введении приводятся сведения о системах счисления, дается
классификация языков программирования и их краткая характери­
стика. В первой части пособия в качестве базового языка програм­
мирования изучается язык C++ (за исключением его средств объект­
но-ориентированного программирования и стандартных библиотек)
и рассматривается технология программирования. Вторая часть
посвящена решению классических задач прикладного программиро­
вания, таких как сортировка массивов, транспортная задача (задача
коммивояжера) и поиск в таблице. Кроме утилитарного значения,
рассмотрение решения этих задач предметно знакомит с методоло­
гией нисходящего иерархического проектирования программ, мо­
дульного программирования, рекурсией, элементами теории графов
и т.п. В прилоэюениях приведены следующие полезные сведения:
• варианты тестов и программных проектов;
• сведения о создании программного проекта в различных интег­
рированных средах программирования;
• рекомендации по структуре программы и пример оформления ее
исходного текста;
• методика отладки программы;
• примерная программа дисциплины "Программирование и основы
алгоритмизации", рекомендованная Министерством образования
и др.
Для удобства преподавателей и студентов пособие содержит
по 20 вариантов контрольных заданий по основным разделам курса,
заданий на выполнение программных проектов и пример тестовых
экзаменационных вопросов. В пособие включены, снабженные отве­
тами, упражнения для самопроверки, что позволяет использовать
его и для самостоятельного изучения материала. По желанию, вме­
сте с учебным пособием можно приобрести компакт-диск, содержа­
щий описание ПМ-ассемблера, его интегрированную среду про­
граммирования, полные тексты демонстрационных программ автора
и др.
в соответствии с возможностями учебного плана предусмат­
риваются следующие три траектории изучения материала:
1. Траектория, рассчитанная на 130 академических часов заня­
тий в рамках подготовки бакалавров (направление 5502 "Автомати­
зация и управление") и специалистов (направление 6519). Это мак­
симальная траектория, охватывающая весь изложенный в учебном
пособии материал.
2. Минимальная траектория, рассчитанная на 65 академиче­
ских часов занятий. В рамках такого варианта:
• не изучается материал, изложенный в подразд. 1.2, 6.6, 6.8, в
разд. 7, 8 (кроме подразд. 8.1), 10, 11, 12 (кроме подразд. 12.1-
12.6), 15 (кроме подразд. 15.8 и 15.9), 16 и 17 (кроме подразд.
17.4);
• не выполняются программные проекты, описанные в приложени­
ях П.1.2.1 и П.1.2.3.
3. Промежуточная траектория, рассчитанная на 100 академи­
ческих часов занятий. В рамках такой траектории, по усмотрению
преподавателя, не изучается только часть материала, пропущенного
в предыдущей траектории.
Ваши отзывы об учебном пособии, конструктивные замечания
и критику направляйте по адресу
davydov@aivt.ftk.spbstu.ru.

Автор
1. ВВЕДЕНИЕ

Электронная вычислительная машина (ЭВМ) представляет со­


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

1.1. Системы счисления

в ЭВМ программы представлены с использованием двоичной


системы счисления. Причина этого кроется в следующем.
Основным элементом ЭВМ является электронный ключ, имею­
щий два состояния - "включено" или "выключено". Это хорошо
соответствует двоичной системе счисления, в которой используются
две цифры: "О" и " 1 " , обозначающие один двоичный разряд - бит.
Рассмотрим системы счисления более подробно и, в частности,
системы счисления, применяемые в ЭВМ.
Система счисления - совокупность приемов и правил для запи­
си чисел цифровыми знаками. Различают непозиционные и позици­
онные системы счисления.
В непозиционной системе счисления значение знака (символа)
не зависит от его положения в числе. Пример - римская система
счисления.
Позиционная система счисления - система, в которой значение
цифры числа определяется ее положением (позицией) в числе. Лю­
бая позиционная система счисления характеризуется основанием.
Основание "^" позиционной системы счисления - количество цифр,
используемых при изображении числа в данной системе.
Для позиционной системы счисления справедливо равенство,
\idi3b\bdiQMOQ развернутой формой записи числа:
где A^^j^ - произвольное число, записанное в системе счисления с ос­
нованием 'V"; ^/ - коэффициенты ряда или значения разрядов числа
(цифры системы счисления); п + 1 т - количество целых и дробных
разрядов числа.
На практике, для краткости, используют сокращенную запись
чисел:
Ая) = ^п<^п-х • ••а,а^,а_,а_2...а_,„ (2)

Пример. Для десятичного числа 349,17 развернутая форма за­


писи
3-10'+4-10'+9-10VM0-'+7 10-',
а сокращенная
349.17
В вычислительной технике, в основном, используются двоич­
ная, восьмеричная и илестнадцатеричная системы счисления (вось­
меричная и шестнадцатеричная - для более компактной записи дво­
ичных кодов). В нашем обиходе используется десятичная система
счисления. По этой причине необходимо уметь переводить числа из
систем счисления с основаниями, равными целым степеням 2, в де­
сятичную систему счисления и наоборот.
В двоичной системе счисления для записи числа в сокращен­
ной форме используются цифры О и 1, в восьмеричной - О, 1,2, ..., 7,
в десятичной - О, 1,2, ..., 9 и в шестнадцатеричной - О, 1,2, ..., 9, А,
В, С, D, Е, F.
Перевод чисел из двоичной, восьмеричной или илестнадцате-
ричной систем счисления в десятичную систему легко выполняется
с помощью развернутой формы записи числа (1). Например, двоич­
ное число
42) =1101,001(2)
соответствует десятичному числу
^ = Ь2'+1-2'ч-1-2'+1-2-' =13,125
Аналогично выполняется перевод из восьмеричной и шестна-
дца.теричной систем в десятичную систему счисления. Рекомендуем
самостоятельно составить соответствующие примеры перевода в де­
сятичную систему. Обратите внимание, что при выполнении пере­
вода в десятичную систему вычисления ведутся в десятичной сис­
теме.
Перевод чисел из десятичной системы в двоичную систему
счисления поясним примером.
Пусть требуется перевести десятичное число 13,125 в двоич­
ную систему счисления. Целая и дробная части десятичного числа
переводятся по разному: целая - делением на основание q — 2^ г.
дробная - умножением на q = 2. Vi деление, и умножение выполня­
ются в десятичной системе счисления.
При делении 13 на 2 получаем частное 6 и остаток 1. При де­
лении 6 на 2 получаем частное 3 и остаток 0. При делении 3 на 2
получаем остаток 1 и частное 1. Поскольку частное меньше двух,
то на этом деление заканчивается.
При умножении 0,125 на два получаем 0,250 {целая часть
произведения 0). При умножении 0,250 на 2 получаем 0,500 {целая
часть произведения 0). При умножении 0,500 на 2 получаем 1,000
{целая часть произведения 7). Поскольку дробная часть теперь ну­
левая, то умножение на этом заканчивается.
Остатки от деления, взятые в обратном порядке, и последнее
частное дают целую часть числа в двоичной системе счисления, а
целые части от умножения, взятые в прямом порядке, дают дробную
часть числа. В результате получаем число в двоичной системе
счисления
1101,001(2)
Обратите внимание, что перевод дробной части числа завер­
шается при получении после точки всех нулей или при получении
требуемого числа разрядов дробной части (требуемой точности).
Особенно просто выполняется перевод чисел из двоичной сис­
темы счисления в системы счисления с основанием, равным степе­
ням 2, и наоборот. При указанном переводе удобно пользоваться
табл. 1.

Табл. 1. Перевод чисел из двоичной системы в восьмеричную или


шестнадцатеричную системы счисления и наоборот
Восьмеричная Двоичная Шестнадцатеричная Двоичная
цифра триада цифра тетрада
0 000 0 0000
1 001 1 0001
2 010 2 0010
3 011 3 ООН
4 100 4 0100
5 101 5 0101
6 по 6 оно
7 111 7 0111
8 1000
9 1001
А 1010
В 1011
С 1100
D 1101
Е 1110
F 1111
Примеры перевода чисел из одной системы счисления в дру­
гую:
1. 4 2 ) = 1 101,1 1001(,) ^8)=?
Для перевода двоичного числа в восьмеричную систему снача­
л а д в и г а е м с я от т о ч к и в л е в о , в ы д е л я я д в о и ч н ы е т р и а д ы . Е с л и о с ­
т а в ш а я с я п о с л е д н я я г р у п п а б и т о в не о б р а з у е т т р и а д ы , т о д о п о л н я е м
ее д о т р и а д ы н у л я м и слева. З а т е м д в и г а е м с я от т о ч к и в п р а в о , т а к ж е
выделяя триады. Последнюю, неполную группу битов, дополняем до
т р и а д ы д о б а в л е н и е м н у л е й с п р а в а . В р е з у л ь т а т е д в о и ч н о е ч и с л о за­
писывается в виде
001 101 , 1 1 0 0 1 0
В соответствии с табл. 1 заменяем полученные триады восьме­
ричными цифрами и получаем

2. 42)=И01Д 1001(2) 4.6)=?


Перевод числа выполняется аналогично предыдущему, но вме­
сто т р и а д в ы д е л я ю т с я д в о и ч н ы е т е т р а д ы :
1101 , 1100 1000
В соответствии с табл. 1 заменяем полученные тетрады шест-
надцатеричными цифрами и получаем
^16) = -^'^^(16)

3 . 4,6)-^^1,^^06) 42)=?
в соответствии с табл. 1 заменяем шестнадцатеричные цифры
тетрадами
1010 1111 0001 , 1 1 1 1 1110
и получаем
^2) =101011110001,1111111(2)
4. 48)=710,526(,,) 42)=?
Этот пример предлагаем выполнить самостоятельно.
В о з м о ж е н б ы с т р ы й и п р о с т о й п е р е в о д ч и с е л из в о с ь м е р и ч н о й
системы в шестнадцатеричную систему счисления и обратно. Такой
п е р е в о д м о ж н о в ы п о л н и т ь в д в а э т а п а - с н а ч а л а из и с х о д н о й с и с т е ­
м ы с ч и с л е н и я в д в о и ч н у ю с и с т е м у и з а т е м из д в о и ч н о й в н о в у ю с и с ­
тему счисления. Примеры такого рода также предлагаем выполнить
самостоятельно.

1.2. Классификация языков программирования


и их краткая характеристика

Я з ы к и п р о г р а м м и р о в а н и я д е л я т с я на д в е г р у п п ы :
1. Машинно-зависимые языки, которые можно применять на
одной ЭВМ или на ограниченном подмножестве машин с одинако­
вой архитектурой.
2. Машинно-независимые языки - их можно использовать на
любой ЭВМ. Языки этой группы называют универсальными языка­
ми.
Машинно-зависимые языки^ в зависимости от их близости к
машинным языкам, делятся на три группы:
• машинные языки (языки нулевого уровня);
• ассемблерные языки (языки первого уровня или языки типа 1:1,
последнее означает, что одна ассемблерная команда после транс­
ляции порождает ровно одну машинную команду);
• макроассемблеры (языки второго уровня или языки типа \\п).
Аналогично, маилинно-независимые языки включают сле­
дующие группы языков:
• Процедурные языки (третий уровень): Си, C+-I-, Паскаль,
ФОРТРАН, БЭЙСИК и др. Процедурные языки требуют детальной
разработки алгоритма решения и, по существу, являются языками
для записи алгоритмов решения задач.
• Проблемные языки (четвертый уровень) или языки типа "заполни
бланк". Это языки описания задач, специализированные языки.
Используя подобный язык программирования, пользователь со­
общает только, какую задачу надо решить и с какими данными.
Как решить задачу - "знает" язык. В качестве примера проблемно­
го языка можно назвать язык ПРОСПО, разработанный фирмой
IBM для программирования систем управления производствен­
ными процессами.
• Универсальные языки (пятый уровень): ПЛ/1, АЛГОЛ-68, Ада и
др. При создании универсальных языков в их состав включили все
лучшее, что имелось на момент создания в процедурных языках.

1.2.1. Машинные языки

Рассмотрим машинные языки на примере простого машинного


языка (ПМ), разработанного для студентов кафедры "Автоматика и
вычислительная техника" Санкт-Петербургского государственного
политехнического университета проф. Лекаревым М.Ф. Язык ПМ
подробно рассмотрен в [1] и его описание имеется на прилагаемом
компакт-диске. Здесь, если вместе с учебным пособием Вы приоб­
рели еще и компакт-диск, рекомендуем рассмотреть имеющийся в
[1] материал, включая структурную организацию и функционирова­
ние простой ЭВМ.
Пример. З а п р о г р а м м и р у е м на м а ш и н н о м я з ы к е П М р е ш е н и е
следующей задачи: ввести исходные данные
А,В,С,

н а п е ч а т а т ь для к о н т р о л я в в е д е н н ы е д а н н ы е , в ы ч и с л и т ь и н а п е ч а т а т ь
Е=АВ + С/\,5
Система команд м а ш и н ы П М с о д е р ж и т н е с к о л ь к о д е с я т к о в
р а з л и ч н ы х одноадресных команду из к о т о р ы х в т а б л . 2 п р и в о д я т с я
л и ш ь те, к о т о р ы е б у д у т н е о б х о д и м ы д л я р е ш е н и я у к а з а н н о й з а д а ч и .

Т а б л . 2. Т а б л и ц а к о д о в о п е р а ц и й ( К О П )
КОП Выполняемое действие (обозначение команды)
01 Передача слова из ОЗУ в регистр А арифметико-логического устройства
АЛУ (ЧТЕНИЕ)
02 Передача слова из регистра А АЛУ в ОЗУ (ЗАПИСЬ)
03 Слолсение содержимого регистра А АЛУ с операндом, заданным в коман-
де. Результат помещается в регистр А АЛУ (+)
04 Вычитание ( - ). Операция выполняется аналогично сложению
05 Умножение ( * ).Операция выполняется аналогично сложению
06 Деление ( / ).Операция выполняется аналогично сложению
07 Ввод слова с устройства ввода в ОЗУ (ВВОД)
08 Вывод слова из ОЗУ в устройство вывода (ВЫВОД)

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

Табл. 3. Таблица символов (ТС)


Имя переменной Адрес в ОЗУ (для удобства используется десятичный адрес
вместо двоичного адреса)
А 00000
В 00001
С 00002
"1,5" 00003
Е 00004

2. Р а с п р е д е л и т ь п а м я т ь д л я к о м а н д п р о г р а м м ы . Н а п р и м е р , у с ­
ловимся размещать программу, начиная с адреса 01000 (для удобст­
ва и с п о л ь з у е т с я д е с я т и ч н ы й а д р е с в м е с т о д в о и ч н о г о а д р е с а ) .
3. С о с т а в и т ь с о б с т в е н н о п р о г р а м м у , т.е. з а п и с а т ь к о д ы ма­
ш и н н ы х к о м а н д с у к а з а н и е м м е с т а их р а з м е щ е н и я в О З У . Н а п о м и ­
наем, что р е а л ь н о в Э В М и с п о л ь з у ю т с я д в о и ч н ы е к о д ы к о м а н д и

10
адресов, но мы для собственного удобства воспользуемся и здесь
десятичными кодами. Машинная программа приведена в табл. 4.
Проанализировав содержимое табл. 3, оценим возможности и
особенности программирования на машинных языках.
В оперативном запоминающем устройстве (ОЗУ) между ме­
стом размещения данных и команд программы осталась "дырка" -
ячейки с адресами 00005...00999, которую для полного использова­
ния памяти нужно ликвидировать. Однако заранее оценить размер
памяти, необходимой для размещения данных и команд, нельзя или,
по крайней мере, очень трудно.

Табл. 4. Машинная программа


1 Адрес КОП Адрес Действие команды
команды операнда
01000 07 00000 УВв -> ОЗУ по адресу 000000 (А)
01001 08 00000 Содержимое ячейки ОЗУ с адресом 00000 (А) - ^ УВыв
01002 07 00001 УВв -> ОЗУ по адресу 000001 (В)
01003 08 00001 Содержимое ячейки ОЗУ с адресом 00001 (В) —> УВыв
01004 07 00002 УВв -> ОЗУ по адресу 000002 (С)
01005 08 00002 Содержимое ячейки ОЗУ с адресом 00002 (С) —> УВыв
01006 01 00002 Содержимое ячейки ОЗУ с адресом 00002 (С) - ^
регистр А АЛУ
01007 06 00003 Содержимое регистра А АЛУ (С) разделить на
содержимое ячейки ОЗУ с адресом 00003 (1.5) и
результат поместить в регистр А АЛУ
01008 02 00004 Содержимое регистра А АЛУ (С/1.5) - ^ ОЗУ по адресу
00004 (Е)
01009 01 00000 Содержимое ячейки ОЗУ с адресом 00000 (А) —>
регистр А АЛУ
01010 05 00001 Содержимое регистра А АЛУ (А) умножить на
содержимое ячейки ОЗУ с адресом 00001 (В) и
результат поместить в регистр А АЛУ
01011 03 00004 Содержимое регистра А АЛУ (А*В) сложить с
содержимым ячейки ОЗУ с адресом 00004 (С/1.5) и
результат поместить в регистр А АЛУ
01012 02 00004 Содержимое регистра А АЛУ (А*В+С/1.5) -^ ОЗУ по
адресу 00004 (Е)
01013 08 00004 Содержимое ячейки ОЗУ с адресом 00004 (А*В+С/1.5)
-^ УВыв 1

При внесении изменений в программу в процессе ее отладки


эти размеры меняются. Поэтому действия, указанные выше в пп. 1 -
3, приходится повторять.
Другие недостатки программирования с использованием ма­
шинного языка заключаются в следующем.
1. Действия, указанные в пп. 1 - 2 нетворческие, поэтому при
их выполнении программист делает много ошибок. Вместе с тем их
можно автоматизировать с помош;ью ЭВМ.

11
2. Реальные системы команд ЭВМ громоздки (сотни различ­
ных команд), а использование двоичных кодов команд программы
трудоемко и ненаглядно. Машинную программу трудно восприни­
мать (см. табл. 4 без левого и правого столбцов).
Из-за отмеченных недостатков машинные программы давно
перестали писать. Вместе с тем отметим достоинство программ, на­
писанных с использованием машинных языков, - они имеют наи­
большее быстродействие и требуют минимальных затрат памяти. Но
это может быть достигнуто только ценой больших затрат времени
квалифицированного программиста.
Многие из названных выше недостатков машинных программ
устраняются при программировании на ассемблерных языках.

1.2.2. Ассемблерные языки (на примере ПМ-ассемблера)

Ассемблерные языки отличаются от соответствующих машин­


ных языков следующими особенностями:
1. Вместо двоичных, восьмеричных, шестнадцатеричных и де­
сятичных записей кодов операций и адресов используется их на­
глядная символическая запись. Перевод символических записей в
двоичные коды, воспринимаемые машиной, выполняется автомати­
чески, с помощью ЭВМ.
2. Размещение в ОЗУ данных и команд также автоматически
выполняет ЭВМ, причем без "дырок". Действия, указанные в пп. 1 -
2 выполняет специальная системная программа-переводчик, назы­
ваемая транслятором (компилятором).
3. Оптимальность программы по быстродействию и занятой
памяти практически сохраняется (проигрыш не более 5... 10 % ) . Это
делает ассемблерные языки практически основными для системных
программистов.
Пример программы на языке ПМ-ассемблер, подобный приме­
ру из подразд. i.2.1, приведен в [1] в разд. 7 (см. прилагаемый ком­
пакт-диск).
Перевод (перекодировка) ассемблерной программы на язык
машинных команд производится транслятором по следующей схеме
(рис. 1). Трансляция выполняется в два прохода.
Проход 1. Составляется таблица символов (ТС).
Проход 2. Выполняется перекодировка команд в двоичные ко­
ды (используются ТК и ТС).

1.2.3. Макроассемблерные языки

Макроассемблерные языки сейчас являются самыми мощными.


Объясняется это тем, что макроассемблер обладает всеми возмож-

12
ностями ассемблера и, дополнительно, мощным аппаратом макро­
вызовов и макроопределений. Поясним возможности указанного ап­
парата примером.
Пример. Предположим, что часто встречается вычисление
R^{X^+Y^)/{X'Y)

Текст программы Таблица кодов


на ассемблере операций (ТК)

Таблица
символов (ТС)

Текст программы
на машинном
языке
Рис. 1. Схема трансляции в два прохода

Соответствующая группа машинных команд оформляется в виде


макроопределения, имеющего следующую структуру:

[Г 2 10 11 20 21 40
к МЕТКА ОПЕРАЦИЯ ОПЕРАНД КОММЕНТАРИЙ

к Стандартное начало , MACRO - ключевое слоъо, SUB(X, Y, R) -


К придумывает программист
MACRO SUB(X, Y, R)
ЧТЕНИЕ X Тело
* X
ЗАПИСЬ Х2
ЧТЕНИЕ Y
* Y
+ Х2
/ X
/ Y
ЗАПИСЬ R макроопределения
MEND Стандартный конец

Пусть, например, необходимо вычислить


С = (А^+В')/(А-В) и G-={E^+F^)/(E-F)
Это можно сделать в программе с помощью однострочных
макровызовов:

13
2 10 11 20 21 40 41
МЕТКА ОПЕРАЦИЯ ОПЕРАНД КОММЕНТАРИЙ

SUB(A, В, С)
SUB(E, F, G)

Здесь X, Y, R - формальные, а А, В, С и Е, F, G - фактические


параметры. Вызов "SUB(A, В, С) " заменяется фактически модифици­
рованным телом макроопределения, в котором формальные пара­
метры X, Y, R заменены соответственно фактическими параметрами
Л, В, С.
Макроассемблер - основной язык системных программистов. В
частности, программист может, при необходимости, разработать
свой набор макроопределений - это эквивалентно разработке в рам­
ках макроассемблера "своего", специализированного языка с опера­
торами - макровызовами. При этом сохраняется высокая эффектив­
ность программ, написанных на макроассемблере. Проигрыш по бы­
стродействию и занятой памяти не превышает 10... 15 %.

1.2.4. Машинно-независимые языки.


Процедурные и универсальные языки

Чтобы сопоставить машинно-независимые языки с процедур­


ными и универсальными языками, запишем тот же пример на проце­
дурных и универсальных языках высокого уровня:
/'*' пример на языке Си, Вычисляем D := А * В + С / 2 . 0 * /
^include <stdio.h>
±nt main ( void )
{
float Л, B, C, D;
scanf( " %f %f %f", &A, &B, &C ) ;
printf( " %f %f %f". A, B, С ) ;
D=A*B+C/2.0;
printf( "\n %f", D ) ;
return 0;

PROGRAM PEMPAS ( INPUT, OUTPUT ) ;


{ Пример на языке ПАСКАЛЬ. Вычисляем D : = A * B + C / 2 . 0 } |
VAR
А , Б , С , D: REAL;
BEGIN { Для PRMPAS
READ( А, В, С ) ;
WRITE( А, В, С ) ;
D := А ^ В + С / 2.0;
WRITE( D )
END. { Для PRMPAS

14
PROGRAM PRMF7 7
С Пример для языка ФОРТРАН? 7. Вычисляем D : = A ' ^ B - h C / 2 . 0
REJLL А, В, С, D
READ *г ^f Br С
PRINT *r А, В, С
D = А ^ В + С / 2.0
PRINT *, D
STOP
END

100 REM Пример на БЭИСИКе. Вычисляем D : = A * B + C / 2 . 0


110 INPUT А, В, С
120 PRINT А, В, С
130 PRINT А * В + С / 2 . 0
140 END

\ / ^ Пример на языке PL/1. Вычисляем D := А * В + С / 2.0 */


PRMPL1: PROCEDXmE OPTIONS ( MAIN ) ;
DECLARE ( A, Br Cr D ) REAL FLOAT DEC ( 6 ) ;
GET LIST( Ar Br С ) ;
PUT LIST( Ar Br С ) ;
D = A ^ В + С / 2.0
PUT LIST ( D ) ;
END PRMPLl;

Из приведенных примеров видны удобства работы на машин­


но-независимых языках. По этой причине ясно, что языки програм­
мирования высокого уровня - основные языки проблемных програм­
мистов. Эффективность программ, написанных на языках высокого
уровня, понижена в 2 - 4 раза. Для программ на языках Си/С++ по­
нижение эффективности составляет лишь 1,5 раза и объясняется это
тем, что этот язык включает в себя многие средства ассемблерных
языков.
ЧАСТЬ 1. БАЗОВЫЙ ЯЗЫК
ПРОГРАММИРОВАНИЯ

2. ЯЗЫК ПРОГРАММИРОВАНИЯ
ВЫСОКОГО УРОВНЯ C++

Язык программирования Си был разработан в 1972 году Д.


Ритчи в фирме Bell Laboratories (США) как универсальный язык
системного программирования в связи с созданием популярной опе­
рационной системы UNIX. Эта операционная система (ОС) была, в
основном, написана на языке Си, что обеспечило ее переносимость
на любые ЭВМ. Действительно, для каждой архитектуры ЭВМ дос­
таточно написать только транслятор с языка Си и с его использова­
нием ОС UNIX легко переносится на новую ЭВМ, принадлежащую
соответствующей архитектурной линии.
При разработке языка Си был принят компромисс мелсду низ­
ким уровнем языка ассемблера и высоким уровнем таких языков, как
Паскаль, ФОРТРАН, БЭЙСИК, ПЛ/1 и др. Многие и многие опера­
ции языка Си (манипулирования строками, ввода-вывода и др.) вы­
несены за пределы языка и реализованы как подпрограммы, которые
могут быть вызваны из Си-программ. Такое решение обеспечило
высокую эффективность языка Си (высокое быстродействие и ма­
лые затраты памяти).
Язык Си - современный язык, включающий управляющие кон­
струкции, рекомендуемые теоретическим и практическим програм­
мированием. Такими конструкциями являются следование, ветвле­
ние, циклы; модули, называемые функциями. Язык Си побуждает
программиста использовать в своей работе нисходящее проектиро­
вание, структурное программирование и пошаговую разработку мо­
дулей. В дополнение к сказанному, язык 0++, являющийся даль­
нейшим развитием языка Си, содержит такой важный инструмент,
как средства объектно-ориентированного программирования (ООП).
Таким образом, если есть желание работать в среде програм­
мотехники, то один из первых вопросов, на который нужно ответить
"Да" - это вопрос "Умеете ли Вы программировать на языках
Си/С++?".

16
2.1. Введение. Структурное и модульное
программирование

Суть структурного программирования иллюстрирует рис. 2.


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

Что такое АЛГОРИТМ? Как его записать?

Аль Хорезми

Структурное Дейкстра
программирование
Ветвления Следование Циклы
if - then - else while
switch do - while
for
Рис. 2. Алгоритм и технология структурного программирования

Если посмотреть на верхнюю часть графического конспекта-


рисунка, то внимание сразу привлекает слово АЛГОРИТМ.

2.1.1. Алгоритм и способы его записи

АЛГОРИТМ - одно из основных понятий современной матема­


тики и программирования. Само слово происходит от имени узбек­
ского математика IX в. Мухаммеда, уроженца Хорезма (по-арабски
"Аль Хорезми"). Его работы по арифметике и алгебре были переве­
дены на латинский язык в XII в. и оказали большое влияние на раз­
витие математики в Европе. Сформулированные ученым правила
выполнения четырех арифметических действий получили название
"алгоризм", дальнейшая трансформация — "алгоритмус", "алгорифм"
и "алгоритм". Интуитивное понятие алгоритма можно выразить сле­
дующим образом.
Алгоритм - строгая и четкая система правил, которая опреде­
ляет последовательность действий над некоторыми объектами и по-

17
еле конечного числа шагов приводит к достижению поставленной
цели.
Примерами алгоритмов могут являться:
• рецепты приготовления блюд;
• пояснения, "как пройти", "как проехать";
• правило умножения целых чисел "столбиком" и т.п.
Вместе с тем, "школьная" таблица умножения не является ал­
горитмом.
Способы записи алгоритмов. Один и тот же алгоритм может
быть записан (описан) различными способами. Наибольшее распро­
странение в практике программирования получили [2]:
• текстуальная (словесная) запись алгоритма;
• запись алгоритма с помощью схемы (разновидность визуального
способа - формализма);
• запись алгоритма с использованием диаграммы Нэсси-
Шнейдермана (разновидность визуального формализма);
• запись алгоритма с использованием Р-схемы (разновидность ви­
зуального формализма);
• запись алгоритма с помощью псевдокода^
• запись алгоритма в терминах языка программирования.
Эти способы записи алгоритмов не исключают друг друга, а
используются последовательно, на различных этапах решения зада­
чи. Только три способа записи алгоритма — с использованием схемы,
диаграммы Нэсси-Шнейдермана и Р-схемы - являются альтернатив­
ными на соответствующем этапе решения задачи.
Дадим краткую характеристику перечисленных выше способов
записи алгоритмов. С этой целью один и тот же алгоритм запишем
различными способами.
Текстуальная запись алгоритма. Имеется следующий алго­
ритм, записанный в текстуальной форме:
1. Начало алгоритма.
2. Выполнить некоторое действие (оператор) s i .
3. Если выполнено условие "Усл1", то выполнить операторы
s2, s3 и перети к п. 4. Иначе - перейти к пп. 3.1.
3.1. Пока выполняется условие "Усл2", выполнять пп. 3.2
и 3.3. Иначе - перейти к п. 4.
3.2. Если выполнено условие "УслЗ", то выполнить опера­
тор s4, иначе — выполнить оператор s5.
3.3. Выполнить оператор s6.
4. Пока выполняется условие "Усл4", выполнять оператор s7.
Иначе — перейти к п. 5.
5. Выполнить оператор s8.
6. Конец алгоритма.

18
Запись алгоритма с помощью схемы (ГОСТ 19.701 - 90, со­
вместим с меэюдународным стандартом). Запись того же самого
алгоритма в виде схемы иллюстрирует рис. 3.
В случае простой схемы сравнительно несложно обеспечить ее
бесспорную наглядность. Но по мере роста сложности отображаемо­
го фрагмента алгоритма (программы), его логическая структура на­
чинает "тонуть" в "клубке спагетти", в который постепенно превра­
щается схема алгоритма [2]. Поэтому практика использования схем
алгоритмов уже давно считается устаревшей и программисты при­
меняют ее как инструмент разработки только эпизодически. Там, где
стандарты организации требуют наличия схем алгоритмов, они поч­
ти неизменно рисуются после написания программы.
Запись алгоритма с помощью диаграммы Нэсси-Шнейдермана
(рис. 4). Диаграмма Нэсси-Шнейдермана была предложена в сочета­
нии со структурным программированием как средство борьбы с
проблемой "клубка спагетти", присущей схемам алгоритмов. Эта
диаграмма, бесспорно, заменяет одномерное представление вложен­
ных операторов двумерным (см. рис. 4). Тем не менее, по мере роста
сложности программного кода появляются проблемы отображения,
поскольку элементы диаграммы быстро становятся все меньше и
меньше. На рис. 4 приведена диаграмма Нэсси-Шнейдермана для
того же самого алгоритма, что и ранее.
Запись алгоритма с помощью Р-схемы (рис. 5). Используемая
при этом Р-технология программирования разработана в Институте
Кибернетики АН УССР. Согласно Р-технологии программа должна
быть представлена в форме нагруженного по дугам структурного
графа (Р-схемы). Р-схема состоит из подграфов, каждый из которых
имеет один вход и один выход. Вершины графа называются состоя­
ниями Р-схемы. Переходы из одного состояния в другое представ­
лены помеченными дугами, причем каждая дуга может быть поме­
чена условием перехода и действием, выполняемым в процессе вы­
полнения перехода. На рис. 5 приведена Р-схема для того же самого
алгоритма.
Запись алгоритма с помощью псевдокода. Довольно широкое
распространение получил еще один способ записи алгоритма с по­
мощью псевдокода. Этот способ" записи является промежуточным и
используется перед записью алгоритма в терминах выбранного язы­
ка программирования. Псевдокод представляет собой удобный для
практики промежуточный язык. Это и не естественный язык, и не
язык программирования, а их симбиоз. Псевдокод похож на язык
программирования тем, что может использовать его некоторые ин­
струкции, но, с другой стороны, допускает и словесную, и формуль­
ную записи там, где сразу сложно воспользоваться языком програм­
мирования.

19
а) Точка входа или выхода
( )
Операционный блок
(функциональный узел)

Решающий блок (предикатный узел)

Циклическая конструкция

Поток управления
б)

ГЦикл «Усл2»
Не «Усл2»

Нет
<^УслЗ^
Да
Цикл «Уcл4>^
Не «Усл4»
s4 s5

1
s7
s6

1
Цикл «Усл4»
\ / Цикл «Усл2»

s8

f Конец J
Рис. 3. Схема алгоритма:
а) основные обозначения; б) пример использования
а) Заголовок программы
Программа
Тело программы
Простая конструкция Оператор

Условная конструкция

Циклическая конструкция Условие


Тело цикла

Пример программы
б)
s1
Усл1
т F

Усл2
s2
УслЗ / ^
т \
S3 s4 s5

s6

Усл4
s7
s8
Рис. 4. Диаграмма Нэсси-Шнейдермана:
а) графические элементы; б) пример использования

2.1.2. Структурное и модульное программирование

Применительно к решению задачи на ЭВМ, можно сформули­


ровать, что алгоритм, или программа для вычислительной машины
состоит их двух важных разделов: описания действий, которые не­
обходимо выполнить, и описания данных, с которыми оперируют
упомянутые действия. Действия описываются с помощью операто­
ров, а данные - с помощью определений или объявлений.
В 1965 г. профессор Эйндховенского университета Дейкстра
(Нидерланды) начал пропагандировать стиль программирования,
получивший название "программирование без оператора goto (без­
условного перехода)", первоначально принятый большинством про­
граммистов негативно. Однако, в течение нескольких последующих
лет этот же стиль, получивший название структурного программи-

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

а) Состояние Р-схемы

Совмещение двух состояний в


одно (цикл)

Дуга Р-схемы Условие

Действие

б)
Усл1
склгыэ Усл4
s7

Рис. 5. Р-схема:
а) графические элементы; б) пример использования

Основными управляющими конструкциями структурного про­


граммирования являются:
• СЛЕДОВАНИЕ - если в записи алгоритма (программы) подряд
написаны несколько действий (операторов) друг за другом, то они
будут выполняться последовательно в таком же порядке.
• ЕСЛИ-ТО-ИНАЧЕ - условная конструкция, определяющая раз­
ветвление в порядке выполнения действий (операторов). Дослов­
ный перевод этой конструкции if-then-els е.
• ЦИКЛЫ. В структурном программировании предусмотрены цик­
лические конструкции трех видов:
1. Цикл с предусловием ПОКА-ДЕЛАЙ: пока истинно некото­
рое условие, делай то-то и то-то (дословный английский перевод
этой конструкции while).
2. Цикл с постусловием ПОВТОРЯЙ-ПОКА (do-while). Отли­
чается от предыдущего цикла тем, что тело цикла повторяется не
менее одного раза.

22
3. Цикл с заранее заданным числом повторений (Jor).
Этих трех элементарных конструкций, называемых базовыми
конструкциями структурного программирования, достаточно, чтобы
управлять порядком выполнения действий в любом алгоритме. От­
метим также, что каждая из названных конструкций имеет только
один вход и один выход, что делает их использование очень удоб­
ным.

Ветвление (условная конструкция)


1) Общий случай (рис. 6 <з)

"ЕСЛИ" УСЛОВИЕ "ТО"ВЕТВЬ'ТО "ИНАЧЕ" ВЕТВЬ-ИНАЧЕ "ВСЕ"

а) («ЕСЛИ»)

(«ИНАЧЕ») Нет
-4-
ВЕТВЬ-ИНАЧЕ ВЕТВЬ-ТО

(«ВСЕ»)

б) ... («ЕСЛИ»)

(«ИНАЧЕ») Нет

ВЕТВЬ-ТО

... («ВСЕ»)
Рис. 6. Ветвление (условная конструкция):
а) общий случай; б) частный случай
/ / C++ реализация
±f( А > В )

D = Е; // ВЕТВЬ-ТО

23
// ...
else
{
С = F/ // ВЕТВЬ-ИНАЧЕ
// . ..
}

2) Частный случай (см. рис. 6 б)

"ЕСЛИ" УСЛОВИЕ "ТО" ВЕТВЬ-ТО "ВСЕ"


// C+-h реализация
±f( А > В )
{
D = Е; // ВЕТВЬ-ТО
// . ..
;

Циклы
1) С предусловием (рис. 7, 8). Пример
SUMMA = Y,I

"ПОКА" УСЛОВИЕ "ДЕЛАТЬ" ТЕЛО-ЦИКЛА "ВСЕ"


// C++: ЦИКЛ С ПРЕДУСЛОВИЕМ
SUMMA = 0 / 1 = 1 / // ПОДГОТОВКА ЦИКЛА
while ( I <= 20 )
{
SUMMA += I/ // Эквивалентно SUMMA = SUMMA + I;
I += 1/
}
// C++: ЦИКЛ FOR (CM, РИС, 8)
for( 1=1/ I <= 20/ I++ )
// I++ эквивалентно 1 = 1 + 1 /
{
SUMMA += I/
}

2) С постусловием (рис. 9).


20
Пример: SUMMA = ^I

"ПОВТОРЯЙ" ТЕЛО-ЦИКЛА "ПОКА" УСЛОВИЕ


// C++: ЦИКЛ С ПОСТУСЛОВИЕМ
SUMMA = 0 / 1 = 1 / // ПОДГОТОВКА ЦИКЛА

24
do
{
SUMMA -h= I; // Эквивалентно SUMMA = SUMMA + J ,
I += 1;
}
while( I <= 20 ) ;

SUMMA = 1 + 2 + ... + 20

SUMMA:=0;
I := 1;

(«ПОКА»)

SUMMA := SUMMA + I;
I : = ! + 1; УСЛОВИЕ
(«ДЕЛАТЬ»)
ТЕЛО-ЦИКЛА Нет («ВСЕ»)

Вариант по ГОСТ

SUMMA:=0;
I := 1;

Цикл «I»
I >20

SUMMA := S U M M A + 1 ;
I := 1 + 1;

Цикл «I»

Рис. 7. Цикл с предусловием (while)

Наряду с методологией структурного программирования, хо­


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

25
ные подзадачи, оформленные в виде отдельных модулей, которые в
языках Си/С-ь+ называются функциями.

Вариант по ГОСТ

SUMMA := 0;

Цикл «I» 1, 20, +1 - начальное, конечное


I >20 значения "1" и шаг изменения "1"

SUMMA :=
SUMMA + I

Цикл «I»

Рис. 8. Цикл с предусловием {for)

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


ва ее параметров можно использовать "правило семь ± два".
Смысл этого правила заключается в том, что человек хорошо вос­
принимает до семи некоторых элементов - параметров функции,
операторов языка программирования и т.п. Таким образом, при хо­
рошо выполненной декомпозиции размер функции не превосходит
обычно 2 5 - 8 1 строк текста, а количество параметров не превышает
5 - 9 . Размер функции 2 5 - 8 1 строк текста получается, если в ее бло­
ке содержится не более 5 - 9 элементарных конструкций, каждая из
которых занимает не более 5 - 9 строк. Модули (функции Си) можно
хранить в отдельных файлах, отлаживать параллельно, что способ­
ствует сокращению сроков проектирования программных проектов
и привлечению к работе над проектами коллективов программистов.
Модульное программирование, получившее также название
нисходящего программирования, для сложных программных проек­
тов может носить иерархический характер, т.е. полученные вначале
программные модули, в свою очередь, при необходимости, также
декомпозируются, с тем, чтобы достичь указанных выше показате­
лей, соответствующих хорошему стилю программирования.

26
Вариант по ГОСТ

S U M M A = 1 + 2 + ... + 20
SUMMA:=0;
I := 1;

SUMMA:=0;
I := 1; Цикл «I»

(«ПОВТОРЯЙ»)

SUMMA := SUMMA + I; SUMMA := SUMMA + I


I := I + 1; ТЕЛО-ЦИКЛА
I := I + 1;
(«ПОКА»)
I >20
УСЛОВИЕ Цикл «I»
Нет

Рис. 9. Цикл с постусловием

2.2. Язык программирования и его описание


(на примере языков Си/С++)

Совокупность понятий, относящихся к указанной в заголовке


теме, иллюстрирует рис. 10. На нем сразу же обращают на себя вни­
мание слова "ЯЗЫК ПРОГРАММИРОВАНИЯ", "АЛФАВИТ",
"СИНТАКСИС + СЕМАНТИКА" и "цветок с лепестками". Обсудим
эти понятия подробнее.
Алгоритмический язык (язык программирования), как средство
записи алгоритмов, является формализованным средством, предна­
значенным не столько для общения между людьми, сколько для об­
щения между человеком и ЭВМ. Это определяет следующие разум­
ные требования к языку программирования.
1. Он должен быть достаточно простым^ чтобы быть доступ­
ным широкому кругу людей.
2. Язык должен допускать однозначное истолкование алгорит­
ма, чего нельзя сказать об обычном разговорном языке.
3. Алгоритм, записанный на некотором языке, должен быть
сначала переведен на машинный язык ЭВМ. Переводом занимается
специальная программа - транслятор. Язык должен быть простым
и в том плане, чтобы не было особых сложностей при создании и
функционировании транслятора.

27
Этим требованиям в достаточной степени удовлетворяют язы­
ки Си/С++.
Языки Си/С++, как и любые другие языки программирования,
полностью определяются заданием их алфавита (словаря исходных
символов), точным описанием их синтаксиса (грамматики) и се­
мантики (смысла) - "СИНТАКСИС + СЕМАНТИКА" (см. рис. 10).

- напоминает о том, что, например, буквы греческого алфавита


использовать нельзя

ж нельзя пользоваться римскими символами

/* */
\ \п \г \t \" \'
и др.
/* + - ; % » «
< > < = > = == !=
& I - '^ ! . - > { } ( ) &&
: = + = - = *= /= %= » = « =
&= 1= '^= [ ] ++ - ,
Строч­
ные и Только в языке C++
:: // .* ->*
пропис­ и др.
ные ла­
тинские
буквы Специальные символы
J..

АЛФАВИТ (ЛИТЕРЫ)
СИНТАКСИС + СЕМАНТИКА
Простота Однозначность
ЯЗЫК ПРОГРАММИРОВАНИЯ
Рис. 10. Язык программирования и его описание

Алфавит языка - набор основных символов (литер), исполь­


зуемых для записи алгоритма. На рис. 10 изображены литеры языков
Си/С++ - три "лепестка цветка". Следует отметить, что некоторые
литеры алфавита являются составными, изображаются двумя или
тремя символами, но рассматриваются как неделимые (например,
"+=" или " » = " ) .
Рис. 1 1 содержит информацию о способах описания синтакси­
са языка, где обращают на себя внимание две фамилии — Д. Бэкус и
Н. Вирт и приведена информация, касающаяся способов описания

28
синтаксиса языка, предложенных Д. Бэкусом и Н. Виртом в проти­
вопоставлении друг другу (две параллельных колонки).
Из допустимых символов языка, указанных на рис. 10, можно
писать программу на языке Си/С++, но не в произвольном виде, а в
соответствии с синтаксисом языка. Удобными способами описания
синтаксиса языка являются следующие способы.
1. Использование металингвистических формул (предложены
Д. Бэкусом, автором языка АЛГОЛ-60).
2. Синтаксические диаграммы (предложены Н. Виртом, авто­
ром языка Паскаль).
На рис. 11 приведены определения одних и тех же понятий как
через металингвистические формулы, так и через синтаксические
диаграммы.
Металингвистическая формула позволяет определить некото­
рое понятие путем перечисления всех его значений. Она использует
следующие обозначения:
"::=" - знак, который читается как "это есть по определению";
<Определяемое_понятие> - пишется слева от "::=";
I - обозначает "ИЛИ";
( ) — круглые скобки, обозначают "И";
{ } — фигурные скобки, обозначают неограниченное повторение
ноль, один, два и т.д. раз, заключенной в них конструкции;
[ ] — квадратные скобки, обозначают необязательность конст­
рукции, заключенной в эти скобки.
Из рис. 11 следует, что в языке Си два имени, имеющие совпа­
дающие восемь первых символов, будут восприниматься одинако­
выми. Вместе с тем отметим, что в интегрированных средах про­
граммирования на языке C++ различимая длина идентификаторов
может задаваться программистом с помощью соответствующей оп­
ции. Прописные и строчные буквы идентификаторов различимы.
Так, в частности, ALPHA и alpha - разные идентификаторы.
Использование синтаксических диаграмм поясняет правый
столбец на рис. 11. Синтаксическая диаграмма - это схема, состав­
ленная из линий со стрелками, прямоугольников и овалов. В прямо­
угольник заключают объект, определенный в другом месте, а в ова­
лы - литеры или составные символы языка. Сопоставляя определе­
ния понятий обоими способами легко понять смысл и особенности
применения синтаксических диаграмм. Из сопоставления можно за­
ключить, что метод синтаксических диаграмм проще и нагляднее.
Его, в основном, и рекомендуется использовать.

29
д. Бэкус Н. В и р т
Металингвистические Синтаксические
формулы диаграммы
Прописная_буква Строчная_буква
<Прописная_буква> ::= A|B|C|...|Z
<Строчная_буква> ::= a|b|c|...|z

-Буква> ::= ( <Прописная_буква> | Буква


<Строчная_буква> ) ^ Прописнаябуква
^ ^
Строчнаябуква •—J

-Ненул_восьм_цифра> 1|2|...|7 Ненул_восьм_цифра

-Восьмеричная_цифра> ::=
Восьмеричная_цифра
( <Ненул_восьм_цифра>|0 )
1
Ненул_восьм
_цифра
I
<Ненул__дес_цифра> ::= Ненул_дес_цифра
( <Ненул_восьм_цифра>|8|9 )
X
Ненул_восьм
_цифра

<Десятичная_цифра> :;= Десятичная_цифра


( <Ненул^ес_цифра>|0 )

Ненул_дес_
цифра
I
<Идентификатор> ::= ( <Буква>|_ ) Идентификатор
{ ( <Буква>|
<Десятичная_цифра>|_)}
Буква Буква

!!! ДЛИНАИДЕнтификатора !!! Дес_цифра

Ж
Эквивалентно
ДЛИНАИДЕ 8! >К. о-
Рис. 1 1. Способы описания синтаксиса языка

Семантика определяет смысл предложений (операторов), за­


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

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

2.3. Структура и конструкция программы на Си/С++

Базовыми элементами языков Си/С++ являются:


• комментарии;
• идентификаторы;
.• служебные (зарезервированные) слова;
• константы;
• операторы;
• разделители.
Из базовых элементов строится программа. Рассмотрим снача­
ла базовые элементы, а затем и структуру программы.

2.3.1. Комментарии

Синтаксическая диаграмма комментария к фрагменту Си-


программы приведена на рис. 12.

Комментарий

>Ci*

Печатный символ

Рис. 12. Определение комментария в языке Си

В Си-программе комментарии используются для документиро­


вания и могут начинаться и заканчиваться в любом месте програм­
мы, где может находиться символ "пробел", и могут содержать лю­
бое количество строк:
/'*' Это однострочный комментарий */

/-^
Компилятор языка Си рассматривает эти
строки как комментарий
"-/

Обратите внимание, что вложенные комментарии наподобие


показанного ниже не допускаются стандартом ANSI и большинст­
вом компиляторов:

31
Эта часть комментария правильная
/'*• Начало этого комментария игнорируется. */
Эта строка теперь находится вне комментария! Ошибка!

Этот пример показывает, что внутренняя пара символов "/*"


игнорируется, а первая же пара символов "*/" завершит коммента­
рий. Тем самым, предпоследняя строка и последняя пара символов
"*/" окажутся вне комментария и при попытке их компиляции будет
выдано сообщение об ошибке.
Наряду с рассмотренными вариантами в языке C++ имеется и
другая форма записи комментария:
/ / Это однострочный комментарий

Комментарии подобного вида удобно использовать как ло­


кальные комментарии для пояснений к определению некоторого
объекта или пояснений к отдельному оператору.
/* Коммен- // Такое вложение возможно! -тарий */
// Коммен- /* И так тоже можно! */ -тарий

Двусмысленность!
X = Y//* Это деление */Z;

Надо так:
X = Y/ /->" Это деление ^/ Z;

2.3.2. Идентификаторы

Идентификаторы были рассмотрены выше (см. рис. 11). Иден­


тификатор представляет собой имя некоторого объекта программы.
Подробнее об объектах программы говорится ниже в разд. 3.

2.3.3. Служебные слова

Служебные слова представляют собой идентификаторы, имею­


щие специальное значение для компиляторов языков Си/С++. Их
нельзя использовать как имя переменной. Ниже приведен список
служебных слов языка C++:
asm auto bool break
case catch char class
const const cast continue default
delete do double
dynamic cast else enum explicit

32
export extern false float
for friend goto if
Inline int long xmitable
namespa.ce new operator private
protected public register reinterpret^cast
return short signed sizeof
static static_cast struct switch
template this throw true
try typedef typeid typename
vmion unsigned using virtual
void. volatile wchar t while

Трансляторы языков Cu/C++, соответствующие требованиям


стандарта ANSI, воспринимают только слуэюебные слова, записан­
ные строчными буквами. Функции служебных слов будут рассмат­
риваться ниже по мере изучения материала.
Напоминаем, что не следует использовать имена объектов
(идентификаторы), совпадающие со служебными словами.

2.3.4. Константы

Определение константы с помощью синтаксической диаграм­


мы приведено на рис. 13.

Константы
Целая_костанта

#i Символьная константа

Строковая_константа

Константа с пл. точкой

Рис. 13. Определение константы

Константы, в отличие от переменных, являются фиксирован­


ными значениями^ которые можно вводить и использовать на языках
Си/С++.

Целые константы. Целые константы (рис. 14) не имеют


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

33
дится иметь дело с данными, представляющими комбинации битов
(получаются более короткие записи). Определение десятичной,
восьмеричной и шестнадцатеричной констант приведено на рис. 15 -
17.
Целые константы могут быть обычной длины или длинные.
Длинные целые константы оканчиваются буквой "/" или "L"
Размер целых констант обычной длины зависит от реализации,
(для шестнадцатиразрядного процессора — 2, для тридцатидвухраз­
рядного — 4 байта). Длинная целая константа всегда занимает 4 бай­
та. Таким образом, на тридцатидвухразрядном процессоре эквива­
лентны длинная целая константа и целая константа обычной длины.

Целая_константа
Десятичная_константа

Восьмер._константа

#J Шестнад._константа —••

Рис. 14. Определение целой константы

Десятичная_константа

Ненул ._десят._цифра

Десятичная_цифра
11 -1028 57944L
Рис. 15. Определение десятичной константы

Восьмеричная_константа

ч2>- Восьм._цифра
013 02000 0160000L
Рис. 16. Определение восьмеричной константы

34
Шестнадцатеричная_константа

\ ^ Шестнадцатерич-
М о ) ^ ная_цифра

] — • щ

Шестнадцатеричная цифра 0X400


ОхЬ
OxEOOL

Десят._цифра

Рис. 17. Определение шестнадцатеричной константы

Внутреннее представление константы целого типа в ЭВМ —


целое число в двоичном коде. При использовании десятичной целой
константы старший бит числа интерпретируется как знаковый (О —
положительное число, 1 — отрицательное). Для восьмеричных и ше-
стнадцатеричных целых констант возможно представление только
положительных чисел и нуля, поскольку старший разряд рассматри­
вается как часть кода числа, а не как его знак. Более подробное об­
суждение внутреннего представления в ЭВМ целых констант выхо­
дит за рамки данной книги и будет рассмотрено при изучении
арифметических основ построения ЭВМ.
Диапазон значений десятичных констант обычной длины для
шестнадцатиразрядного процессора - от -32768 до +32767, для три-
дцатидвухразрядного процессора - ^ ^ ...-г^^ i;; Диапазон значе­
ний восьмеричных и шестнадцатеричных констант обычной длины
для шестнадцатиразрядного процессора - 0...(2'^-1), для тридцатид­
вухразрядного процессора - 0...(2^2 _])
Диапазон значений длинных десятичных констант не зависит
от разрядности процессора и составляет ^""-^ •+(2 -1)) Диапазон
значений длинных восьмеричных и шестнадцатеричных констант
также не зависит от разрядности процессора и составляет 0...(232 _])

Константы с плавающей точкой. Определение константы с


плавающей точкой приведено на рис. 18. Внутреннее представление
в ЭВМ констант с плавающей точкой состоит из двух частей — ман­
тиссы и порядка. При этом константы с плавающей точкой типа
float занимают 4 байта, из которых один двоичный разряд отводится
под знак мантиссы, 8 разрядов под порядок и 23 под мантиссу. Ман­
тисса - число большее 1,0, но меньшее 2,0. Поскольку старшая циф-

35
pa мантиссы всегда равна 1, то она не хранится. Для констант с пла­
вающей точкой типа double, занимающих 8 байт, под порядок и
мантиссу отводятся соответственно 11 и 52 разряда. Длина мантис­
сы определяет точность числа, а длина порядка — диапазон числа.
Для констант с плавающей точкой типа long double под число отво­
дится 10 байт. Также заметим, что более подробное обсуждение
внутреннего представления fe ЭВМ констант с плавающей точкой
выходит за рамки данной книги и будет рассмотрено при изучении
арифметических основ построения ЭВМ.
В языке C++, когда в конце константы с плавающей точкой от­
сутствуют б у к в ы / F, /, L, константа имеет тип double (8 байтов или
64 бита с диапазоном значений ±l,7•10~^°^..±l,7 •Ю^'*^^). Если же кон­
станта заканчивается б у к в о й / и л и F, то она имеет тип float, занима­
ет 4 байта и диапазон значений ±3,4•10'^^..±3,4•10^^^. Аналогичным
образом, при завершении константы буквами / или L константа име­
ет тип long double, занимает 10 байт с диапазоном значений

Константа_с_плавающей_точкой
F-константа

F-константа F-константа

•СУ
е Hh F-константа

10. Ю.Гэкв. 10.F


0.0054 0/00541 экв. 0.0054L
.0054
F-константа 5.5е-3

<7>
-• Десятичная_константа

Рис. 18. Определение константы с плавающей точкой

36
Символьные константы. Для кодирования одного символа
используется байт (восемь битов). Благодаря этому набор символов
содержит 256 символов, образующих две группы:
• печатные символы;
• непечатные символы.
Непечатным символам соответствуют специальные управляю­
щие коды, которые служат для управления внешними устройствами
или для других видов управления. В качестве примера непечатного
символа назовем символ перехода к новой странице, управляющий,
например, работой принтера.
Символьная константа в языках Си/С+-ь состоит либо из одно­
го печатного символа, заключенного в апострофы, либо управляю­
щего кода, заключенного в апострофы. Управляющие коды пред­
ставляют непечатные символы (табл. 5). Символьная константа рас­
сматривается как символьный беззнаковый тип данных с диапазо­
ном значений от О до 255. Константа ' \ 0 ' называется нулевым сим­
волом или нулевым байтом.
Примеры: 'д:' ' Г Лп'

Табл. 5. Управляющие символы (коды)


Управляющий код Назначение
\п Переход к новой строке
V Возврат каретки
\/ Горизонтальная табуляция
\v Вертикальная табуляция
\b Возврат на одну позицию
Y Переход к новой странице
\\ Обратная косая черта
\» Кавычка
\' Апостроф
\шестнадцатеричная_константа или
\восьмеричная_константа

Строковые константы. Строковая константа содержит по­


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

37
"Эта строка содержит символ табуляции \t"
"В строке указан символ,, вызывающий звуковой сигнал: \07'

Последняя строка пустая, в ней нет ни одного символа, однако


для ее хранения используется один байт - завершающий нулевой
байт.

"СтрокаЛп"
С т Р о к а \п \0 Байты памяти, содержащие коды от
О до 255
Нулевой
байт
Рис. 19. Размещение строки в оперативной памяти

2.3.5. Структура Си-программы

Си-программа - совокупность одного или нескольких модулей.


Модулем является самостоятельно транслируемый файл. Такой файл
обычно содержит одну или несколько функций. Функция состоит из
операторов языка Си. Структуру Си-программы иллюстрирует рис.
20.
Си-программа
Модуль (файл с определениями данных и операторами)
Внешние определения данных
Функция
Внутренние определения данных
Операторы

Функция
Внутренние определения данных
Операторы

Модуль (файл с определениями данных и операторами)


Внешние определения данных
Функция
Внутренние определения данных
Операторы

Функция
Внутренние определения данных
Операторы

Рис. 20. Структура Си-программы

38
Термин "функция" в языках Си/С+-*- охватывает понятия "под­
программа", "процедура" и "функция", используемые в других язы­
ках программирования. Как следует из рис. 20, Си/С++-программа
может содержать одну функцию (главная функция main) или любое
количество функций. Выполнение программы начинается с главной
функции. Приведем простой пример подобной программы.

Си++. Программа с одним модулем (файлом) и двумя функция


ми. Чтение с клавиатуры одной строки символов, заканчивающей­
ся символом '\п ' . Каждый символ печатается вместе с его деся­
тичным, восьмеричным и шестнадцатеричным кодами
V

^include <stdio.h> // Для функций ввода-вывода


// В результате выполненмя директивы включения на место
// предыдущей строки помещается содержимое файла stdio.h
// Прототип функции: используется компилятором для проверки
// правильности записи заголовка в определении функции и
// правильности вызова функции
void convert( ±nt ) ;

// Выполнение программы начинается с выполнения следующей


// ниже главной функции
int main ( void ) / / Возвращает О при успехе
{
±пЬ ch; // Прочитанный символ
// На экран выводятся две строки, являющееся аргументами
// функции экранного вывода printf
print f ( "\п Программа изображает символы и их "
"коды. \п" ) ;
printf( "\п Наберите строку символов и нажмите клавишу "
"Enter. \п" ) /
ch = getchar( ) ; // Подождать ввода символа
while ( ch != '\п' ) // '\п' вводится после нажатия Enter
{
// Вызов функции печати символа ch и его десятичного,
// восьмеричного и 16-ричного кодов. Компилятор
// контролирует правильность вызова функции,
// используя ее прототип
convert ( ch ) ;
ch = getchar( ) ; // Ввод следующего символа
}
printf( "\п Обработка закончена. \п" ) ;

return 0;

// Определение функции, печатающей символ и его коды

39
void convert (
±nt ch ) // Изображаемый символ
{
printf( "Символ 10 код 8 код 16 код \п" ) ;
// Непечатные символы имеют десятичные коды О.. 31, а
// десятичный код символа ' ' равен 32
± f ( c h < ' ' )
printf( "Управляющий (непечатный) символ: \п" ) ;
// Обратите внимание, что один и тот же символ печатается
// вначале в символьном формате %с, а затем
// соответственно в форматах десятичного %d,
// восьмеричного %о и 16-ричного %к чисел. Число
// форматов и количество следующих за управляющей
// строкой аргументов совпадают
print f( "%с %d %о %х \п",
ch, ch, ch, ch ) ;

return;
}

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


программы, но и иллюстрирует, как нужно оформлять программу,
использовать комментарии и т.п.
Проанализируем этот пример и подведем некоторые итоги.
Каждая Си-программа должна иметь одну и только одну главную
функцию с именем main. С этой функции начинается исполнение
программы. Другие функции могут быть вызваны из функции main
или из какой-либо другой функции в процессе выполнения про­
граммы. Эти функции могут находиться в том же модуле (файле),
что и функция main, или в других модулях.
Функция может иметь нудь или более аргументов. Аргументы
являются переменными, которые используются для передачи дан­
ных между функциями (main не имеет аргументов, а функция
convert имеет один аргумент - переменную ch).
Каждая функция после своего заголовка содержит блок, кото­
рый начинается с "{" и заканчивается " } " . Блок содержит определе­
ния данных, за которыми следуют операторы функции. Определения
данных создают переменные, которые будут использованы в функ­
ции. Операторы задают действия, которые должны быть выполнены
над переменными.
Все элементы данных должны быть определены перед их ис­
пользованием. Определения данных всегда завершаются точкой с
запятой. Операторы также завершаются точкой с запятой.

40
2.4. Простой ввод-вывод в языках Си/С++

Языки Си/С++ не содерэюат встроенных средств ввода-


вывода. Для реализации ввода-вывода в составе системы програм­
мирования Си/С+-ь поставляется библиотека стандартных функций,
содержащая наряду с другими функциями функции ввода-вывода.
Функции ввода-вывода библиотеки позволяют читать данные из
файлов на магнитных дисках и с устройств и писать данные в файлы
и на устройства. Библиотека Си поддерживает три уровня ввода-
вывода:
• ввод-вывод потока;
• ввод-вывод нижнего уровня;
• ввод-вывод для консоли и порта.
Здесь мы рассмотрим только ввод-вывод потока.

2.4.1. Ввод-вывод потока

При вводе-выводе потока все данные рассматриваются как по­


ток отдельных байтов - это либо файл на магнитном диске, либо фи­
зическое устройство (дисплей, печатающее устройство и т.п.). Та­
ким образом, операции ввода-вывода потока означают работу с фай­
лами или устройствами.
Ввод-вывод потока позволяет.
1. Открывать и закрывать потоки.
2. Читать и записывать символ.
3. Читать и записывать целые данные.
4. Читать и записывать строки.
5. Читать и записывать форматированные данные любого типа.
6. Анализировать ошибки ввода-вывода потока и достижение
конца потока (конца файла).
Для использования функций ввода-вывода потока в программе
необходимо директивой include включить в состав текста програм­
мы файл stdio.h:
^include <stdio.h>

Обработка данной директивы состоит в замене строки ди­


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

41
Открытие потока. Перед выполнением операций ввода-
вывода для потока его нужно открыть. Для этой цели служит функ­
ция уЬрег7(^^, описание которой имеет вид:
^include <stdlo.h>
FILE * fopen ( // Возвращает указатель на открытый
// файл
const char *path. // Указатель на имя открываемого
// файла
const cha.i: *type ); // Указатель на вид доступа к файлу

Функция открывает файл path в режиме доступа type. Сим­


вольная строка type задает вид доступа к файлу в соответствии с
табл. 6. Функция/open возвращает указатель на открытый файл. Ну­
левой указатель (NULL) означает ошибку. Многочисленные приме­
ры открытия файлов с контролем ошибок приведены ниже.

Табл. 6. Виды доступа к файлу


Вид доступа type Назначение
"г" Открывается файл для чтения. Если файл не существует или не
может быть найден, то возникает ошибка
'V' Открывается пустой файл для записи. Если файл уже
существует, то его содержимое будет уничтожено
"л" Открывается файл для записи в конец (добавления). Если файл
не существует, то он сначала будет создан
•V+" Открывается файл для чтения и для записи (файл должен
существовать, иначе - ошибка)
"w+" Открывается пустой файл для чтения р для записи. Если файл
уже существует, то его содержимое будет уничтожено
"а+" Открывается файл для чтения и для записи в конец
(добавления). Если файл не существует, то он сначала будет
создан

Закрытие потока. Для закрытия потока служит функция


fclose(), которую следует вызвать сразу же после окончания работы
с потоком:
ilnclude <stdio.h>

±nt fclose( / / Возвращает О при успехе и EOF при


// ошибке
FILE stream ) ; / / Закрываемый поток

Примеры закрытия файлов с контролем ошибок приведены


ниже.

Предопределенные указатели потока. В начале выполнения


Си-программы автоматически открывается пять потоков:

42
• стандартный ввод (предопределенный указатель stdin);
• стандартный вывод (предопределенный указатель stdout);
• стандартный вывод сообщений об ошибках (предопределенный
указатель stderr);
• стандартный дополнительный поток (предопределенный указа­
тель stdaux);
• стандартная печать (предопределенный указатель stdprn).
По умолчанию stdin соответствует клавиатуре терминала,
stdout и stderr - экрану терминала, stdaux - дополнительному порту и
stdprn - печатающему устройству.
Предопределенные указатели пяти перечисленных стандарт­
ных потоков можно использовать в любой функции ввода-вывода,
которая в качестве аргумента требует указатель потока.

Функции чтения из потока и записи в поток. Функции чте­


ния из потока и записи в поток, имеющиеся в языке Си, перечисле­
ны в табл. 7.

Табл. 7. Функции чтения из потока и записи в поток


Объект Чтение Чтение из Чтение Запись в Запись в Запись в
операции из stdin любого из стро­ stdout любой строку Си
потока ки Си поток
Серия бай­ fread fwrite
тов
Символ getc fgetc put fputc
getchar fgetchar putchar fputchar
ungetc
Данное int getw putw
Строка gets /gets puts /puts
Формат, scan/ fscanf sscanf print/ /print/ sprint/
данные vprintf v/print/ vsprint/ 1

На данном этапе среди функций, перечисленных в таблице,


рассмотрим лишь универсальные функции для ввода scan/-/scan/ и
для вывода printf-fprint/

2.4.2. Ввод с использованием функций scanf-fscanf

Вначале рассмотрим и проанализируем несколько примеров.


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

/*
Программа-пример 1 (начало) .

43
ввод в ЯЗЫКЕ Си • написать фрагмент Си-программы^ которая
из файла " exl dat" на магнитном диске прочитает указанные ни-
же значения:
±пЬ 1г // OxFA
а. // -22
12; // 074
float f; // 1.57
long 1; // -125874
char ch. // 'Z '
str[ 20 ];// "Нам Тхань"
Написать вид читаемых данных (вид строк в файле
"exl. dat")
-^/
^Include <stdlo.h> // Для функций ввода-вывода

int main ( void ) // Возвращает О при успехе


{
int i, 11^ 12;
float f;
long 1;
char ch, str[ 20 ];
FILE *f_ln; // Указатель на структуру со
// сведениями о файле для ввода
int ret code; // Возвращаемое значение для fscanf

// Открываем файл exl.dat для чтения


f_ln - fopen( "exl.dat", "г" ) ;
if( f__ln -= NULL )
{ // Ошибка открытия файла
printf( "\n Файл exl.dat для чтения не открыт. " ) ;
return 1;
}

// Читаем данные из файла exl.dat


retcode = fscanf( f_ln,
" 1 = %х ll=%d 12=%о f=^%f l = %ld ch^%c str=%s%c%s",
&1, Sell, &12, &f, &1, &ch, str, &str[3], &str[4] ) ;
if( retcode != 9 )
{
print f ( "\n Данные в fscanf прочитаны с ошибками." ) ;
return 2;

return О;
}
// Конец примера 1
Вид строк исходных данных в файле exl.dat:
l=OxFA 11=-22 12=074

f==1.57 1 = -125874 ch=z


str=HaM Тхань

44
Как работает функция fscanf?
Вначале слева направо просматривается управляющая строка
"...". Если очередным символом является символ "пробельной груп­
пы" (пробел или '\Г' или \п'), то в исходных данных (во входном по­
токе) пропускаются все подряд идущие символы пробельной груп­
пы, пока не встретится другой символ.
Если в управляющей строке встретится формат, который на­
чинается с символа "%", то из входного потока читается последова­
тельность символов до пробельного символа. Она преобразуется в
кодовый формат в соответствии с типом формата и записывается по
адресу, заданному в соответствующем аргументе (запись &/ означа­
ет адрес, по которому в оперативной памяти размещается перемен­
ная /). Если до пробельного символа раньше встретится символ, не
допустимый в записи читаемого значения, то ввод по текущему
формату остановится на этом символе. Символ управляющей стро­
ки, следующий за символом "%", указывает способ преобразования
символов из входного потока в кодовый формат (табл. 8).
!!! Число форматов и число аргументов в ф у н к ц и и / у с а « / о б я ­
зательно должно быть одинаковым.
Если в управляющей строке встретился символ, отличный от
символа пробельной группы и от символа "%", то функция fscanf
считывает очередной символ из входного потока. При несоответст­
вии прочитанного символа символу, указанному в управляющей
строке, функция/л'сал?/прерывает работу и конфликтный символ ос­
тается во входном потоке. В случае соответствия прочитанный сим­
вол пропускается и функция продолжает работу. Отмеченная осо­
бенность позволяет организовать так называемый "не слепой" ввод.
Это означает, что во входном потоке (в файле исходных данных) с
помощью лидирующих символов можно указать, к какой перемен­
ной относится вводимое значение (см. текст примера выше).
Рассмотрим еще один пример, являющийся логическим про­
должением предыдущего примера.

/*
Программа --пример 2 (начало) .
ВВОД В ЯЗЫКЕ Си:
1. Написать фрагмент Си-программы, которая из файла
"exl.dat" на магнитном диске прочитает указанные ниже значе-
ния:
±пЬ 1; // Ох FA
short 11/ // -22
±nt 12; // 074
float f; // 1.57
long- 1; // -125
char ch. // 'z'

45
str[ 20 ];// "Нам Тхань"
2, Написать вид читаемых данных (вид строк в файле
"exl.dat")
*/

Табл. 8. Способы преобразования символов при вводе


1 Сим­ Тип, ожидаемый при вводе Тип аргумента
вол
за%
d Десятичное целое Указатель на int
D Десятичное целое Указатель на long int
о Восьмеричное целое Указатель на int
О Восьмеричное целое Указатель па long int
х,Х Шестнадцатеричное целое без префиксов Указатель на int
Ох или ОХ
i Десятичное, шестнадцатеричное или вось­ Указатель на int
меричное целое
I Десятичное, шестнадцатеричное или вось­ Указатель на long int
меричное целое
и Десятичное целое без знака Указатель на unsigned int
и Десятичное целое без знака Указатель на unsigned long
int
\e,E,f, Величина с плавающей точкой из мантис­ Указатель из.float
сы и порядка
с Символ. Пробельные символы, которые Указатель на char
обычно пропускаются, считываются, если
указано "с". Чтобы прочесть из потока сле­
дующий не пробельный символ, исполь­
зуйте формат Vols
S Символьная строка Указатель на символьный
массив, достаточно боль-
пюй, чтобы разместить
вводимое поле и завер­
шающий нуль-символ '\0',
добавляемый автоматиче­
ски
% Символ % Не преобразуется, участвует
во вводе как символ '%'
/? • Из потока ничего не читается Указатель на переменную
типа int, в которую записы­
вается количество симво­
лов, считанных из потока
вплоть до этой точки при
текущем вызове функции
Р Величина в виде XXXX:YYYY^ где цифры X Указатель на объект (far*
и Y являются шестнадцатеричными циф­ или near*). Формат %р вы­
рами верхнего регистра полняет преобразование
указателя к требуемому
указателю используемой
модели памяти |

46
^include <stdio.h> • // Для функций ввода-вывода
int main ( void ) // Возвращает 0 при успехе
{
Int i , 12;
short 11;
float f;
long 1;
char ch, str[ 20 ];
FILE *f_ln/ // Указатель на структуру со
// сведениями о файле для ввода
±nt ret code/ // Возвращаемое значение для
// fscanf
// Открываем файл exl.dat для чтения
f_ln = fopenC "exl.dat", "г" ) ;
±f( f__ln == NULL )
{
printf( "\n Файл exl.dat для чтения не открыт. ") ;
return 1;
}

// Читаем данные из файла exl.dat


retcode = fscanf( f_ln,
" 1 = %х ll = %hd 12^%о f=^%f l = %41d874 ch = %c str=%s%c%s",
&1, &11, &12, Scf, Sclr &chr str, &str[3], &str[4] ) ;
±f( retcode != 9 )
{
prlntf( "\n Данные в fscanf прочитаны с ошибками." ) ;
return 2;
}

return 0;
}
// Конец примера 2
Вид строк исходных данных в файле exl.dat:
l^OxFA 11=-22 12=074

f=1.57 1=-125874 ch=z


str=HaM Тхань
V

Из рассмотренного примера следует, что в общем случае


структура формата имеет вид:

% ['^] [ширина ] [префикс] тип

Звездочка (*), следующая за знаком процента, подавляет запо­


минание следующего вводимого поля. Поле считывается в соответ-

47
ствии с форматом, но преобразованная величина никуда не записы­
вается. "Ширина" - положительное десятичное целое, задающее
максимальное число символов при вводе. Если "ширина" избыточ­
ная, то чтение, как и ранее, выполняется до пробельного символа.
Если "ширина" меньше, чем число символов до пробельного, то чи­
таются и преобразуются только символы числом не более "ширина",
(см. пример 2).
Префиксами могут быть:
N - используется для печати адресов near (формат %Np);
F - используется для печати адресов far (формат УоГр);
h - для ввода коротких целых с типом short (см. пример 2);
/ - для ввода длинных целых и вещественных с типом long (см.
пример 2).
Рассмотрим еще один, более сложный, иллюстрирующий при­
мер.

V7*
Программа --пример 3 (начало) .
ВВОД В ЯЗЫКЕ Си:
1. Написать фрагмент Си-программы, которая из файла
"exl.dat" на магнитном диске прочитает указанные ниже значе-
ни я:
±пЬ ±. // OxFA или 250
11, // 74
12; // 18
float f; // 1,57
long- 1; // -125874
char ch, //
str[ 20 ];// "Нам Тхань"
2. Написать вид читаемых данных (вид строк в файле
"exl.dat")
V
^include <stdlo.h> // Для функций ввода-вывода

±nt main ( void ) // Возвращает О при успехе


{
Int 1, 11, 12;
float f;
long 1;
сЪаг ch, str[ 20 ];
FILE *f_ln; // Указатель на структуру со
// сведениями о файле для ввода
±nt ret code; // Возвращаемое значение для
// fscanf
// Открываем файл exl.dat для чтения
f_ln = fopen( "exl.dat", "г" ) ;
±f( f_ln == NULL )
{

48
printf ( "\n Файл exl.dat для чтения не открыт. " ) ;

// Читаем данные из файла exl.dat


retcode = fscanf( f_in,
" %х %d %о %f %ld %c %s%c%s"r &±f &il, &i2, &f, &1,
&ch, str, &str[3], &str[4] ) ;
±f( retcode 1=9)
{
printf( "\n Данные в fscanf прочитаны с ошибками." ) ;
r-etuirn 2;
}

// За крыва ем файл
retcode = fclose( f_in ) ;
±f( retcode == EOF )
{
printf( "\n Файл exl.dat не закрыт." ) ;
return 3;
}

jzebvLzm 0;
}
// Конец примера 3
Вид строк исходных данных в файле ех1.dat:
OxFA 074

22 1.57 -125874 z
Нам Тхань
V
В этом примере /1 получает значение 74, так как читается по
формату Vod (десятичный формат). Аналогично, /2 получает деся­
тичное значение 18, так как читается по формату Voo (восьмеричный
формат - восьмеричный код 22 соответствует десятичному коду 18).
В заключение отметим, что функция 5са«/идентична функции
fscanf^ но вместо входного потока, заданного первым аргументом,
она по умолчанию использует предопределенный входной поток
stdin. По этой причине в вызове функции scan/ CUWCOK аргументов
начинается сразу с управляющей строки.

2.4.3. Вывод с использованием функций printf-fprintf

Вначале рассмотрим и проанализируем пример. Особенностью


приведенной ниже программы-примера также является ее оформле­
ние в виде, предусматривающем возможность выполнения на ЭВМ.

49
Программа-пример 4 (начало) .
ВЫВОД В ЯЗЫКЕ Си/С++:
Укажите вид строк печати в файле fl,out на магнитном диске
после выполнения приведенной ниже программы
'^7
^include <stdio.h> // Для функций ввода-вывода

±nt main ( void. ) // Возвращает О при успехе


{
// Данные для печати
float f = 1.5е2;
long- double
Id = 2.0e-3L;
±пЬ i = 7/
long- ±nb 11 = 121;
short ±nt si = 5;
FILE *f_out/ // Указатель на структуру со
// сведениями о файле для вывода
int retcode; // Возвращаемое значение для fсlose

// Открываем файл fl.out для записи


f_out = fopen( "fl.out", "w" );
±f( f_out == NULL )
{
print f ( "\n Файл fl.out для записи не открыт. " );
return 1;
}

// Записываем в файл fl.out


fprlntfi f__out, " %30s\n f=%f %5s l = %10d\n",

fprlntfi f_out, " ld=%-Lf f=%15f f=%15.2f f=%+15.2f\n".


Id, frf. f );
fprlntf( f_out, " l = %10.5d f=^%E ll = %ld sl = %hl\n",
i/ ff 11, si );

// Закрываем файл fl.out


retcode = fclose( f_out );
±f( retcode == EOF )
{
printf( "\n Файл fl.out не закрыт." );
return 2;
}

return 0/
}
// Конец примера 4

Как работает д^уякияя fprintjl


Первый аргумент в вызове функции (foui) указывает поток, в
который производится запись (вывод). Работа функции начинается с

50
просмотра слева направо управляющей строки "...". Если в управ­
ляющей строке нет ни одного формата, то после нее аргументов то­
же не будет. Символы в управляющей строке "..." могут быть трех
видов.
1. Управляющие символы (в кодовой таблице первые 32 сим­
вола), примерами управляющих символов являются '\п\ \t\ '\0х7' и
т.д. Если встречается управляющий символ, то он выполняет пред­
писанные ему действия. Например, '\п' вызовет переход в потоке ( в
нашем случае в файле /Lout) на следующую строку, '\^' - выполнит
печать пробелов в соответствии с используемым значением табуля­
тора (табулятору может соответствовать 2 - 1 6 пробелов, обычно че­
тыре или восемь) и т.д.
2. Форматы, которые начинаются с символа "%". Если встре­
тился формат, то из списка аргументов берется соответствующий
ему аргумент, значение которого преобразуется в соответствии с
типом формата и выводится в поток (в нашем случае в файл fJ.out).
!!! Число форматов в управляющей строке должно быть равно
числу аргументов !!!
3. Остальные символы, которые называются печатными и вы­
водятся в выходной поток в том виде, как они изображены.
Работа функции заканчивается после просмотра управляющей
строки до конца или при возникновении ошибки.
Формат имеет следующую структуру (в квадратных скобках
указаны поля формата, которые могут отсутствовать):
% [флаг] [ширина__поля_вывода ] [ . точность] [префикс] тип

Допустимые значения полей "флаг", ".точность", "тип" и дей­


ствия, выполняемые перечисленными полями, приведены в табл. 9 -
11. Примеры их использования даны выше. Результаты действия
форматов приведены в файле результатов/7.оwr, где условно с по­
мощью символа "^" показано расположение пробелов.
Поле "флаг" управляет выводом в поток ( табл. 9). В формате
может быть указано несколько флагов одновременно, если они не
противоречат друг другу.
Поле "ширина_поля_вывода" задает минимальное число выво­
димых символов. Это неотрицательное целое десятичное число. Ес­
ли "ширина" излишняя, то слева или справа, в зависимости от флага
"-", поле вывода дополняется пробелами. Если ширина недостаточ­
на, то поле вывода увеличивается до требуемой длины, т.е. усечения
выводимого данного не будет!

51
Табл. 9. Действие флагов форматирования
Флаг Смысл Значение по умолча­
нию
- Выравнивание результата по левому краю Выравнивание по пра­
заданного поля вому краю
+ Вывод величины с указанием знака "+", если Знак выводится толь­
величина принадлежит к типу со знаком ко для отрицательных
величин
Пробел Вывод пробела перед величиной, если это по­ Пробел не выводится
ложительное число со знаком
# Для форматов о, х или Х выводит перед числом Префикс в указанных
префикс 0, Ох или ОХ соответственно. Для случаях не выводится.
форматов е, Е и л и / в ы в о д и т число с десятич­ Десятичная точка
ной точкой. выводится, если за
Для форматов g, G выводит число с десятич­ ней следует цифра.
ной точкой и предотвращает усечение лишних Лишние нули
нулей. усекаются.

"Точность"' - неотрицательное десятичное число, перед кото­


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

Табл. 10. Действие поля формата ".точность"


Тип Смысл Умолчание
d, i, и, Указывает минимальное число выводи­ Если точность равна нулю,
о, X, X мых цифр. Если точность меньше, чем или вообще опущена, или
надо, то число не усекается. Если точ­ стоит просто точка, то в ка­
ность больше, чем надо, то число допол­ честве точности берется еди­
няется слева нулями. ница.
Е, e,f Указывает число цифр, выводимых после Точность равна шести. Если
десятичной точки (в случае усечения по­ она равна нулю или стоит
с л е д и ^ цифра округляется). просто точка, то десятичная
точка не выводится.
g^G Указывает максимальное число значащих Выводятся все значащие
цифр выводимого числа. цифры.
с Ни на что не влияет. Выводится символ.
S Точность указывает максимальное число Символы выводятся до тех
выводимых символов. Лишние символы пор, пока не будет достигнут
строки не выводятся. нуль-символ.

"Префиксы h, I, L, N, F".
Префикс "/г" (sHort) для типов d, i, о, х, А" (табл. 11) указывает
на тип аргумента short int, а для типа и - на тип аргумента unsigned
short int.
Префикс "/" для типов d, i, о, х, Jf указывает на тип аргумента
long int, для типа и - на тип аргумента unsigned long int, а для типов
е, Е, f, g, G - на тип double.

52
Табл. 11. Символы типа
\d, i int Десятичное целое со знаком
и int Десятичное целое без знака
0 int Восьмеричное целое без знака
X или int Шестнадцатеричное целое без знака с использованием
\х цифр "abcdef' или "ABCDEF"
/ float Величина со знаком вида [-]dd.dd, где d - десятичная циф­
ра. Число цифр до точки определяется величиной числа, а
после - точностью.
\Е, е float Величина со знаком вида [-]d.ddddde[3HaK]ddd или [-
ld.ddddd£[3HaK]ddd
G'S float Величина со знаком, выводимая в ф о р м а т е / и л и е, Е, в
зависимости от того, что компактнее при заданной точно­
сти
с int Отдельный символ i
S Строка Символы выводятся или до достижения нуль-символа,
или после вывода количества символов, заданного в поле
"точность"
п Указатель на Выводится число символов, успешно записанных к дан­
целое ному м о м е т у . Эта величина присваивается переменной
inl с адресом в аргументе.
р Указатель ти­ Выводит адрес, на который указывает аргумент, в виде
па void far * ХХХХ : УУУУ (сегмент : смещение) с использованием
шестнадцатеричных цифр верхнего регистра

Префикс "Z," для типов е, Е, f, g, G указывает на тип аргумента


long double.
Префикс "F" для типов р, s, п указывает на тип аргумента
дальний указатель, а префикс 'W" для тех же типов - на тип аргумен­
та ближний указатель.
Файл результатов работы программы/7.ow/' имеет следующий
вид (символ ^ обозначает пробел):

^i=''^'^^^00007^f=l. 500000Ei-02^1±=12^si=5

Обратите внимание, что функция print/ идентична функции*


/print/, но вместо выходного потока, заданного первым аргументом,
она по умолчанию использует предопределенный входной поток
stdout. По этой причине в вызове функции/7гш(/'список аргументов
начинается сразу с управляющей строки.
Функции ргш^/х^^гш^/'возвращают число выведенных символов
при успешном завершении или EOF при ошибке. Однако это воз­
вращаемое значение обычно не контролируют.

53
2.4.4. Упражнения для самопроверки

1. Имеется следующий фрагмент Си-программы:


float a, Ь;
±nt i^ J/
cha.r cl , c2^ c3
±nt retcode;
retcode = fscanfi stdin, " %i %3d %c %c %c %f %f'\
&i, &j, &cl, &c2r &c3r Sea, &b ) ;

Строки исходных данных в файле с указателем stdin имеют


следующий вид:
17 123456
2.4еЗ 112
14,5

Какие значения получат переменные retcode, а, Ь, i, J, cl, с2,


с31

2. Имеется следующий фрагмент Си-программы:


float a;
±nt ^f jr
char cl, c2, c3;
±nt retcode;
char c4, c5, s[20];

Написать фрагмент программы, обеспечивающий чтение из


файла f.dat, на магнитном диске следующих значений:
а = 1.5 i = 21 j = -12 cl = 'в'
с2 = 'е' сЗ = 'с' с4 = 'а' с5 = 'н'
S => "Прочита иная-строка"

Как при этом будут выглядеть строки исходных данных в фай-


n^f.datl
Предусмотреть контроль корректности значений, возвращае­
мых функциями библиотеки Си.

3. В программе имеются следующие переменные:


±nt d = 254;
float f = 1234.56;
char *str = "Строка символов";

Используя, по возможности, только эти данные написать про-

54
грамму, выводящую в файл результатов/ile.out следующие строки (в
них символ ^ обозначает местоположение пробела):
/•-/-254 " "-^"-^-^-"7 " " f " " ^^2547
(^^^^^1234,5600) " " (1234. 5 600''^^^^)
/Стр/^-^/м/

Ответы и решения для этих и последующих упражнений для


самопроверки можно проверить в разд. 18.
3. т и п ы Д А Н Н Ы Х и и х АТРИБУТЫ

Вспомним еще раз определение программы по Н. Вирту:

"Программа = структуры данных + алгоритм'

В соответствии с приведенным определением начнем рассмот­


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

3.1. Имена

В языках Си/С++ любая область памяти компьютера, которая


может быть использована программой, называется объектом. Любое
выражение, представляющее собой ссылку на объект, называется
адресным выраэюением или, короче, адресом^ именем.
Например, в рассмотренной выше программе объект "с/г" был
определен следующим образом:
i n t main ( sroldL )
{
±nt ch; // Объект с именем (адресом,
// адресным выражением) ch и типом
// ±nt
retuxm 0;

Имена объектов (например, ch) являются просто идентифика­


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

56
3.2. Типы данных

Тип данного указывает компилятору языка C++, сколько памя­


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

Табл. 12. Основные (стандартные) типы данных


Служебное слово Размер в байтах Назначение
char 1 Для символа (-128 ... +127)
wchart 2 Для символа из расширенного
набора (-32768 ... +32767)
int Зависит от реализации Для целого значения
bool 1 Для логического значения
(falsey true)
float 4 Для значения с плавающей точкой
(по абсолютной величине от
3.4Е-38до3.4Е+38)

Происхождение и перевод служебных слов:


char (CHARacter: буква, симвом);
wchar_t (wide character type: расширенный символьный тип);
int (INTeger: целое число);
float (число с плавающей точкой).

Существуют четыре спецификатора типа (табл. 13), уточняю­


щих внутреннее представление и диапазон значений стандартных
типов: unsigned (без знака), signed (со знаком), short (короткий),
long (длинный).
Символьный тип (char). Под величину символьного типа от­
водится один байт, что позволяет хранить в нем любой символ из
256-символьного набора ASCII. Величины типа char применяются
также для хранения целых чисел из диапазона +127 ... -128. По
умолчанию тип char эквивалентен типу signed char. При использо­
вании типа unsigned char значения могут находиться в диапазоне О
... 255. Величины типа unsigned char применяются также для хране­
ния целых чисел из диапазона О ... 255.
Расигиренный символьный тип (wchart). Этот тип предна­
значен для работы с набором символов, для кодировки которого не­
достаточно одного байта (например, для набора Unicode). Символь­
ные и строковые константы с типом wchart записываются с пре­
фиксом L, например:

57
iinclude <stdio.h>
int main ( void )
{
wpr±ntf( L"%s\n%c\n", L"string", L'A' ) ;
reburn 0;
}

В результате выполнения этой программы на экран выводятся


следующие строки:
string
А
Табл. 13. Уточняющие спецификаторы типа
Служебное слово Размер в байтах Назначение
unsigned char 1 Байт с неотрицательным целым
значением (0 ... 255)
unsigned или Зависит от реали­ Для неотрицательногЬ целого
unsigned int зации значения
short или 2 Целое значение
short int (от -32768 до +32767)
unsigned short или 2 Беззнаковое короткое целое
unsigned short int (0 ... 65535)
long или 4 Целое длинное
long int (-231...+ (2^1 - 1 ) ) 1
unsigned long или 4 Беззнаковое целое длинное
unsigned long int (0...(232-1))
double или 8 Вещ. с двойной точностью
long float ±1,7.10-'^..±1,7.10^'°'
long double 10 Вещ. с повыщенной точностью
±3,4•10-'^•'^..±3,4.10"'^''

Целые типы. Размер типа int зависит от реализации, (для ше­


стнадцатиразрядного процессора - 2, для тридцатидвухразрядного -
4 байта). Диапазон значений для шестнадцатиразрядного процессора
- от -32768 до +32767, для тридцатидвухразрядного процессора -
(-231...+ (231 _1))
Спецификатор short перед именем типа указывает компилято­
ру, что под число требуется отвести два байта, независимо от раз­
рядности процессора. Спецификатор long означает, что целая вели­
чина занимает четыре байта. Таким образом, на шестнадцатиразряд­
ном процессоре эквивалентны типы int и short int, а на тридцатид­
вухразрядном - int и long int.
Внутреннее представление величины целого типа — целое чис­
ло в двоичном коде. При использовании спецификатора signed

58
старший бит числа интерпретируется как знаковый (О - положи­
тельное число, 1 — отрицательное). Спецификатор unsigned позволя­
ет представлять только положительные числа, поскольку старший
разряд рассматривается как часть кода числа.
По умолчанию, все целочисленные типы считаются знаковы­
ми, то есть спецификатор signed можно опускать.
Логический тип (bool). Величины логического типа могут
принимать только значения false и true, которые являются служеб­
ными словами. Внутренняя форма представления значения false — О
(нуль). Любое другое значение интерпретируется как true. При пре­
образовании к целому типу true имеет значение 1.
Типы с плавающей точкой (floaty double и long double).
Внутреннее представление для типов с плавающей точкой состоит
из двух частей — мантиссы и порядка. При этом величины типа float
занимают четыре байта, из которых один двоичный разряд отводит­
ся под знак мантиссы, 8 разрядов под порядок и 23 под мантиссу.
Мантисса — число большее 1,0, но меньшее 2,0. Поскольку старшая
цифра мантиссы всегда равна 1, то она не хранится.
Для величин типа double, занимающих восемь байт, под поря­
док и мантиссу отводятся соответственно 11 и 52 разряда. Длина
мантиссы определяет точность числа, а длина порядка — диапазон
числа.
Спецификатор long перед именем типа double указывает, что
под число отводится 10 байт.
Чтобы проверить размер памяти, выделяемой для объекта дан­
ного типа, можно написать программу, использующую операцию
''sizeof {size - размер). Значением этой операции является размер
любого объекта или спецификации типа, выраженный в восьмиби­
товых байтах:

/ / См++. Программа печатает размеры объектов основных и


// производных типов

^include <stdio.h> // Для функций ввода-вывода


int main ( void ) // Возвращает О при успехе
{
printf ( "Тип объекта Его размер в байтах \п\п" ) ;
prlntfC "char %d \п", slzeof( char ) ) ;
printf ( "unsigned char %d \ л " , sizeof ( unslgnecL cbar ) ) .
printf ( "int %d \ л " , sizeof ( int ) ) ;
printf( "unsigned %d \ л " , sizeof( unsigned ) ) /
printf( "short %d \n", sizeof( short ) ) ;
printf( "unsigned short %d \ n " ,
sizeof( unsigned short ) ) ;
printf( "long %d \ n " , sizeof( long ) ) /
printf( "unsigned long %d \ л " .

59
sizeof ( unsigned long- ) ) ;
printf( "float %d \n", sizeof ( float ) ) ;
printf( "double %d \n", sizeof ( double ) ) ;
print f ( "long double %d \n", sizeof ( long double ) ),
•retujETZi 0;
}

Tun void. Кроме перечисленных, к основным типам языка от­


носится тип void, но множество значений этого типа пусто. Он ис­
пользуется для определения функций, которые не возвращают зна­
чения, для указания пустого списка аргументов функции, как базо­
вый тип указателей и в операции приведения типов. Все это будет
рассмотрено далее.
В заключение приведем с использованием синтаксических диа­
грамм правила определения объектов в программах на языке Си/С++
(рис. 21). Следует заметить, что наряду с приведенными на этом
рисунке разновидностями, есть и другие разновидности специ­
фикации типа и определяемого объекта, которые мы рассмотрим
позже.
В дополнение к имени и типу объекта существуют еще два ат­
рибута:
• область действия;
• время жизни объекта.
Эти два атрибута определяются классом хранения, который
связывается с конкретным объектом.

3.3. Класс хранения: область действия


и время жизни

Областью действия объекта (данного) называется та часть


программы, в которой можно пользоваться этим объектом. В част­
ности, областью действия может быть:
• блок операторов ( { . . . } );
• модуль (файл);
• вся программа в целом.
Временем жизни данного называется отрезок времени, в тече­
ние которого значение этого данного доступно в некоторой части
программы. Время жизни данного может быть столь коротким, как
время исполнения операторов блока, или столь же длинным, как
время выполнения всей программы.
В языках Си/С++ область действия и время жизни объекта оп­
ределяются его классом хранения, в качестве которого можно ис­
пользовать следующие классы:

60
• внешний;
• внешний статический;
• внутренний статический;
• автоматический;
• регистровый.

Список_опред._объектов

*н Спецификация_типа — г - н Определяемый_объект

Специф._типа

unsigned
>
•( char У

и short У

и long У
•Г float \

•Г long V^ double
>

Определяемый_объект

Идентификатор

Рис. 21. Правила определения объектов

3.4. Внешние и внешние статические данные

Дадим более полное определение модуля на рис. 22. Отсюда


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

61
Рассмотрим далее несложный иллюстрирующий пример.

Модуль (файл с объявлениями и определениями данных


и операторами)
Объявления и определения внешних данных
Функция
Внутренние определения данных
Операторы

Функция
Внутренние определения данных
Операторы

Рис. 22. Обобш[ение определения модуля

Файл Р2.СРР
Программа с одним модулем и двумя функциями. Программа яв­
ляется примером и конкретизацией информации, представленной
на рис. 19

#include <stdio.h> // Для функций ввода-вывода


// Прототип функции
void, save ( void. ) ;

// Определение внешних данных: область действия - программа,


// время жизни - программа
int i l , ±2;

// Выполнение программы начинается с выполнения' следующей


// ниже главной функции
int main ( void ) // Возвращает О при успехе
{
save ( ) / // Вызов функции: определяет И, i2
printf( "\п 11 = %d 12 = %d"r 11, 12 ) ;
re turn 0;
}

// Определение, функции, задающей значения 11, 12


void save( void )
{
11 = 10; 12 = 15;
return;

В этом примере определены внешние данные /1 и /2. Область


действия внешних данных (в примере /1 и /2) распространяется на

62
весь модуль, а время их эюизни совпадает со временем выполнения
программы. Любая функция, находящаяся в этом файле (модуле)
может иметь доступ к внешним данным. Таким образом, внешние
данные файла являются общими данными для всех функций того же
файла.
В общем случае область действия внешних данных можно рас­
пространять и за пределы файла (модуля), используя служебное сло­
во extern {EXTERNal - внешний).
В качестве иллюстрации перепишем программу для предыду­
щего примера с использованием двух файлов.

Файл РЗ.СРР
Двухфаиловый программный проект с двумя функциями. Пример
иллюстрирует область действия и время жизни данных, имеющих
внешний класс хранения.
V

^include <stdio.h> // Для функций ввода-вывода


// Прототип функции: хотя определение этой функции находится
// в другом файле данного программного проекта (SAVE.CPP),
// в данном файле прототип также нужен - он используется
// для контроля правильности вызова функции
void save ( void. ) /

// Объявление внешних данных: дополнительной памяти


// объявление не занимает, а лишь говорит о том, что
// соответствующие данные, определены в другом файле
extern Int 11, 12;

int main ( void ) // Возвращает О при успехе


{
save( ) ; // Вызов функции: определяет 11, 12
prlntf( "\n 11 %d 12 = %d", 11, 12 ) ;

return 0;
}

Файл SAVE.CPP
Используется в программном проекте, главная функция кото-
\рого имеется в файле РЗ.СРР.
V

// Прототип функции: в принципе, в этом файле прототип не


// нужен, так как файл содержит определение этой функции.
// Мы оставляем здесь прототип только для унификации
void save ( void ) ;
// Определение внешних данных: занимает память, располагая в
// ней соответствуюище данные

63
±nt ilr ±2;
-void save ( void. )

il 10; ±2 15;

jretuim/
;
в приведенном примере внешние данные /1 и /2 определены в
файле SAVE.CPP. Такое определение должно присутствовать толь­
ко в одном файле многофайлового программного проекта. Чтобы
воспользоваться этими данными вне файла, где они определены (на­
пример, в файле РЗ.СРР), их следует объявить с использованием
служебного слова extern. Таким образом, определение создает дан­
ное (см. файл SAVE.CPP), а объявление (см. файл РЗ.СРР)- только
ссылается на данное, определенное в другом файле (рис. 23). Обра­
тите внимание, что объявление внешних данных, в отличие от их
определения, может присутствовать в нескольких файлах программ­
ного проекта.

ОПРЕДЕЛЕНИЕ
Определить данное типа int
, Имя нового данного

int 11;
ОБЪЯВЛЕНИЕ
Указывает, что данное определено в
другом месте (в другом файле)
Указывает тип данного
Имя существующего данного

extern int i1;


Рис. 23. Определение и объявление внешних данных

В определении данного перед спецификацией его типа можно


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

64
^include <stdlo,h> // Для функций ввода-вывода
// Прототип функции: хотя определение этой функции находится
// в другом файле данного программного проекта (SAVE1.СРР) ,
// в данном файле прототип также нужен - он используется
// для контроля правильности вызова функции
void savel ( void. ) ;

// Объявление внешних данных: дополнительной памяти


// объявление не занимает, а лишь говорит о том, что
// соответствующее данные определены в другом файле
// Gxtejcn int 11; Такое объявление ошибочно!
extejcn int 12;

int main ( void ) // Возвращает 0 при успехе


{
// Вызов функции: определяет
savel( ) ; // значение 12
%d", 12 ) ;
prlntf( "\n 12

return 0;

Файл SAVE1.CPP
Используется в программном проекте, главная функция кото­
рого имеется в файле Р4.СРР
V
// Прототип функции: в принципе, в этом файле прототип не
// нужен, так как файл содержит определение этой функции.
// Мы оставляем здесь прототип только для унификации
void savel( void ) ;

// Определение внешнего и внешнего статического данных:


// занимает память, располагая в ней соответствуюш^^е данные
static int 11; // Доступно только в этом файле
int 12; // Доступно в этом файле и файле
// Р4,СРР
void savel( void )
{
11 = 10; 12 = 15;

return;
}

Обратите внимание, что в этом примере в файле Р4.СРР объ­


явление
extern int 11.

65
было бы ошибочным, так как областью действия /1 является только
файл SAVE1.CPP.
Подводя итоги сказанному, выполним некоторые обобщения.
В общем случае, программный проект является многофайловым. В
рассуждениях, относящихся к объектам с описателем класса хране­
ния "внешний", будем считать, что программный проект содержит
три файла (рис. 24 а).

н
Гипотетический программный проект
fl.cpp f2.cpp fS.cpp
1 mill ^ ^ ^ ^ ^ ^ И
1 ovforn И- ^ ^ ^ H
1 extern int i1; ^ ^ H

float i 1 ;

a)
Гипотетический программный проект
fl.cpp f2.cpp fS.cpp

H
•"staticl^^
1 ctatin i9- ^ ^ ^ ^ H
1 static char ch; ^ H

{
char i2;

6)
Рис. 24. Внешние объекты:
a) с описателем класса хранения внешний;
б) с описателем класса хранения внешний статический

На этом рисунке область действия объекта /1 с описателем


класса хранения extern содержит весь файл fl.cpp, часть файла
f2.cpp и часть файла f3.cpp (выделена заливкой). Отметим, что на­
чальная часть файла f2.cpp не входит в область действия объекта /1
потому, что в этом файле объявление объекта /1 помещено в сере­
дину файла. Аналогично, в файле f3.cpp вложенный блок не входит

66
в область действия, так как в нем переопределяется объект с иден­
тификатором / 1 . Область действия объекта / 1 с описателем класса
хранения extern можно сделать максимальной — все файлы про­
граммного проекта. Для этого достаточно объявление объекта /1 в
файлах fZ.cpp и О.срр поместить в их начало, а вложенные блоки не
должны содержать переопределение объекта / 1 . Еще раз напомним,
что определение объекта с описателем класса хранения extern
долэюно быть только в одном файле программного проекта (лю­
бом), а в остальных файлах долэюно использоваться только объяв­
ление объекта.
Рис. 24 б иллюстрирует области действия объектов с описате­
лем класса хранения "внешний статический". Так, областью дейст­
вия объекта/является весь файл П.срр (и только он), областью дей­
ствия объекта ch является залитая часть файла f2.cpp, а областью
действия объекта /2 является залитая часть файла О.срр.
Теперь можно сформулировать ряд важных уточнений, отно­
сящихся к областям действия и времени жизни объектов с описате­
лями класса хранения "внешний" и "внешний статический":
\. Временем эюизни внешних данных является интервал време­
ни, в течение которого программа выполняется. Это верно как для
внешних, так и для внешних статических данных. Следовательно,
если внешней переменной будет присвоено значение, то оно будет
сохраняться в течение всего времени выполнения программы и не
будет утрачено между вызовами функций.
2. Областью действия внешнего данного в общем случае явля­
ется вся программа за исключением влоэюенных блоков, в которых
содерэюатся переопределения данного с тем эюе именем. Вложен­
ным блоком называется конструкция вида { ... }, в которой между
фигурными скобками могут находиться определения данных и опе­
раторы.
3. Областью действия внешнего статического данного явля­
ется файл, где это данное определено, за исключением влоэюенных в
этот файл блоков, в которых содерэюатся переопределения данного
с тем эюе именем.
Обратите особое внимание на два последних уточнения' отно­
сительно областей действия внешних и внешних статических дан­
ных - это очень важно!
Остальные классы хранения данных - автоматический, внут­
ренний статический и регистровый - гораздо уже по области дейст­
вия и, за исключением внутренних статических данных, по времени
жизни. Данные этих классов привязаны к отдельным функциям или
блокам. Поэтому перед рассмотрением областей действия и времени

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

3.5. Функции

Выше типы данных и области действия рассматривались при­


менительно только к объектам данных. В языках Си/С++ эти атри­
буты могут быть связаны и с функциями. Рассмотрим определения и
объявления функций.
Общий вид определения функции представлен на рис. 25.

Класс: внешний (extern, по умолчанию) или


статический (static).
Будем пользоваться умолчанием - опускать extern,

Тип возвращаемого значения (int - по умолчанию,


void - отсутствует)
Имя функции
static
extern
float power( int number, float exponent)
{

} Имена параметров

Типы параметров

Рис. 25. Определение функции

Прежде всего, следует заметить, что функцию можно сделать


статической, указав перед ее именем и типом слово static. В языках
Си/С++, по умолчанию, все функции трактуются как внешние, если
только перед типом функции не указано служебное слово static. Это
означает, что областью действия внешней функции является вся
программа. Определение функции как статической сужает область
ее действия на оставшуюся часть файла, в котором она определена.
Из рис. 25 следует также, что функции можно приписать тип.
Тем самым будет определен тип данного, возвращаемого функцией
в качестве результата. Если тип возвращаемого функцией результата
отличается от int, то об этом следует сообщать компилятору, как в
месте ее определения, так и в любом месте ее внешнего объявления
в других файлах. Показанный на рис. 25 пример иллюстрирует так­
же способ присваивания имен параметрам функции и способ опре­
деления их типов.

68
Объявление функции в языках Си/С++ называется прототи­
пом функции. Вид его аналогичен заголовку определения функции,
за которым вместо блока функции { ... } следует символ ";". Другое
отличие списка параметров в прототипе заключается в том, что либо
разрешается указание всех имен параметров, как в заголовке опре­
деления функции, либо все имена параметров можно опустить.
Давайте теперь рассмотрим иллюстрирующий пример — запи­
шем прототип, определение и пример вызова функции, определяю­
щей наибольшее и наименьшее значение из двух аргументов. При
проектировании функции обычно вначале составляют специфика­
цию функции, которую можно рассматривать как графическую фор­
му записи прототипа функции. Спецификация (прототип) функции
определяет ее интерфейсные свойства. Это означает, что на данном
этапе функция рассматривается как "черный ящик" и определяются
только ее интерфейсные свойства — входные и выходные данные.
Спецификация функции для рассматриваемого примера представле­
на на рис. 26.

double Arg1 double &Max

double Arg1 MaxMin double &Min

Исходные данные Процесс Результаты


(передаются по (передаются по ссылке)
значению)
Рис. 26. Спецификация функции

Теперь запишем исходный текст в виде законченной програм­


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

Файл MAXMIN. СРР


Однофайловый программный проект с двумя функциями. Пример
иллюстрирует работу с функцией: объявление (прототип) функ­
ции^ определение функции, вызов функции без возвращаемого
значения и оба варианта передачи параметров функции - по зна- \
чению и по ссылке
V

^include <stdlo.h> // Для функций ввода-вывода


// Прототип функции: Argl, Агд2, Мах, М1п - параметры функции
// В данном случае прототип функции является обязательным,
// так как вызов функции выполняется раньше, чем функция
// определена
void MaxMin ( double Argl, double Arg2, double &Max,

69
double &Min ) ;
±nt main ( void ) // Возвращает 0 при успехе
(
double al = 1.5, a2 - -17.1, Mx, Mn;
// Вызов функции без возвращаемого значения: al, а2, Мх,
// Мп - аргументы функции
MaxMln ( al, а2, Мх, Мп ) ;
prlntf( "\п al = %1д, а2 = %1д, Мх = %1д, Мп = %1д \п",
al, а2, Мх, Мп ) ;

jretujrn О;
}

// Определение функции, вычисляющей Мах:=наиб. (Argl,Агд2) и


// М1п:^наим. (Argl,Агд2). Функция не имеет возвращаемого
// значения
void MaxMin (
double Argl, // Исходное данное ~ передается по
// значению
double Агд2, // Исходное данное - передается по
// значению
double &Мах, // Ответ - передается по ссылке
double ScMin ) // Ответ - передается по ссылке
{
±f( Argl>Arg2 )
{
Max = Argl; Mln = Arg2;
}
else
{
Max = Arg2; Min = Argl;
}

return;
}

Еще раз напомним, зачем нуэюен прототип (объявление) функ­


ции. Прототип функции используется для контроля правильности
вызова функции. В рассмотренном выше примере прототип функции
MaxMin применяется компилятором при вызове этой функции Здесь
компилятор сравнивает:
• возвращаемое значение функции в прототипе (void - отсутствует)
со способом вызова функции (вызов должен начинаться с имени
функции);
• сравнивает количество параметров в прототипе и их типы с коли­
чеством аргументов в вызове функции и типами аргументов.

70
в нашем примере имеет место их полное соответствие, что
свидетельствует об отсутствии ошибок в вызове функции. Попутно
заметим, что в языке Си прототип функции не обязателен. При его
отсутствии компилятор выдает лишь предупреждение о невозмож­
ности проверить правильность вызова такой функции, что не пре­
пятствует выполнению программы. Это является недостатком языка
Си. В языке же С-ь+, напротив, прототип является обязательным и
это хорошо.
Если в файле программного проекта, где находится вызов
функции, имеется определение этой функции, причем определение
функции предшествует ее вызову, то наличие прототипа не является
обязательным. При его отсутствии для контроля правильности вызо­
ва функции компилятор использует заголовок функции из определе­
ния функции.
Рассмотрим процесс передачи аргументов а\ и а2 функции
MaxMin в приведенной выше программе. Что же функция MaxMin
получает в действительности - копии значений аргументов а\ и а2
или значения этих аргументов? В данном случае функция получает
копии значений аргументов al и а2. Передача функции копий зна­
чений аргументов, в противоположность передаче функции значе­
ний самих аргументов, называется передачей аргументов по значе­
нию. При таком способе передачи значения аргументов a l и а2 ко­
пируются, на время работы функции, в дополнительную область па­
мяти и используется в функции в качестве параметров ArgX и Arg2.
По завершении работы функции указанная область памяти освобож­
дается и может быть повторно использована. При этом сами аргу­
менты al и а2 остаются неизменными (даже, если в теле функции
значения параметров будут изменены). Такой способ передачи ар­
гумента в функцию, по существу, означает "упрятывание" информа­
ции в функции. Следовательно, он хорош для передачи в функцию
исходных данных, которые после завершения функции должны со­
хранить прежние значения.
В языке C++, в отличие от языка Си, существует и другой спо­
соб передачи аргумента в функцию - передача аргумента по ссылке.
В нашем примере такими аргументами являются Мх и Мп. При пе­
редаче аргументов Мх и Мп по ссылке в качестве параметров Мах и
Min используется сами аргументы Мх и Мп. По завершении работы
функции аргументы Мх и Мп останутся такими, какими они были
перед завершением функции (в нашем случае Мх получает наиболь­
шее, а Мп — наименьшее значение из a l и а2). Такой способ переда­
чи аргумента в функцию хорош для получения из функции ответа.

71
в рассмотренном нами примере функция не имела возвращае­
мого значения. Но ведь существуют и функции, имеющие возвра­
щаемое значение. Когда же их следует применять? Ответ на этот во­
прос прост — если из функции получаем единственный ответ. В этом
случае удобнее его получать как значение, возвращаемое функцией.
Рассмотрим пример, иллюстрирующий такой способ получения от­
вета. В качестве решаемой задачи рассмотрим более простую зада­
чу, являющуюся частью только что рассмотренной задачи - запишем
прототип, определение и пример вызова функции, определяющей
наибольшее значение из двух аргументов. Спецификация соответст­
вующей функции приведена на рис. 27.

double Arg1
double
double Arg1
Max

Исходные данные Процесс


Наибольшее из Arg1 и
(передаются по Arg2 получаем как
значению) возвращаемое
значение
Рис. 27. Спецификация функции с возвращаемым значением

Теперь запишем исходный текст в виде законченной програм­


мы, содержащий записи прототипа, вызова функции и ее определе­
ния.
/*
Файл МАХ.СВР
Однофайловый программный проект с двумя функциями. Пример
иллюстрирует работу с функцией: объявление (прототип) функ-
ции, определение функции и вызов функции с возвращаемым зна-
чением

^include <stdio.h> // Для функций ввода-вывода


// Прототип функции: Argl^ Агд2 - параметры функции, функция
// имеет возвращаемое значение. В данном случае прототип
// функции является обязательным, так как вызов функции
// выполняется раньше, чем функция определена
dovLble Мах ( double Argl, double Arg2 ) ;

±пЬ main ( void ) // Возвращает 0 при успехе


{
double al 1.5, a2 = -17.1, Мх;
// Вызов функции с возвращаемым значением: al, а2 -
// аргументы функции

72
Мх = Max( al, a2 ) ;
printf ( " \ л al = %lg, a2 = %lg, Mx = %lg \n",
air a2, Mx ) ;
z-etuni 0;
}

// Определение функции
double Max ( // Возвращает наиб. (Argl ,Агд2)
double Arglr // Исходное данное - передается по
// значению
double Агд2 ) // Исходное данное - передается по
// значению
(
±£( Argl>Arg2 )
{
retuim Argl;
}

re bum A r g2 ;
}

В рассмотренном примере функция Max имеет возвращаемое


значение. Поэтому вызов этой функции должен быть записан в фор­
ме выражения присваивания. В этом выражении слева от знака опе­
рации '=' должно указываться имя переменной с типом как тип зна­
чения, возвращаемого функцией, а справа от знака '=' должно следо­
вать имя функции. В нашем примере в вызове функции Мах имеет
место точное соответствие прототипу этой функции и в части воз­
вращаемого значения, что также говорит об отсутствии ошибок в
использовании функции. В заключение отметим, что класс хранения
таких объектов, как параметры функций, передаваемые по значе­
нию, называется автоматическим (другие названия - локальный,
рабочий). Такое название означает, что область действия парамет­
ра, передаваемого по значению, ограничивается текущей функцией,
точнее блоком функции за исключением тех вложенных в блок
функции блоков, в которых содержится переопределение имени па­
раметра. Область действия параметра, передаваемого в функцию по
ссылке, определяется соответствующим аргументом в вызове функ­
ции.
Временем эюизни параметра, передаваемого в функцию по
значению, является продолжительность исполнения функции. Как
только функция завершит работу, значения ее параметров, передан­
ных по значению, будут утеряны. Время же жизни параметра, пере­
даваемого в функцию по ссылке, также определяется соответствую­
щим аргументом в вызове функции. Автоматический класс хранения
могут иметь не только параметры функций, но и другие объекты.

73
3.6. Автоматические, регистровые
и внутренние статические данные

Автоматические, регистровые и внутренние статические дан­


ные можно определить внутри любого блока операторов языков
Си/С++. Общий синтаксис блока представлен на рис. 28.

{ Начало блока

Внутренние определения данных

Операторы

Конец блока

Рис. 28. Общий синтаксис блока

Отметим разницу в синтаксисе блока языков Си/С++. В языке


Си внутренние определения данных блока должны обязательно
предшествовать операторам блока, а в языке C++ внутренние опре­
деления данных и операторы блока могут быть перемешаны. Но при
этом необходимо, чтобы использованию внутреннего данного в опе­
раторе блока обязательно предшествовало его определение.
В качестве примеров блоков, известных нам на данном этапе,
можно назвать блоки в операторе //, блоки в циклических операто­
рах, блоки функций и обычные блоки. Существуют и другие разно­
видности блоков, которые будут рассмотрены далее.
Данные можно определить внутри блока как имеющие либо
автоматический auto {AUTOmatic), либо статический static, либо ре­
гистровый register классы хранения (рис. 29). По умолчанию, когда
описатель класса хранения опущен, предполагается автоматиче­
ский класс хранения!!!

74
Определение_внутренних_данных
^ Специф._типа Идентификатор

auto

static

register
>
Рис. 29. Определение внутренних данных

Областью действия внутренних данных с классами хранения


автоматический, регистровый и внутренний статический является
блок, где они определены, включая те вложенные в него блоки, в
которых не содержатся переопределения тех же самых имен.
Время эюизни внутренних данных с классом хранения auto и
register совпадает со временем выполнения блока. Следовательно,
они создаются (размещаются в памяти) в момент входа в блок и
уничтожаются при выходе их него. При этом внутренние данные с
классом хранения register (они могут иметь только целый тип int)
хранятся в быстродействующих машинных регистрах, если это воз­
можно, или они эквивалентны данным с классом хранения auto в
противном случае.
Время снсизни внутренних статических данных с классом хра­
нения static совпадает со временем выполнения всей программы.
Они создаются однократно и сохраняют значения между повторны-
ми входами в блок, в котором они определены.

Рассмотрим ряд иллюстрирующих примеров.

Файл Р7.СРР
Двухфайловый программный проект (файлы Р7.СРР и SAVE4.СРР)
с тремя функциями. Пример иллюстрирует время жизни и область
действия параметров функции и внутренних автоматических дан­
ных
V
^include <stdio.h> // Для функций ввода-вывода
// Прототипы функций
voxd save4 ( float ) ;
float get ( void ) ;

int main ( void. ) // Возвращает О при успехе


{

75
/ / Определение внутренних автоматических данных. Можно и
// в такой эквивалентной форме: float mv^ pi;
auto float /nv, pi;

pi = 22. Of / 7; sa\re4 ( pi ) ; mv = get ( ) ;


print f ( "\n mv ^ %f pi = %f"^ mv, pi ) ;

retvLm 0;
}

/*
Файл SAVE4.CPP
Используется в программном проекте, главная функция кото­
рого имеется в файле Р7.СРР

// Прототипы функций
void save4( float ) ;
float get( void ) ;
static float fv;

void save4( float mv )


{
fv = mv;
return;
}

float get( void )


(•

return fv;
}

Внутренняя автоматическая переменная wv, определенная в


функции main, и параметр mv в функции save4 размещены в разных
областях памяти и не влияют друг на друга. Первая из них имеет об­
ластью действия блок функции main и время жизни, равное времени
выполнения main. Параметр mv функции save4 имеет в качестве об­
ласти действия блок этой функции и время жизни - время выполне­
ния save4.
Автоматические, регистровые внутренние данные и внутрен­
ние статические данные можно переопределять при вложении бло­
ков друг в друга, что иллюстрирует следующий пример.

/*
Файл Р8.СРР
Однофайловый программный проект с одной главной функцией.
Пример иллюстрирует переопределение данных во вложенных бло­
ках
*/

76
^include <stdio.h> // Для функций ввода-вывода
int main ( void. ) // Возвращает О при успехе
{
// Определение внутренних автоматических переменных
int counter^ // Действует в блоке main и во
// вложенном блоке while
i; // Действует в блоке main и не
// действует во вложенном блоке
// while
counter = О; 1 = 10;
while ( counter < i )
{
// Переопределение внутренней автоматической
// переменной во вложенном блоке - она
// располагается в другой области памяти
// и действует в блоке while
int i;
i ^ 0; counter++;
printf( "\n counter ^ %d i = %d", counter, i ) /
}
print f ( "\n\n counter = %ci i = %d \л", counter, i ) ;
retuizn 0;
}

Результаты выполнения этой программы имеют следующий


вид:

counter = 1
i =О
counter ^ 2
i =О
counter = 3
i =О
counter = 4
i == О
counter = 5
i =О
counter = 6
i = 0
counter = 7
i ^ 0
counter = 8
i = 0
counter =91=0
counter = 10 i = 0
counter^ = 10 i = 10

Как указывалось выше, внутренние статические данные имеют


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

/ / Определение функции
void, save ( float mv )
{
// Определение внутреннего статического данного с его
// инициализацией при трансляции
static int counter == 0;

11
counter++,

return/
}

Значение counter будет сохраняться между вызовами функции


save и, следовательно^ по нему можно судить сколько раз вызыва­
лась эта функция.

3.7. Инициализация данных

в языках Си/С-1-+ большинство данных может быть явно или


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

Табл. 14. Области действия, время жизни


и инициализация объектов
1 Класс Внешний Параметр Автома­ Регист­ Внутренний
хранения Внешний стати­ функции тический ровый статичес­
ческий кий
Область Програм­ Файл Функция Блок Блок Блок
действия ма
Время Програм­ Програм­ функция Блок Блок Программа
жизни ма ма
Инициали- Все Все Нет Все Все Все
зируе-
мость
объектов
[ Момент При ком­ При ком­ Нет При каж­ При каж­ При компи­
инициали­ пиляции пиляции дом входе дом вхо­ ляции
зации в блок де в блок
Инициали­ Нулем Нулем Нет Не Не Нулем
зация по определе­ опреде­
умолча­ но лено
нию

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


extern и static присваиваются нулевые начальные значения. Пере­
численные данные и большинство других данных могут быть явно
инициализированы в момент определения с помощью указания по­
сле их имени знака '=' и константного выраэюения:
static ±nt counter = 0;
// Константное выражение не содержит переменных
long max_size = 512 * 200L;

78
Данные с классами хранения extern и static инициализируются
однократно в момент компиляции. Автоматические и регистровые
данные инициализируются в процессе выполнения программы при
каждом входе в блок, в котором они определены.

3.8. Упражнения для самопроверки


1. Что напечатает следующая программа?

^include <stdlo.h>
// Прототипы функций
int next ( void ) ;
tub reset ( void ) ;
int last ( void ) ;
int nw ( ±nb ) ;

int i = 1;

int main ( void )


{
auto Int 1, j ;
1 = reset( ) ;
fox:( j = 3; j <= 3; j++ )
{
prlntf( "1 = %1 j = %l\n", i, J ) ;
print f ( "next ( )=%l\n"^ next ( ) ) ;
prlntf( "last( )=%l\n"r last( ) ) ;
print f( "nw (1+j ) =%l\n", nw(l+j) ) ;
}

jretujrn 0;

static Int 1=10;

int next ( void )


{
T&txuon 1 += 1;
}

int last ( void )


{
return 1 -= 1;
}

int nw ( Int 1 )

79
{

static int j = 5;

rGtuJcn 1 =j += 1;

/ k k k k k k k k k k k k - k k k k k k - k k k k k фз^р[Л 3 k k k k k k k k k k k k k k k k k k k k k k k k k k k k /

extern Int i/

±nt reset ( void. )


{
jretuxn i /

;
2. Что напечатает следующая программа?
^include <std±o.h>
// Прото типы функций
int next ( int );
int reset ( void. ) ;
int last ( int ) ;

int i = 2;

int main ( void )


{
auto int i, j ;

i = reset ( );
£or( j = 1; j <= 2; j++ )
J
printf( "\ni = %d j = %d\n"r i, j );
print f( "next ( i ) = %d\n", next ( i ) );
printf( "last( i ) = %d\n'\ last ( i ) );
}

return 0;
}
/ / k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k - k k k k k k k k k k k k k - k k k k k k k k k

int reset( void )


{
return ++i;
}
/ / k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k
int next ( int j )
{
return j = i++;
}
/ / k k - k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k k

int last ( int j )


{
static int i = 10;

80
return j = i--/
i
Как уже указывалось, ответы для этих упражнений можно про­
верить в разд. 18.

3.9 Производные типы данных

в языках Си/С++ предусмотрены несколько производных ти­


пов данных, среди которых основными являются:
• массивы;
• структуры;
• объединения.

3.9.1. Массивы

Структура данных, называемая массивом, позволяет опреде­


лить непрерывный (по отношению к расположению в памяти) набор
однотипных объектов данных.
Приведем пример определения массива:

Этот массив символьного типа способен хранить 15 символов.


Начальные значения элементов не определены: определение мас­
сива дано без инициализации
V
char kaf__name [ 15 ];

Индивидуальный доступ к отдельным элементам массива осу­


ществляется с помощью индексированных имен:
kaf_name[ О ] ... kaf_name [ 14 ]

Обратите внимание, что индексы элементов массива изменя­


ются в диапазоне О ... 14, а не в диапазоне 1 ... 15.
Массивы могут иметь любой класс хранения, кроме register.
Области действия и времена жизни массивов такие же, как и у про­
стых данных. Массивы моэюно инициализировать (рис. 30):
chstr kaf_name[ 15 ] = { 'К', 'а', 'ф\ 'е\ 'д'г
'Р\ ' а ' , ' Ч 'АЧ ^B^, 'Г', ^\0^ } /

По принятому соглашению массивы символов, содержащие


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

81
По этой причине массив kafjname может хранить в виде стро­
ки название кафедры, состоящее не более чем из 14 символов!

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14
kaf_name | к а Ф е Д Р а А В Т \0 ? ? ?
Рис. 30. Инициализация массива

Символьный массив можно инициализировать и более удоб­


ным способом:
char kaf_name[ 15 ] = "Кафедра АВТ";

Это определение символьного массива с инициализацией пол­


ностью эквивалентно предыдущему. Вместе с тем, приведенное ни­
же определение символьного массива с инициализацией дает не­
сколько иной результат:
cJiax- kaf_name [ ] = "Кафедра АВТ"/

В последнем случае в памяти резервируется 12 байтов - ровно


столько, сколько требуется для хранения инициализирующей стро­
ки.
Можно аналогичным образом определять массивы с любым
типом элементов с инициализацией или без нее:
/ / Определение массива из 9 элементов с типом double без
// инициаЛИЗации
double а[ 9 ];
// Определение массива из 4 элементов с типом double с
// инициализацией
double b[ 4 ] = { 1.0, 2.0, -3.1, 4.5 } ;

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


зуется массив значений с плавающей точкой. Предварительно опре­
делим понятие стек, широко применяемое в программировании и
вычислительной технике.
Стек - непрерывная область оперативной памяти, в которой
хранятся объекты заданного типа, и работа с которой организована
по правилу "последним записан - первым прочитан". По этой при­
чине стек часто называют очередью типа LIFO (Last Input First
Output).

Файл P9.CPP
Двухфайловый программный проект (файлы Р9.СРР и STACK.СРР)
с тремя функциями: главной функцией и функциями занесения в
стек и извлечения из стека. Стек организован на базе внешнего

82
статического массива, состоящего из элементов с типом float.
Стек доступен в функциях не за счет передачи через список па­
раметров^ а за счет своей области действия и времени жизни.

^include <stdio.h> // Для функций ввода-вывода


// Прототипы функций
void push ( float ) ;
void, pop ( float &r int & )/

int main ( void ) // Возвращает 0 при успехе


{
int flag; // 0 - извлечение из стека не
// выполнено г иначе - выполнено
float out_value;// Значение, полученное из стека
push ( 2.4f ) ; // Занести в стек 2.4f
push ( -17. 4f ) ; // Занести в стек -17.4f
for( int 1 = 0; i < 3; ±++ )
{
// Извлечение из стека
pop ( out_value, flag ) ;
// Анализ полученного результата
if( flag )
{
printf( "\n Результат извлечения: %f ",
out_value ) ;
}
else
{
printf( "\n Стек пуст" ) ;
}
}

return 0;
}

Файл STACK.CPP
Содержит функции для занесения элемента и извлечения эле­
мента из стека. Используется в программном проекте, главная
функция которого имеется в файле Р9.СРР
V
^include <stdio.h> // Для функций ввода-вывода
// Прототипы функций (в принципе - прототипы здесь не нужны,
// мы оставляем их в этом файле только для унификации
void push ( float );
void pop ( float &, int & ) ;

^define N10 // Размер стека


// Стек: массив из 10 элементов - доступен только в этом

83
// файле, время жизни - программа
static float s[ N ] ;
// Указатель вершины стека: вначале стек пуст
static unsigned, int
top;
// Занесение в стек
void push(
float V ) // Заносимое значение
{
if( top < N )
{
s[ top++ ] = V/
}
else
{
print f ( "\n Стек полон - занесение не выполнено" ) ;
}

return;
}

// Извлечение из стека
void pop (
// Извлеченное значение - передается по ссылке
float &out,
// О - извлечение не выполнено (стек пуст)
int &f )
{
if( top > О )
{
out = s[ --top ]; f = 1;
}
else
(
f = 0;
}

return;

Результаты выполнения данной программы имеют вид:

Результат извлечения: -17.400000


Результат извлечения: 2.400000
Стек пуст

Рассмотренные выше примеры использовали так называемые


одномерные массивы, обращение к элементам которых выполняется
с помощью одного индекса (индексного выражения). Указанное ин­
дексное выражение (например, /+/) должно иметь целый тип без

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

Файл Р10.СРР
Однофайловый программный проект с одной главной функцией.
Пример иллюстрирует работу с двумерными массивами из элемен­
тов символьного типа
V

^include <stdlo.h> // Для функций ввода-вывода


^define N 8 // Строковый размер массива
^define М 16 // Столбцовый размер массива
// Определение двухмерного символьного массива с
// инициализацией: N строк, каждая строка из М символов
char arr__kaf_name [ N ] [ М] =
{
"Состав ФТК:", / / Инициализация первой строки
"1. Кафедра АВТ",
"2. Кафедра ТК",
"3. Кафедра САУ",
"4. Кафедра МУС",
" 3 . Кафедра ИИТ",
"6. Кафедра САПР",
" 7 . Кафедра СУД"
};
int main ( void ) // Возвращает О при успехе
{
a u t o ±nt Index; // Индекс строки двухмерного массива

fori Index = 0; Index < N; lndex++ )


{
print f ( "\n %s "r arr_kaf_name [ Index ] ) .
}

return 0;

Результаты выполнения данной программы имеют вид:


Состав ФТК:
1. Кафедра АВТ
2. Ка федра ТК
3. Кафедра САУ
4. Кафедра ИУС
5. Кафедра ИИТ

85
6, Кафедра САПР
7. Кафедра СУД

Используемый в программе двухмерный массив arrkafname


хранит информацию, представленную в табл. 15.

Табл. 1Ь. Структура массива


Строки/ 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
столбцы
0 С о с т а в Ф Г К \0 ? 9 ? ? ?
1 1 К а Ф е Д р а А В т \0 7
2 2 К а Ф е д р а Т к \0 ? ?

6 6 К а Ф е д р а С А п р \0
7 7 К а Ф е д р а с У Д \0 ?

В этой таблице элемент массива аггjkaf_name[ 6][ 7 ] хранит


код символа 'д'. Элементы этого массива в оперативной памяти ком­
пьютера располагаются в подряд идущих байтах по строкам:
arr_kaf_name [ О ][ О ] arr_kaf__name[ О ][
arr_kaf_name [ О ] [ 15 J
arr_kaf_name [ 1 ][ О ] агг kaf namef 1 ][
агг kaf_name[ 1 ][ 15 ]

arr_kaf__name [ 7 ] [ О ] arr_kaf_name [ 7 ][
arr_kaf__name [ 7 ][ 15

3.9.2. Массивы - как аргументы функций

Массив, в отличие от других видов данных, рассмотренных


нами, всегда передается в функцию по ссылке, а не по значению. По­
чему?
Потому, что массив может занимать много места в памяти и
копировать его, как это делается при передаче аргумента по значе­
нию, расточительно.
Так как массив занимает смежные ячейки памяти, то при ис­
пользовании имени массива в качестве аргумента языки Си/Сн-+
обеспечивают передачу функции адреса первого элемента этого
массива. Модифицируем последний пример.

Файл Р11,СРР
Одно файловый программный проект с двумя функциями. Пример
иллюстрирует передачу массива в функцию

^include <stdlo.h> // Для функций ввода-вывода

86
^define N8 // Строковый размер массива
idefine М 16 // Столбцовый размер массива
// Определение двухмерного символьного массива с
// инициализацией: N строк^ каждая строка из М символов
static char arr_kaf_name [ N ] [ М] =
{
"Состав ФТК:", // Инициализация первой строки
"1 . Кафедра АВТ",
"2. Кафедра ТК",
"3, Кафедра САУ",
"4. Кафедра МУС",
"5. Кафедра ИИТ",
"6. Кафедра САПР",
"7. Кафедра СУД"
);
// Прототип функции: обратите внимание как записывается
// параметр - двумерный массив. Здесь прототип также не
// обязателен, так как файл содержит определение функции
void display ( cha.r arr_kaf_name [ N] [ М ] ) ;

int main ( void ) // Возвращает 0 при успехе


{
// Вызов функции, печатающей строки массива: обратите
// внимание, как записывается аргумент-массив
display ( arr__kaf_name ) ;

return 0;
}

// Печать строк двухмерного массива


void display (
// Массив для печати: передается по ссылке, т.е. не
// копируется
char arr kaf name [ N] [ М ] )
{
auto int index; // Индекс строки двухмерного массива
fori index = О; index < N; index++ )
{
printf( "\n %s ", arr_kaf_name[ index ] ) ;
}

return;
}

Результаты выполнения этой программы выглядят так же, как


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

87
// Определение внешнего статического массива с
// инициализацией. Массив содержит элементы целого типа:
// N строк, каждая строка из М элементов
stable ±zit а[ 3 ] [ 4 ] =
{
{ 1 , 3 , 5 , 1 } , // Инициализация первой строки
{ 2, 4, 6, 8 } ,
{ 3, 5, 7, 9 }
} ;

Резюмируя сказанное, можно заключить, что массив представ­


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

3.9.3. Упражнения для самопроверки

Написать прототип, определение функции и пример вызова


функции для решения следующей задачи:
• вычислить сумму элементов одномерного массива х[ 7V ] {N ~ 50) це­
лого типа имеющих нечетные индексы;
• получить одномерный массив z[N] (N = 40) из двух заданных масси­
вов целого типа jc[ Л^ ], >'[ Л^ ] по правилу:
z[ i ] := тах{ х[ 1 ], у[ i ] }

Возможный вариант ответа можно посмотреть в разд. 18.

3.9.4. Структуры

Различают объявление структуры и определение структурного


объекта. Объявление структуры и определение структурного объек­
та можно выполнять в Си-программе по отдельности или же совме­
стно. Поясним сказанное примерами.
/*
Объявление структуры, содержащей сведения о студенте. Об­
ратите внимание, что данное объявление не размещает никакого
объекта в оперативной памяти, а лишь вводит новый структурный
тип

3timet STUDENT__INFO

88
// Фа культет
char fak_name[ 30 ];
char fio[ 30 ];// ФИО
// Номер группы
char group_name[ 7 ];
char date[ 9 ];// Дата поступления студента в ВУЗ
float stip/ // Размер стипендии
};

Здесь использованы следующие соглашения:


struct - начало объявления и/или определения;
STUDENT_INFO - имя (тэг) структуры;
{
... - список элементов (полей) структуры
} ;

Обратите внимание, что, тэг структуры принято записывать с


использованием прописных букв. Элементы структуры могут иметь
любой тип, допустимый в языке Си, например, тип массива, струк­
турный и т.п. Как уже указывалось, приведенное объявление создает
новый тип с именем STUDENTINFO.
Для создания же структурированного объекта надо использо­
вать его определение:
// Данное определение размещает объект current в оперативной
// памяти компьютера и использует ранее сделанное
// объявление типа STDENT_INFO
struct STUDENT_INFO
current; // Для Си или C++

ИЛИ

/ / Только для C+ + , если нет объекта с другим типом и именем


// STUDENT__INFO
STUDENT_INFO current;

Приведенное выше объявление структуры STUDENT INFO и


определение структурированного объекта current можно объеди­
нить, причем это даст такой же результат:
/'^
Комбинация объявления структурного типа и определения
структурного объекта
V
struct STUDENT_INFO // Тэг структуры
{
// Факультет
char fak name[ 30 ];

89
char fio[ 30 ];// ФИО
// Номер группы
char group_name[ 7 ];
char date[ 9 ];// Дата поступления студента в ВУЗ
float stip; // Размер стипендии
} current; // Имя структурного объекта

Как и массивам^ структурам может быть приписан любой


класс хранения, за исключением класса register. Тем самым будут
определены область действия и время жизни структурированного
объекта. Структуры, как и массивы, можно инициализировать.
Дополнительно отметим, что для элемента структуры статиче­
ский класс хранения в языке Си использовать нельзя. Такая возмож­
ность предусмотрена только в языке C++. При этом в языке C++ для
нескольких объектов одного и того же структурного типа в памяти
размещается только один элемент структуры со статическим клас­
сом хранения, а не несколько.
Для ссылки на элементы структурного объекта current следует
использовать операцию "точка". Эту операцию называют квалифи­
кацией элемента.
/ / Символ, начинающий название факультета
current.fak_name[ О]
current.stip // Размер стипендии студента

Сколько байтов памяти занимает current? Точный размер этого


объекта можно определить с помощью операции sizeof:

s±zeo£( current ) или sizeof ( struct STUDENT__INFO ) ИЛИ


sizeof( STUDENT_INFO )

Последний вариант допустим только в языке C++.


Имена элементов структуры не конфликтуют с такими же име­
нами других объектов, имеющих отличающиеся типы:
Int stip; // правильно
struct STUDENT__INFO
currentl; // Тоже правильно

Объекты stip и currentl.stip располагаются в разных областях


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

90
struct STUDENT_INFO // Тэг структуры
{
// Факультет
сЬяг fak_name[ 30 ];
char fio[ 30 ];// ФИО
// Номер группы
cbatr group__name [ 7 ];
char date[ 9 ];// Дата поступления студента в ВУЗ
float stip; // Размер стипендии
} current = // Имя структурного объекта
{
"ФТК",
"Иванов И. И. " ,
"1081/4",
"01-09-97" ,
100 000,Of
} ;

// Можно создавать массив структур, например:


struct STUDENT_INFO
group[ 12 ];
group[ 5],stip // Стипендия шестого студента группы

3.9.5. Структуры в качестве аргументов функций

Структура может быть передана в функцию целиком. При этом


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

Файл Р14.СРР
Однофайловый программный проект с двумя функциями. Пример
иллюстрирует передачу структуры в функцию по значению
(следует заметить, что этот способ не рекомендуется)
V
iinclude <stdlo.h> // Для функций ввода-вывода

/*
Объявление структурного типа, определение и инициализация
структурированного объекта
V

struct STUDENT_INFO // Сведения о студенте


{
// Факультет
char fak__name[ 30 ];

91
char fiol 20 ];// ФИО
// Группа
char group^^name [ 7 ];
char date[ 9 ];// Дата поступления в университет
float stlp; // Размер стипендии
current = // Определение объекта
{
"ФТК:",
"Иванов И. И.
"1081/4",
"01-09-97",
100000,Of
};

// Прототип: обратите внимание, как записывается параметр


// структура при его передаче по значению. В принципе,
// здесь прототип не обязателен, так как файл содержит
// определение функции
void dlsplay_s ( struct STUDENT_INFO ) ;

int main ( void ) // Возвращает 0 при успехе


{
// Вызов функции, печатающей строки массива: обратите
// внимание, как записывается аргумент-структура
d±splay_s ( current ) ;

retvLrn О;
}

// Печать элементов структуры


void display_s (
struct STUDENT_INFO // Структура для печати: передается
S ) // по значению, т.е. копируется
{

printf( "\п Факультет: %s S.fak name ) ;


printf( "\n ФИО: %s ", s.fio ) ;
printf ( "\n Номер группы: %s ", s.group_name ).
printf( "\n Дата поступления: %s ", s.date ) ;
printf( "\n Размер стипендии: %f ", s.stip ) ;

return;

/*
Файл P15.CPP
Однофайловый программный проект с двумя функциями. Пример
иллюстрирует передачу структуры в функцию по ссылке. Такой
способ передачи структуры в функцию рекомендуется в качестве
основного
*/

^include <stdio.h> // Для функций ввода-вывода

92
Объявление структурного типа, определение и инициализация
структурного объекта
V
struct STUDENT INFO // Сведения о студенте
{
// Факультет
cha,r fak__name[ 30 ];
char fio[ 20 ];// ФИО
// Группа
сЬа.г group__name [ 7 ];
char date[ 9 ];// Дата поступления в университет
float stip; // Размер стипендии
current = // Определение объекта
{
"ФТК:",
"Иванов И. И.
"1081/4",
"01-09-97",
100000,Of
};

// Прототип: обратите внимание, как записывается параметр


// структура при его передаче по ссылке.
void display_s ( struct STUDENT_INFO & ) ;

int main ( void ) // Возвращает 0 при успехе


{
// Вызов функции, печатающей строки массива: обратите
// внимание, как записывается аргумент-структура
display_s( current ) ;

return 0;
}

// Печать элементов структуры


void display__s (
struct STUDENT__INFO // Структура для печати: передается
&S ) // по ссылке, т.е. не копируется

printf( "\п Факультет: %s ", s.fak_name ) ;


printf( "\n ФИО: %s ", s.fio ) ;
print f ( "\n Номер группы: %s ", s.group_name ) ;
printf( "\n Дата поступления: %s ", s.date ) ;
printf( "\n Размер стипендии: %f ", s.stip ) ;

return;
}
3.9.6 Упражнения для самопроверки

1. В текстовом файле "ctrl4.dat" имеется 15 строк, каждая из кото­


рых имеет следующий формат:

93
число_ 1 число_2

Здесь "число_Г' определяет вид геометрической фигуры (1 - квадрат, 2 -


круг), а "число_2" - параметр фигуры (при "число 1" — 1 - длина сторо­
ны, а при "число_2" = 2 - радиус).
1.1. Написать определение массива структур для хранения ука­
занных сведений о геометрических фигурах. Каждый элемент массива
должен иметь следующие поля:
• имя фигуры;
• длина стороны или радиус;
• площадь фигуры.
1.2. Написать фрагмент программы для чтения из файла на маг­
нитном диске "ctrl4.dat" информации о геометрических фигурах.
1.3. Написать фрагмент программы, вычисляющий площади
геометрических фигур.
1.4. Написать фрагмент программы, печатающий в файл
"ctrl4.out" параметры геометрических фигур. Сведения об отдельных
фигурах располагаются в отдельной строке и имеют вид:
круг: радиус= . . . , площадь^ ...

квадрат: длина стороны= ..., площадь= ...


Предусмотреть контроль корректности значений, возвращаемых
функциями библиотеки Си, указать какие включаемые файлы требует
представленный фрагмент.
4. ОПЕРАТОРЫ И УПРАВЛЕНИЕ
ИХ ИСПОЛНЕНИЕМ

В теории программирования доказано, что любая программа


может быть закодирована (записана) с помощью комбинаций трех
элементарных конструкций, каждая из которых имеет только один
вход и только один выход. Как уже упоминалось, такими элемен­
тарными конструкциями являются следующие конструкции:
• последовательность операторов (следование);
• выбор (ветвление);
• итерация (цикл).
Вы уже знакомы с несколькими различными операторами язы­
ка Си, которые реализуют эти конструкции. К ним относятся опера­
торы z/, while, do-while и for.
Однако кроме элементарных конструкций в языках Си/С-н+
существуют и другие операторы, которые помогают облегчить про­
граммирование. Перечень операторов языков Си/С++ в форме син­
таксической диаграммы показан на рис. 31.

4.1. Пустой оператор

Пустой оператор состоит из одного символа — ";". Он не вы­


полняет никаких действий. Тогда возникает вопрос - а зачем он ну­
жен?
Пустой оператор используется как заполнитель в других, более
сложных операторах, например, в операторах z/, for, while. По этой
причине пустой оператор будет обсуждаться при рассмотрении ука­
занных операторов и им подобных.

4.2. Операторы-выражения

Операторы-выражения представляют собой просто выражения,


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

95
Оператор
L
A
^V ' У ^
'
L
^ Выражение
^

->( return V ^
< < >
Выражение
4'>
->^ break
^
->r continue ^

- ^ goto J ь Идентификатор
^
• jf
switch
^
Блок
^ do-while
while
' ^
for

u i i c ; | ^ a 1 Kjyj

Рис. 31. Перечень операторов

4.3. Операторы break и continue

Эти операторы (break - прервать, continue - продолжить), по­


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

4.4. Блок операторов

Как уже указывалось выше, блок (иногда называемый состав­


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

96
фигурные скобки. Повторно отметим, что в языке C + + определе­
ния объектов и операторы могут чередоваться, но определения
объектов долэюны всегда предшествовать их использованию.
Обратите также внимание на то, что в описании синтаксиса
языков Си/С++ всюду, где указан "оператор", в качестве последнего
можно использовать блок операторов.

4.5. Оператор return

Этот оператор имеет следующие две формы:

return ( выражение ) ; ИЛИ эквивалентно return, выражение;

Первая форма обеспечивает передачу управления из текущей


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

4.6. Оператор if

Этим оператором уже ранее пользовались и достаточно интен­


сивно. Это объясняется тем, что / / один из основных структуриро­
ванных операторов языка Си. Синтаксическая диаграмма этого опе­
ратора представлена на рис. 32.
Работа этого оператора состоит в том, что вначале вычисляет­
ся значение заключенного в скобки "выражения". Если его значение
отлично от нуля (!0, "истина"), то выполняется "оператор_1". Если
использовано служебное слово else (иначе) и значение "выражения"
равно нулю (=0, "ложь"), то выполняется "оператор_2", указанный
после служебного слова else. После выполнения "оператора 1" или

97
"оператора_2" управление передается следующему за if оператору
программы.

!=0

dXIH Выражение Оператор_1

= 0
•Г else V^ Оператор_2

Рис. 32. Синтаксическая диаграмма оператора if

Если значение "выражения" равно нулю, но служебное слово


else отсутствует, то управление сразу же передается следующему
оператору программы. Обратите внимание, что если вычисление
значения "выражения" дает нецелый тип, то полученное значение
перед анализом преобразуется к целому типу. Как обычно, в качест­
ве операторов "оператор_1" или " о п е р а т о р е " можно использовать
блоки операторов:
±£( а > Ь )
так = а;
else
max = b;

Обратите также внимание на местоположение символов ';'. На­


личие этих символов необходимо потому, что в качестве "операто-
ра_Г' и "оператора_2" используются операторы-выражения, кото­
рые должны заканчиваться ';' (см. рис. 31).
/ / Эквивалентная запись
±f(a>b)
{
так = а;
}
else
{
max = b;
)
В соответствии с синтаксической диаграммой для операторов,
приведенной на рис. 3 1 , после операторов-блоков символ ";" не ста­
вится. По этой причине символ "точка с запятой" отсутствует после
символов " } " , завершающих блоки.
Поскольку в качестве "оператора 1" и "оператора_2" можно, в
частном случае, использовать и оператор if то операторы if могут
быть вложенными:

98
char ch;
±f( ch -= 'a ' ;
index = 1;
else ±f( ch == 'b ' ;
index =2;

else ±f( ch == 'y' )


index = 25;
else ±f(ch== 'z ' )
index = 26;
else
index = 0;

В этом примере при значении ch, отличном от одной из строч­


ных букв латинского алфавита, переменной index будет присвоено
нулевое значение. Обратите внимание на местоположение вложен­
ных операторов if. Хороший стиль программирования рекомендует
помещать вложенный if в охватывающий г^после else\
// Пример с "подводными камнями"

dovble а = 77. 7;
int i - 5;
if(i<3)
±f( i ^= 2 )
a = 1.1;
else
a = 2.2;
// Здесь имеем a = 7 7 . 7

/ / Другой пример
double a = 77. 7/
int i = 5;
if(i<3)
{
±f( i -= 2 )
a = 1.1;
I
else
a = 2.2;
// Здесь имеем a =2.2

Почему получены такие результаты? Потому, что языки


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

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

4.7. Оператор switch

Вернемся к рассмотренному выше в конце подразд. 4.6 приме­


ру. Этот пример трудно читать и еще труднее сопровождать. В
подобной ситуации, когда требуется передавать управление одному
из нескольких операторов в зависимости от значения выражения,
можно использовать оператор switch (проверяемое выражение при
этом не может иметь вещественный тип). Приведенный выше
фрагмент при использовании оператора switch приобретает
следующий более наглядный и удобный вид:
char ch;

switch ( ch )
{
case 'a':
1ndex = 1; break;
case 'Ь':
index = 2; break/
case 'у':
Index = 25; break;
case 'z ':
index =^ 26; break;
default:
index = 0;
}

Синтаксис оператора switch (переключатель) поясняется диа­


граммой, приведенной на рис. 33.
Выполнение оператора switch начинается с вычисления заклю­
ченного в скобки "выражения", которое долэюно давать результат
целого или символьного типа. Затем просматриваются друг за дру­
гом префиксы case (случай), вычисляются указанные после служеб­
ного слова case "константныевыражения" и полученные значения
сравниваются со значением выражения, указанного после служебно­
го слова switch. Если эти результаты совпали, то управление переда­
ется оператору, следующему за соответствующим служебным сло­
вом case. Если ни одного совпадения не произошло и при этом ука­
зано необязательное служебное слово default (по умолчанию), то

100
управление передается оператору, следующему за default. Если ни
одного совпадения не произошло, а служебное слово default отсут­
ствует, то управление передается оператору, непосредственно сле­
дующему за последней фигурной скобкой оператора switch.

OnepaTop_switch

( switch V ^ T ( V H Выражение V->( ) V w OnepaTop_case

Оператор case

' N I Константное I ^ / ' ^ T ^ [


case Оператор V-ffi }
J \ _выражение | I V ' У I f I

Ц^ default y

Рис. 33. Синтаксическая диаграмма переключателя

После передачи управления в блок операторов case исполне­


ние указанных в нем операторов производится до конца блока (от
одной альтернативы к другой), если только последовательность вы­
полнения операторов не будет изменена операторами break или
goto. Действие оператора break сводится к передаче управления
оператору, следующему за последней закрывающей фигурной скоб­
кой switch (рис. 34). Следует также отметить, что порядок следова­
ния case в блоке безразличен.
Подведем итоги всему сказанному. Для программирования
ветвлений на три или более направлений имеются две альтернативы
- использование вложенных операторов / / и л и использование опера­
тора switch. Из приведенных примеров видно, что использование
оператора switch является более наглядным и простым. Однако этим
оператором нельзя воспользоваться, если проверяемое выражение
имеет вещественный тип или если проверяется комбинация не­
скольких условий. В этом случае приходится использовать вложен­
ные операторы if

4,8. Оператор while

Этот оператор уже рассматривался нами. Он является операто­


ром цикла с предусловием (рис. 35):

101
switch( выражение ) // Выражение не может быть
{ // вещественного типа
case конст._выр,_1:
Операторы
break; —

case конст._выр,_2:
Операторы
break;

Рис. 34. Переключатель

Оператор while

-Н(^ while
while ")-КГО~Л Выражение Оператор

Рис. 35. Синтаксическая диаграмма оператора while

Работа оператора while заключается в следующем.


1. Вычисляется значение "выражения" и, если его тип отличен
от целого, то полученное значение приводится к целому типу.
2. Если значение "выражения" отлично от нуля ("истина"), то
выполняется "оператор" и осуществляется переход к п. 1.
3. Если значение "выражения" равно нулю ("ложь"), то выпол­
нение цикла завершается и управление передается оператору, сле­
дующему за оператором while.
Таким образом, оператор while эквивалентен следующей по­
следовательности операторов:
cycle: ±f( выражение )
{
опера тор
// Оператор передает управление на оператор^
// следующий за меткой cycle (см. подробнее об
// операторе goto ниже)
goto cycle;
}

"Выражение", используемое в операторе while, называется ус­


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

102
Обычно тело цикла представляет собой блок операторов. Это
позволяет обеспечить циклическое выполнение группы операторов.
Но в частном случае, если "выражение" имеет нулевое значение, те­
ло цикла не будет выполняться ни разу. Это является отличительной
особенностью циклов с предусловием.
Рассмотрим и проанализируем ряд специальных примеров.
/ / пример 1
while ( fun( ) == 1 ) ; // В качестве тела цикла использован
// пустой оператор

В этом примере вся работа осуществляется за счет выполнения


условия цикла и не требуется никаких других операторов (fun() -
функция, возвращающая некоторое целое значение; как только она
вернет значение, отличающееся от единицы - выполнение цикла за­
кончится). В подобных случаях щ\л удовлетворения синтаксических
правил следует в качестве тела цикла указать точку с запятой, кото­
рая обозначает пустой оператор.
/ / пример 2
while ( 1 )
{

bxrea-k ;

Этот пример демонстрирует преднамеренное создание "беско­


нечного" цикла. Так как "выражение" в этом цикле всегда имеет
значение 1 ("истина"), то цикл может исполняться неограниченное
число раз. Одним из способов завершения выполнения такого цикла
является использование в его теле оператора break. Как только это
произойдет, управление будет передано оператору программы, сле­
дующему за оператором while. Оператор break подобным же обра­
зом действует в теле цикла и других циклических операторов, кото­
рые будут рассмотрены ниже.
Циклические операторы, так же как и условные операторы,
можно вкладывать друг в друга. В подобных случаях оператор break
обеспечивает выход только из того цикла, в теле которого он на­
ходится.
В языке предусмотрен оператор continue^ который позволяет
пропускать оставшуюся после него часть тела цикла и начать новую
итерацию, т.е. новое выполнение тела цикла сначала. Таким обра­
зом, по своему действию операторы break и continue являются опе­
раторами с ограниченным диапазоном передачи управления. Дейст­
вие их показано на рис. 36.

103
while( выражение) while( выражение )
{ {

break; continue;

} }

Рис. 36. Использование операторов break и continue в цикле while

4.9. Оператор do-while

Оператор do-while, часто называемый оператор do, является


циклом с постусловием и имеет синтаксическую диаграмму, пред­
ставленную на рис. 37 (обратите внимание на наличие в конце опе­
ратора точки с запятой).

Оператор do-while

- ^ / ddo
o j - H Оператор V-U while V ^ / ( V H Выражение Н->Г ) \>( \ )—•

Рис. 37. Синтаксическая диаграмма оператора do-while

Работа оператора поясняется следующей эквивалентной по­


следовательностью операторов:
cycle:
оператор
±£( выражение ) goto cycle;

Из эквивалентного представления следует, в частности, что в


отличие от цикла while, тело цикла do-while, независимо от значения
"выражения", будет выполнено не менее одного раза. Для изменения
хода выполнения операторов, составляющих тело цикла можно вос­
пользоваться операторами break и continue (рис. 38).

do do
{ {
Ьгяак" continue*

} }
while( выражение ); while( выражение);

Рис. 38. Операторы break и continue в цикле do-while

104
Остальные особенности цикла while (использование пустого
оператора, "бесконечный" цикл, вложенность циклов и т.п.) в рав­
ной степени относятся и к циклу do-while.

4.10- Оператор for

Этот оператор также является циклом с предусловием и имеет


синтаксическую диаграмму, представленную на рис. 39. Работа опе­
ратора for поясняется следующей эквивалентной последовательно­
стью операторов:
выражение!
cycle:
if( выражение2 )
{
опера тор
выражение 3
goto cycle;
}

В качестве "выражения!" и "выраженияЗ" можно использовать


списки выражений, в которых выражения разде^лены запятыми.

Оператор for

>j Выражение_1
о Выражение_2

Выражен ие_3
•о Оператор

Рис. 39. Синтаксическая диаграмма оператора/Ьг

/ / пример
char namel 20 ] = "Кафедра АВТ"^
kaf__name [ 20 ];
int 1;
// Копирование пате в kaf_name с использованием цикла for
fori i = 0; пате[ i ] != '\0'; i ++ )
{
kaf_name [ i ] = name [ i ]/
}
kaf name[ i ] = '\0' /

105
// Копирование name в kaf_name с использованием цикла while
// Инициализация управляющей переменной цикла
1 - О/
while ( пате[ i ] != '\0' )
{
kaf__name[ i ] = name [ i ];
// Модификация управляющей переменной цикла
±++;
}
kaf_name[ i ] == '\0'/

// В качестве упражнения предлагается этот же фрагмент


// записать с использованием цикла do-whlle

// В заключение выполним копирование строк с использованием


// строковой функции St г еру (подробнее о строковых функциях
// будет сказано ниже)
^include <string.h> // Для строковых функций •
strcpy ( kaf__name, name ) ;

Из рассмотренного примера следует, что в подобных случаях


оператор уЬг удобнее и легче для восприятия, чем операторы while и
do'while. Это обусловлено тем, что все три выражения, связанные с
организацией цикла (инициализация, проверка и модификация усло­
вия цикла) собраны вместе. За счет этого не приходится просматри­
вать исходный код в поисках выражений, обеспечивающих
инициализацию и модификацию, как пришлось бы делать при
применении операторов while и do-while.
Из примера также следует, что управляющая переменная
циклических операторов после завершения соответствующих
циклов сохраняет свое значение и значением этой переменной, при
необходимости, можно пользоваться, как это было сделано в нашем
примере.
Из синтаксической диаграммы цикла for следует также, что
пустой оператор может быть использован для пропуска любого из
выражений, входящих в состав этого оператора:
for( ; ; ) оператор // Бесконечный цикл

Как и для циклов while и do-while, для цикла for последова­


тельность передачи управления в теле цикла может быть изменена с
помощью операторов break и continue (рис. 40).

106
for( выр1; выр2; вырЗ ) for( выр1; выр2; вырЗ )
{ {

break; continue;

} Передача управления на
«выражениеЗ» - см.
эквивалентное представление
Рис. 40. Использование операторов break и continue в цикле/Ьг

Пример. Одномерный массив вещественного типа напечатать по


четыре элемента в строку по 15 позиций на элемент
JV
^define N100 // Размер массива

^include <stdlo.h> // Для функций ввода-вывода

{
float а[ N ]/ // Массив для печати

printf( "\п" ) ; // Начать печать с новой строки


for( int i = О; 1 < N; i-f-f ;
{
±f( ( 1 % 4 ) == 0 ) printf( "\n" ) ;
pr±ntf( "%15g", a[ i ] ) ;
}

В качестве упражнения рекрмендуем Вам запрограммировать


эту же задачу с использованием циклов while и do-while.
Повторно напоминаем, что в языке C++ в блоке можно чередо­
вать определения объектов и операторы, но определение объекта
обязательно должно предшествовать его использованию в операто­
ре. В рассмотренном примере таким объектом является /. Область
действия и время жизни / - от точки определения (заголовок цикла)
и до конца блока (объект с автоматическим классом хранения).
Завершая рассмотрение циклических операторов, отметим, что
при программировании цикла есть три возможности:
• использовать цикл/Ьг;
• использовать цикл while;
• использовать цикл do-while.
Возникает вопрос: какой из этих альтернатив следует восполь­
зоваться в конкретном случае? Ответ прост - лучше всего, как было
показано выше, использовать цикл for, а это всегда можно сделать,

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

4.11. Оператор goto и метки операторов

Операторы break и continue определялись выше как операторы


с ограниченным диапазоном передачи управления. В дополнение к
ним языки Си/С++ предоставляют программисту и нашумевший
оператор перехода ^о/'о:
доЬо идентификатор;

Здесь "идентификатор" является именем метки, которая запи­


сывается в виде
идентификатор:

Строго говоря, при написании программ применение операто­


ра goto не является необходимым, так как любая программа может
быть написана с помощью только трех элементарных конструкций,
каждая из которых имеет только один вход и один выход:
• следование;
• ветвление;
• цикл.
Однако существуют ситуации (их немного), когда goto удобен,
и поэтому его включили в язык Си. Из числа подобных ситуаций на­
зовем две.
1. Обычно оператор goto служит для передачи управления в
конец функции в случае обнаружения ошибки, особенно если ошиб­
ки могут возникать во многих местах функции. В конце функции,
куда выполняется переход по goto, выполняется обработка ошибок и
возвращается значение, соответствующее наличию ошибки.
2. Другим примером целесообразного применения goto может
служить выход из многократно вложенных циклов, поскольку опе­
ратор break осуществляет выход только из того цикла, где он ис­
пользован (рис. 41). В остальных случаях использовать goto не сле­
дует.

4.12. Упражнения для самопроверки


1. Изобразить фрагмент схемы программы, соответствующий сле­
дующему фрагменту Си-программы:

108
if( с =^ 1 ) а + + / else ±f( с 2 ) а-
else ±£( с =^ 3 ) а += 1/

while( выражение )
{

while( выражение )
{

lf( /* Ошибка */... ) goto loop_end;

(oop_end:
<
Рис. 41. Использование оператора go/^о для выхода
из гнезда циклов

2. Записать фрагмент программы, соответствующий следующему


фрагменту схемы программы (выполнить действие, противоположное
предыдущему):

Нет
<^а <= Ь ^ г:=3; i к

1 Да
к:=п;
г:= 1;

3. с помощью операторов ветвлений и присваивания записать на


языке Си фрагмент программы, вычисляющий величину
[ п+1 при 1=4,
п ^ [ а+Ь при i='l, 1, 9,
[ а-Ь в остальных случаях

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


int к;

Укажите, что напечатает следующий фрагмент программы:


printf( "\п %5s \ л " , '"*-" ) ;
for( к = 1; к >= -5; к-- )
printf( " %li %3s ", к, "--" ) ;

109
5. Пусть определен массив
±nt а[ 25 ];

Напишите фрагмент Си-программы, который напечатает с новой


строки значения элементов массива "а" по пять элементов в строке и по
десять позиций на элемент. Решить задачу с помощью цикла while.

6. При каких исходных значениях "А:" приведенный ниже цикл бу­


дет выполняться бесконечно?
±zit к;
wh±lG( к < 5 ) к+ + ;
5. ВЫРАЖЕНИЯ И ОПЕРАЦИИ

В языках Си/С++ предусмотрен богатый набор операций. В


дополнение к традиционным арифметическим, логическим операци­
ям, операциям отношения и присваивания предусмотрены сокра­
щенные версии этих операций, побитовые операции и операции над
адресами (указателями). Рассмотрим операции и выражения, ис­
пользующие их, кроме операций над адресами и побитовых опера­
ций, которые будут рассмотрены ниже.
Можно выделить шесть категорий операций:
• ссылки;
• унарные операции;
• бинарные операции;
• тернарные операции;
• присваивания;
• операция "запятая".
Операции ссылки используются, в основном, для доступа к
элементам массивов и структур.
Унарные операции воздействуют на одно значение или выра­
жение.
В бинарных операциях участвуют два выражения, а в тернар­
ных - три.
Приоритеты операций в порядке их убывания и порядок вы­
полнения операций с одинаковым приоритетом указаны в табл. 16.
Из приведенной таблицы следует, что в особый класс бинарных
операций выделены операции присваивания.

Табл. 16. Приоритеты и порядок выполнения операций


Наименование Знаки операций Порядок
операций выпол­
нения
Разрешение области Нет
видимости
Ссылочные [ ] - доступ по индексу: адрес_начала[ выражение ] Слева -
или выражение[ адрес_начала ]) направо
( ) — управление порядком выполнения операций в
выражении; вызов функции:
имя_функции( списокпараметров );
конструирование значения:
тип(списокпараметров)
. — выбор члена класса посредством объекта:
объект, членкласса
-> - выбор члена класса посредством указателя:
указатель->член класса

111
продолжение табл. 16
Наименование Знаки операций Порядок
операций выпол­
нения
Унарные ++ - постфиксный инкремент: lvalue++ Нет
— постфиксный декремент: lvalue-
new - динамически создать объект (выделить
динамическую память): new type или
new type( списоквыражений)
delete — уничтожить объект (освободить
динамическую память): delete указа-
гел ь н а о б ъ е к т
delete[ ] - уничтожить массив объектов:
delete указательнамассивобъектов
++ - префиксный инкремент: -ь+lvalue
— префиксный декремент: —lvalue
* - разадресация (разименование): * выражение
& - получение адреса объекта: & lvalue
+ - унарный плюс: + выражение
— - унарный минус: - выражение
! - логическое отрицание (not): !выражение
'- - поразрядное дополнение: -выражение
sizeof - размер в байтах: 812еоГ(объект)
или sizeof(THn)
typeidO - идентификация типа времени
выполнения: typeid(type)
(type) - приведение типа: (type)выpaжeниe -
приведение типа выражения к типу в скобках
(старый стиль), выполняется справа-налево
COnst_cast — константное преобразование типа:
const_cast<type>(выpaжeниe)
dynamic_cast - преобразование типа с проверкой
во время выполнения:
dynamiccast <1уре>(выражение)
reinterpret__cast - преобразование типа без
проверки: reinteфret_cast<type>(выpaжeниe)
static__cast — преобразование типа с проверкой во
время компиляции: static cast<type>(выpaжeниe)
.* - выбор члена класса посредством объекта:
объект. *указатель_на_член_класса, выполняется
слева-направо
->* - выбор члена класса посредством указателя на
объект:
указатель на объект ->* указатель на член класса,
выполняется слева-направо
Мультипликативные * - умножение: выражение * выражение Слева -
бинарные / - деление: выражение / выражение | направо
% - остаток от деления (деление по модулю):
выражение % выражение

Аддитивные бинарные + - сложение: выражение -^ выражение Слева -


- - вычитание: выражение - выражение направо

Сдвига бинарные « - сдвиг влево: выражение « выражение Слева -


» - сдвиг вправо: выражение » выражение направо

112
Продолжение табл. 16
1 Наименование Знаки операций Порядок
операций выпол­
нения
Отношения бинарные < - меньше: выражение < выражение Слева —
> - больше: выражение > выражение направо
<= - меньше или равно: выражение <= выражение
>= - больше или равно: выражение >= выражение
Отношения бинарные == - равно: выражение == выражение Слева —
!= - не равно: выражение != выражение направо |
Поразрядная "И" & - поразрядное умножение: Слева -
бинарная выражение & выражение направо
Поразрядная ^ - выражение '^ выражение Слева -
"ИСКЛЮЧАЮЩЕЕ направо
ИЛИ" бинарная
Поразрядная 1 - выражение | выражение Слева -
"ВКЛЮЧАЮЩЕЕ направо
ИЛИ" бинарная
Логическая "И" && - логическое умножение: Слева -
бинарная(and) выражение && выражение направо
1 Логическая "ИЛИ" ii - логическое умножение: выражение || выражение Слева -
бинарная (or) направо
Условная тернарР1ая ?: - выражение ? выражение : выражение Справа-
налево
Простое присваивание = - lvalue = выражение Справа —
налево
Совмещенное *= - выражение *= выражение Справа —
присваивание /= - выражение /= выражение налево
%= - выражение %= выражение
+= - выражение += выражение
-= - выражение -= выражение
« = - выражение « = выражение
» = - выражение » = выражение
&= - выражение &= выражение
1= - выражение |= выражение
^= - выражение ^= выражение
Генерация исключения throw - throw выражение Нет
Запятая , - выражение , выражение Слева -
(последовательность) направо |

5.1. Операции ссылки

Порядок выполнения операций (раньше - позже) определяется


их приоритетом. Операции с одинаковым приоритетом могут вы­
полняться в порядке их появления в выражении слева - направо или
справа - налево. В этом плане операции ссылки после операции раз­
решения области видимости имеют наивысший приоритет и выпол­
няются слева - направо (табл. 16).
Имеются следующие разновидности операций ссылки:
• ссылка на элемент массива [ ];

113
• ссылка на элемент структуры .;
• ссылка на элемент структуры с помощью указателя ->.
Особое место в этой группе занимает операция "()", служащая
для управления порядком выполнения операций в выражении. Объ­
ясняется это тем, что данная операция, как и другие операции ссыл­
ки, имеет наивысший приоритет.

Ссылка на элемент массива. Операция предназначена для


выделения конкретного элемента массива. Чтобы использовать эту
операцию, выражение, называемое индексным и имеющее целое
значение, заключают в квадратные скобки и записывают после име­
ни массива:
^define N100 // Размер массива

±nt arr[ N ]; // Определение массива целого типа


// из N элементов
... агг[ i+2 J // Ссылка на элемент массива
... arrf 12 ] // Ссылка на элемент массива

Повторно напомним, что у массива, содержащего N элементов,


значения индекса изменяются в диапазоне от О до Л^-1. Имя массива,
записанное без операции ссылки, означает адрес первого элемента
массива:

а г г эквивалентно <&агг/" О ]

Алгоритм выполнения ссылки на элемент массива следующий


(например, для агг[ i+2 ]):
1. Вычислить значение выражения, которое служит индексом.
Пусть, например, результат s = i+2.
2. Преобразовать s в сдвиг в массиве элемента с индексом /+2
относительно начала массива. Для этого следует умножить s на раз­
мер отдельного элемента массива:
shift = S '*' s±zeof( ±nt )

3. Вычислить адрес элемента массива по формуле:


adr = arr+shift = arr+s*slzeof (int) = &arr [ 0] -hs'^slzeof (int)

Эта функция получила название функции индексации. В ней


агг = 8сагг[ О ] означает адрес первого элемента массива (адрес на­
чала массива).
4. Извлечь значение, находящееся по этому адресу.
Нетрудно заметить, что для доступа к элементам одномерного
массива нужно выполнить довольно много работы - одно умножение

114
и одно сложение. Поэтому работа с массивом по сравнению со c i ^ -
лярными объектами происходит существенно медленнее и это сле­
дует иметь в виду.
Получим функцию индексации для двумерного массива. Дву­
мерный массив (в математике его называют матрицей) располагает­
ся в оперативной памяти по строкам - сначала располагаются эле­
менты первой строки матрицы в порядке возрастания индексов и
адресов оперативной памяти, затем - второй строки и т.д.
#define N10 // Строчный размер матрицы (массива)
#define М 9 // Столбцовый размер массива
// Определение двумерного массива
short ±nt two_arr[ N ][ М ]/

// Функция индексации для элемента массива two__arr[i] [j] :


// 1. Вычисляем значения индексных выражений, указанных в
// квадратных скобках (в данном случае этого не требуется) .
// 2. Вычисляем сдвиг указанного элемента относительно начала
// массива shift = (i*M+j) *s±zeo£(shoirb int) .
// 3, Тогда функция индексации, дающая адрес оперативной
// памяти, по которому находится элемент массива
// two__aгг [i ] [j ] , имеет следующий вид:
// adr = two_arr-hshift = 8ctwo_arr[0] [0] + (i*M+j)
// *3±zeof( short int ; .

Для доступа к элементам двумерного массива вычислительной


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

Ссылка на элемент структуры. Ссылка на элемент структу­


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

current .stip (см. подразд. 3.9.3 о структурах)

представляет значение элемента stip структуры current. Так как


структуры могут быть вложенными, то вложенность распространя­
ется и на ссылку на элемент структуры. Например,
bigstr.smallstr.elem

представляет элемент elem структуры smallstr^ которая, в свою оче­


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

115
STUDENT__INFO s_data, // Структура
*ps_data = &s__data;
// Указатель на эту же структуру
... s_data.stlp /* эквивалентно */ ps__data->stlp

5.2. Унарные операции

Приоритет унарных операций ниже, чем операций ссылок и


выполняются они справа налево (см. табл. 16). Унарные операции
представлены в табл. 17.

Табл.17. Унарные операции


- Инвертирование знака
f Логическое отрицание
++ — Увеличение, уменьшение
sizeo/{ имя типа ) Размер в байтах
(спецификация типа)выражение Преобразование типа
~ Поразрядная инверсия (обсуждается ниже в
разделе "Поля битов и побитовые операции")
& * Адресация, разадресация (обсуждается ниже
в разделе "Указатели")

Унарный минус. Является обычной арифметической операци­


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

Логическое отрицание. Операция отрицания "!" изменяет


значение "истина" на значение "ложь", а значение "ложь" - на значе­
ние "истина". Напомним, что значению "истина" соответствует не­
нулевое целое значение, а значению "ложь" - нуль.

Увеличение и уменьшение. Операции "++" и "—" могут пред­


шествовать операнду (префиксные операции) или следовать за ним
(постфиксные операции). Эти операции соответственно добавляют
или вычитают единицу из значения операнда и присваивают ему по­
лученный результат:
X = X -h 1; /'^ эквивалентно *•/ ++х;
у = у - 1; /* эквивалентно V --у;

X = X + 1; /* эквивалентны = arrf -h-i-x 7/


а = аг г [ X ];

116
а = arr [ X ]; /* эквивалентны */ а = arr [ к + + ];
к = к + 1;
При использовании "++" и "—" тип результирующего значения
тот же, что и тип операнда, над которым выполняется операция.

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


явного изменения типа выражения:
( спецификация_типа )выражение

"Спецификациятипа" может быть любым служебным словом,


задающим спецификацию типа, например, int, short, long, float и т.д.
int 1 = 11000, w = 4;
long a;
// Если целое занимает 2 байта (разрядность процессора 16
// бит), то здесь возникает ошибка при умножении:
// 44000 > 32767. Как быть?
а = 1 "^ w;
а = ( long ) ( 1 * W ) ; // И здесь возникает ошибка при
// умножении: 44000 > 32 767
а = ( long )1 * w; // Так правильно!

Другим распространенным примером использования автома­


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

Операция sizeof Операция выполняется на этапе компиляции


программы и дает константу, которая равна числу байтов, требуе­
мых для хранения в памяти данного объекта. Объектом может быть
имя переменной, массива, структуры или просто спецификация ти­
па. Применение этой операции демонстрировалось выше.
Пример.
±пЬ count, iarrayl 10 ];
for( count = 0; count < slzeof( larray ) / sizeof ( ±nt ) ;
count++ )
printf( "iarray[%d] = %d\n", count, iarrayl count ] ) ;

Применение операции sizeof всюду, rjxQ это возможно, счита­


ется хорошим стилем программирования.

117
5.3. Бинарные операции

приоритет и порядок выполнения бинарных операций пред­


с т а в л е н ы в т а б л . 16. Б и н а р н ы е о п е р а ц и и в о з д е й с т в у ю т на д в а в ы р а ­
жения:

выражение Ыпор выражение

З д е с ь Ыпор - о д н а из б и н а р н ы х о п е р а ц и й , п р и в е д е н н ы х в т а б л .
18.
Т а б л . 18. Б и н а р н ы е о п е р а ц и и
, /, % Умножение, деление, взятие остатка от деления целого на целое
Сложение, вычитание
Операции сдвига (обсуждаются в разделе "Поля битов и побитовые
» операции")
Больше, меньше
Больше или равно
Меньше или равно
Равно
Не равно
&, м Поразрядные логические операции (обсуждаются в разделе "Поля битов
и побитовые операции")
&&, Логическое И, логическое ИЛИ

Арифметические операции: "*", "/", "%", "+" и "-". Эти о п е ­


рации задают обычные действия над операндами арифметического
типа. О п е р а ц и я "%" о з н а ч а е т п о л у ч е н и е о с т а т к а от д е л е н и я о д н о г о
г^елого ч и с л а на д р у г о е :

1 % j дает значение i - ( i/j ) * j

Примеры. 12 % 6 д а е т О, 13 % 6 д а е т 1, 3 % 6 д а е т 3 и т.д.
Если арифметическая операция или операция отношения со­
д е р ж и т о п е р а н д ы р а з л и ч н ы х т и п о в , то к о м п и л я т о р в ы п о л н я е т а в т о ­
м а т и ч е с к о е п р е о б р а з о в а н и е их т и п о в , если з а м е н а т и п о в я в н о не
указана. Такое преобразование производится путем "продвижения"
значений операндов к "наибольшему" типу:

long double (наибольший тип)


double
float
unsigned long int
long int
unsigned int
int
unsigned. char или unsigned short int
ciiax- или short int (наименьший тип)

118
Алгоритм выполнения очередной бинарной арифметической
операции ("*", "/", "+", "-") или операции отношения ("<", ">", ">=",
"<=", "==", "!=") состоит в следующем.
1. Если один операнд имеет тип long double, то и второй опе­
ранд преобразуется к типу long double и выполняется операция.
Иначе производится переход к п. 2.
2. Все операнды с типом float преобразуются к типу double и
производится переход к п. 3.
3. Если один операнд имеет тип double, то и второй операнд
преобразуется к этому же типу и выполняется операция. Иначе вы­
полняется п. 4.
4. Если один операнд имеет тип unsigned long int, то и второй
операнд преобразуется к этому же типу и выполняется операция.
Иначе выполняется п. 5.
5. Если один операнд имеет тип long int, то и второй операнд
преобразуется к этому же типу и выполняется операция. Иначе вы­
полняется п. 6.
6. Если один операнд имеет тип unsigned int, то и второй опе­
ранд преобразуется к этому же типу и выполняется операция. Иначе
выполняется п. 7.
7. Если операнды имеют тип unsigned char и/или unsigned short
int, то они преобразуются к типу unsigned int и выполняется опера­
ция. Иначе выполняется п. 8.
8. Если операнды имеют тип char и/или short int, то они пре­
образуются к типу int и выполняется операция.
Далее по такому же алгоритму выполняется следующая из пе­
речисленных выше арифметических операций и операций отноше­
ний.
Результат арифметической операции имеет такой же тип, как и
тип операндов после преобразования. Результат операции отноше­
ния всегда имеет тип int (О - "ложь", "нет" и 1 - "истина", "да").
И еще раз повторим важное замечание. В вызовах функций ар­
гументы с типом float автоматически преобразуются к типу double, а
аргументы с типом char преобразуются к типу int.

Операции отношения. Приоритеты и порядок выполнения


операций отношения "<", ">", "<=", ">=", == и "!=** указаны в табл.
16. Операция "==" выполняет проверку на равенство, а операция
"!=" - проверку на неравенство. Смысл остальных операций отноше­
ния очевиден.
Запись операции отношения имеет вид:
операнд! операция__отношения опера нд2

119
Как указывалось выше, результатом вычисления отношения
может быть или ненулевое целое значение ("истина") или нулевое
значение ("ложь"). Таким образом, после выполнения присваивания
X = count < slzeof( la гray ) / 2

переменной "х" будет присвоено значение либо единицы, либо нуля.


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

Логические операции (**&&" и "||"Л Приоритет и порядок вы­


полнения логических операций также представлены в табл. 16, а их
смысл описан в табл. 19, называемой таблицей истинности.

Табл. 19. Таблица истинности для логических операций


Операнд1 Операнд2 Операнд1 && Операнд2 Операнд1 |j Операнд2
НеО НеО 1 1
НеО 0 0 1
0 НеО 0 1
0 0 0 0

Вычисление выражения с логическими операциями прекраща­


ется, как только результат становится однозначно определенным:
О && ( А > в )
2 \\ ( ( А+В ) < 4 )

В этих выражениях правый операнд не будет вычисляться.


Пример. Логическая функция задана следующей таблицей ис­
тинности (табл. 20).

Табл. 20.Задание логической функции таблицей истинности


Параметр 1 ( р1 ) Параметр 2 ( р2 ) Значение функции
НеО НеО 0
НеО 0 0
0 НеО НеО
0 0 0

// Прототип
±nt lf( ±nt pi, ±nt p2 )
// Определение функции
±nt If( // Возвращает значение функции
±nt // Параметр функции
±nt P2 ) // Параметр функции
(
±f( ( !pl ) && p2 )

120
retuxm 1,

jretux-n 0;

// Пример вызова функции


±nt value = lf( 4, 0 ) ;

5.4. Тернарная операция

Тернарная операция применяется для конструирования услов­


ных выражений:
выраж__1 ? выраж_2 : выраж__3

Данное выражение интерпретируется следующим образом.


Вначале вычисляется "выраж 1". Если его значение отлично от нуля
("истина"), то вычисляется "выраж_2", следующее за знаком "?". Ес­
ли же "выраж 1" имеет нулевое значение ("ложь"), то за значение
ус^ловного выражения принимается значение " в ы р а ж З " .
Пример.
max = ( a>b ) ? а : b;
// Эквивалентно
±f( a>b )
max = a;
else
max = b;

5.5. Операции присваивания

Приоритеты и порядок выполнения операций присваивания


указаны в табл. 16.
Простая операция присваивания используется следующим
образом:
lvalue = выражение/

Приведенная запись получила название выраэюение присваива­


ния. Так как результатом выражения присваивания служит значение
"выражения", стоящего справа от операции присваивания, которое
присваивается объекту '4value", то выражения присваивания могут
быть вложенными:
max = min = 0;

121
в приведенном примере вначале будет выполнено присваива­
ние min = О, в результате которого ''min'' станет равно нулю, а затем
это же значение будет присвоено "max",
Операцию присваивания можно записывать в сокращенной
форме, что широко используется:
Обычна я ф орма Сокращенная форма
count = count + 2; count += 2;
offset = offset / 2; offset /= 2;
s = s * ( f-h2 ) ; s *= f + 2;
a[ i+2 ] = a[ i+2 ] - 10; a[ i+2 ] -= 10;

Операции присваивания "+=", "/=", "*="^ "%=" и подобные им


(см. табл. 16) называют составными операциями присваивания.
Операция присваивания выполняется следующим образом:
• вначале вычисляется значение выражения, стоящего справа от
операции присваивания;
• полученное значение автоматически приводится к типу объекта,
указанного слева от операции присваивания (! такое преобразо­
вание лучше делать явно с помощью операции приведения типа);
• преобразованное значение присваивается объекту, указанному
слева от операции присваивания.

5.6. Операция "запятая"

Приоритет и порядок выполнения операций "запятая" указаны


в табл. 16. Отметим, что эта операция имеет самый низкий приори­
тет.
Операция может быть использована для разделения несколь­
ких выражений. Эта операция, чаще всего, применяется в операторе
for для того, чтобы выполнялось более одного выражения в управ­
ляющей части этого оператора:
/ / Суммирование первых десяти ненулевых целых значений
£ог( ±пЬ 1=1, sum = 1; 1 < 10; i++, sum += i ) ;
// To же самое, в более длинной форме
±nt sum = 0;
for( ±nt i = 1; 1 <= 10; i + + )
(
6. УКАЗАТЕЛИ

Как известно, оперативная память ЭВМ и, в частности, память


IBM PC делится на восьмибитовые байты. Каждый байт пронумеро­
ван, нумерация начинается с нуля. Номер байта называется адресом.
Говорят, что адрес указывает на определенный байт. Таким образом,
указатель является просто адресом байта памяти.

6.1. Зачем нужны указатели?

в большинстве прикладных программ можно обойтись без яв­


ного применения указателей. К числу таких программ относятся
программы, написанные на языках высокого уровня, кроме про­
грамм, написанных на языках Си/С++.
Зачем эюе нуэюны указатели!
1. Применение указателей во многих случаях позволяет упро­
стить программу и/или повысить ее эффективность.
2. Для управления памятью ЭВМ. Так в состав Си-библиотек
входят функции резервирования и освобождения блоков оператив­
ной памяти в любой момент в процессе выполнения программы. В
языке же C++ для этой цели предусмотрены операторы new и delete.
Эти операторы C++ управляют динамической памятью. Управление
динамической памятью позволяет эффективно увеличивать размеры
программ при тех же ресурсах компьютера и приспосабливать их к
изменяющимся размерам данных. Как программа "узнает", где рас­
положен вновь занятый блок памяти? С помощью указателя!

6.2. Указатели и их связь с массивами и строками

Рассмотрим несколько примеров.


/ / iarray - адрес iarrayf О ]
int Iarrayf 6 ];
// Напечатаем 16-ричный адрес iarray[ О ]
print f ( "iarray = %х \ л " , iarray ) ;

В Си имеется операция "&" взятия адреса, в результате приме­


нения которой к имени данного получается адрес этого данного.
Например, вызов
printf( "iarray = %х \ л " , &iarray[ О ] ) ;

123
дает точно такой же результат, как и предыдущий вызов.
iarray = выражение/
// Ошибка: имя массива является указателем на место
// расположения массива в памяти^ ему нельзя присваивать
// новое значение
"Это строка \п" // Значением строки является
// указатель на первый символ
// строки^ т.е. адрес символа "Э"
// в памяти

6.3. Определение и применение указателей

Определение указателей. Синтаксис определения указателя


имеет вид:
описатель_класса_хранения спецификация^ типа *идентификатор;

"Спецификациятипа" может относиться к любому основному


или производному типу данных. Символ "*" означает "указатель на".
Примеры:
±nt *iptr; / / Определяет iptr как указатель на
// целое
ex t em chctr *cptr; // Объявляет cptr как указатель на
// символ
static float *fptr; // Определяет fptr как указатель на
// статическое данное с плавающей
// точкой

Операции над указателями. Один из способов получения ад­


реса данного состоит в применении унарной операции взятия адре­
са &, приоритет и порядок выполнения которой указаны выше в
табл. 16:
±xit main ( void )
{
int Xr // Целое
*px; // Указатель на целое: пользоваться
// указателем пока нельзя^ так как
// его значение еще не определено

X = 2;
рх = &х; // Теперь указателем рх пользоваться
// можно: *рх равно 2
*рх = 3; // Теперь *рх равно х равно 3

return 0;
}
Единственное ограничение применения операции взятия адре­
са & состоит в том, что ее нельзя применить к объекту с классом
хранения register, а также к полям битов (поля битов обсудим позже
в разделе "Поля битов и битовые операции").
Из приведенного выше примера следует, что к переменной-
указателю, как только ей присвоен допустимый адрес, можно при­
менить операцию разадресации, обозначаемую "*". Приоритет и по­
рядок выполнения этих операций также указаны выше в табл. 16.
/ / Пример без разадресации
int main ( void )
{
int X, у /

X = 2; у = x;

return 0;
}
// Эквивалентный пример с разадресацией
int main( void )
(
int X, у г *рх;

X = 2; рх = &х; у = *рх;

return О;
}
// Здесь *рх означает извлечь значение^ находящееся по адресу
// рх: косвенная адресация
Из приведенного выше первого примера следует также, что
операцию разадресации можно использовать и в левой части выра­
жения присваивания.
Инициализация указателей. Указатели, как и объекты других
типов, можно определять, совмещая определение с инициализацией.
Приведем несколько примеров такого рода.
/ / Использование указателей в инициализирующих выражениях
// (рх - адрес х)
Lnt X, *рх = &х;
char line [ 80 ], "upline = line;
// pline - адрес line[ 0 ]
int *page = 0xB800;
// Указатель на начало видеобуфера
// цветного дисплея

125
Арифметические операции над указателями. В связи с
арифметическими операциями над указателями рассмотрим не­
сколько примеров.

Пример 1.
±пЬ i [ 6 ] г *pi = i /
/ / i и pi - адрес i [ О ]

Будем считать, что первый байт массива / имеет адрес 10 000.


Тогда для первого элемента массива (его индекс равен нулю):
• адрес равен 10 000, или &/[ О ], или /, или/?/;
• значение элемента равно *10 000, или /[ О ], или */, или */>/.
Для элемента массива с индексом/ (J = О, 1,2, 3, 4, 5):
• адрес равен 10000 + / * sizeofi int ), или & / [ / ], или / + / , или pi +
У;
• значение элемента равно *( 10 000 + / * sizeoJ{ int ) ), или / [ / ],
или *( / + / ), или *(/?/ + / ).

Пример 2.
#define N 3 // Строчный размер массива
^define М 4 // Столбцовый размер массива
int а[ N ] [ М ] , *ра == а ;

Будем считать, что первый байт массива "<я" имеет адрес


20000. Тогда для элемента массива, принадлежащего строке с ин­
дексом О и столбцу с индексом 0:
• адрес равен 20 000, или 8са[ О ][ О ], или а, илира',
• значение элемента равно *20 000, или «[ О ][ О ], или *а, или */7а.
Для элемента массива, принадлежащего строке с индексом
/ (/ = О, 1, ..., iV-1) и столбцу с индексом 0:
• адрес равен 20 000 + ( / * М ) * sizeof{ int ), или 8са[ / ][ О ],
или *( а + / ), или а[ / ], или &.ра[ / * М ] ;
• значение элемента равно *( 20 000 + ( / * М ) * sizeof{ int ) ),
или а{ / ][ О ], или *( *(flf+ / ) ), или *<з[ / ], или ра{ / * М ] .
Для элемента массива, принадлежащего строке с индексом
/ (/ = О, 1, ..., 7V-1) и столбцу с индексом/ (j — О, 1, ..., М-1):
• адрес равен 20 000 - ) - ( / * М + / ) * sizeof{ int ), или &а[ / ] [ / ],
или *( а + / ) -f/, или а[ / ] +у, или &ра[ / * М + j ];
• значение элемента равно *( 20 000 + ( / * М +j ) * sizeoJ{ int ) ),
или а[ / ] [ / ], или *(*( а + / ) + / ), или *( а[ / ] +/ ),
или ра[ / * M-^J ].

126
Пример 3.
/ / Программа добавляет строку s к концу строки t

^include <stdlo.h> // Для функций ввода-вывода

int main ( void ) // Возвращает О при успехе


{
char t[ 24 ] == "Персональная ",
"^pt = t , / / pt - адрес начала t
s[ ] = "ЭВМ IBM PC",
"^ps = s; // ps - адрес начала s
while ( *pt != '\0' ) pt-h + ;
// Здесь pt - адрес завершающего символа строки t , т.е.
// адрес '\0'
// Посимвольно копируем строку s в "хвост" строки t, пока
// не будет скопирован нуль-символ
while ( ( *pt = *ps ) != '\0' )
{
pt++; ps++;
}
printf( "\n Сцепление строк (конкатенация): %s", t ) ;

return 0;
}

Хотя на практике это требуется не часто, можно определять


объекты, которые будут указателями на указатели и использовать
несколько уровней косвенной адресации:
/ / Указатель на указатель на целое, т.е. адрес адреса целого
// (уровень косвенной адресации 2)
int <' " *ppi ;

**ppi ... / / Это значение целого

В приведенных выше примерах рассматривались только ариф­


метические операции "н-+" и "+", но наряду с ними могут использо­
ваться и такие арифметические операции, как "-" и "—".

Сравнение указателей. Указатели можно сравнивать с помо­


щью операций отношения:
<, >, <=, >=, ==, !=

! Обратите внимание на очень важное замечание. В сравнении


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

127
Пример.
// Сравнение указателей: копирование одной строки в другую

^include <stdlo.h> // Для функций ввода-вывода

±nt main ( void ) // Возвращает О при успехе


{
cbstr al [ 7 7 , // Строка -приемник
*р1 = al, // р1 - адрес начала al
а2 [ ] = "Пример",
// Строка-источник
*р2 = а2; // р2 - адрес начала а2
// Копировать а2 в al
while ( р2 < ( а2 + sizeof( а2 ) ) )
{
*р1 = *р2; р1+ + ; р2+ + ;
}
pr±ntf( "\п Копия: %s", al ) ;

jretujrn О;

6.4. Указатели на структуры

Другим распространенным применением указателей является


манипулирование структурами:
/ / Объявление структуры, содержащей сведения о студенте
struct STUDENT_INFO
{
// Факультет
char fak_name[ 30 ];
char fio[ 30 ];// ФИО
// Номер группы
char group_name [ 7 ];
char date[ 9 ]/// Дата поступления студента
float stip; // Размер стипендии
} ;

// Указатель на структуру
STUDENT_INFO s_data, // Определение структуры
sl_data уг // Определение еще одной структуры
*ps_data; // Указатель на структуру данного
// типа
ps_data = &s_data; // Указатель на s_data: адрес
// первого байта s_data

// Доступ к элементу структуры: обе приведенные ниже формы

128
// эквивалентны
... s__data . stip ... ps_data->stip ...
Приоритеты и порядок выполнения операций "." и "->"
приведены выше в табл. 16.
/ / Определение адреса элемента структуры: обе приведенные
// ниже формы эквивалентны
... &s__data , stip ... &ps_data->stip ...
/ / Операция присваивания над структурами: оба операнда должны
// быть объектами с одинаковыми типами (тегами)
sl_data = s_data;

К указателям на структуры можно также применять арифмети­


ческие операции. Например,
ps__data-i- +; // Эквивалентно
ps_data += s±zeof( stxract STUDENT_INFO ) ;

В языке Си (но не в языке C-I-+) обычно указатель на структуру


используется для передачи адреса структуры в функцию. Это позво­
ляет получить из функции в качестве результата модифицированное
значение структуры. Заметим, что для этой цели в языке C++ лучше
использовать передачу структуры по ссылке.

6.5. Использование указателей в


качестве аргументов функций

Ранее была рассмотрена программа добавления одной строки в


конец другой. Слияние (конкатенация) строк является достаточно
распространенной операцией. Поэтому, почему бы не превратить
эту программу в функцию? Приведем текст такой функции. Функ­
цию, как типовую, поместим в отдельный файл:

Файл STRCAT.CPP
Добавление строки с указателем ps к концу строки с указа­
телем pt
V
/ / Определение функции
void Streat(
char *pt, // Указатель на строку-приемник
char *ps ) // Указатель на строку-источник

{
while ( *pt ) pt + + /
// Здесь pt - адрес завершающего символа строки с

129
// указателем pt, т.е. адрес '\0'
// Посимвольно копируем строку с указателем ps в "хвост"
// строки с указателем pt, пока не будет скопирован
// нуль -символ

while ( ( ±nt ) ( *pt = *ps ) )


{
pt++/ ps++;
}

return;

/^
Файл P24.CPP
Главная функция, использующая функцию strcat^
*/
^Include <stdio.h> // Для функций ввода-вывода
// Прототип
void strcat ( char *, char * ) ;
±Tib main ( void. ) // Возвращает 0 при успехе
{
// Строка-приемник
char t[ 24 ] = "Персональная ";
strcat ( t , "ЭВМ IBM PC" ) ;
printf( "\n Конкатенация строк: %s", t ) ;

return 0;
}

В связи с рассмотренным примером напомним, что имя масси­


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

130
Рассмотрим еще несколько примеров, целью которых является
показать необходимость использования в списке параметров функ­
ций языка Си адресов объектов для получения из функций результа­
тов (сказанное относится только к языку Си).
/*
Файл Р25.СРР
Однофайловый программный проект с двумя функциями. Пример
иллюстрирует возможность появления ошибки из-за отсутствия в
Си передачи в функцию параметра по адресу (ссылке)
V
^include <stdio.h> // Для функций ввода-вывода

void swap ( int, int ) ; // Прототип

int main ( void ) // Возвраш,ает 0 при успехе


{
int к = 10, у = 20;
swap( X, у ) ;
printf( "\п X = %d, у = %d", Xr у ) ;
// Будет напечатано х = 10, у = 20

геЬлт О;
}

// Перестановка значений не будет выполнена, так как


// параметры функции передаются по значению
void swap (
int X,
int у ) // X <--> у
{
int temp = x; x = y; у = temp/
jcetvLm;
}

Обращаем внимание, что будут напечатаны значения л: = 10 и


у = 20. Почему? Потому, что в функцию swap "х" и ' У передаются
по значению, т.е. копии "х" и " у . Следовательно, изменятся только
копии, а после выхода из swap они будут потеряны.
Чтобы избежать этой ошибки в языке Си (это не относится к
языку C++), нужно в функцию swap передавать адреса "х" и "jv" или,
что то же самое, указатели на эти объекты:

Файл Р26,СРР
Однофайловый программный проект с двумя функциями. Пример
иллюстрирует использование в функции Си параметра-адреса
объекта для получения из нее измененного значения оаъекта

131
^include <stdio.h> // Для функций ввода-вывода
// Прототип
void swap ( int *, int *) ;
int main ( void. ) // Возвращает 0 при успехе
{
int X = 10, у = 20;
swap ( &x, &y ) ; // В функцию передаются указатели
print f ( "\n X = %d, у = %d"r ^r У )f
// Теперь будет напечатано x = 20, у = 10
re turn Of
}

// Перестановка значений будет выполнена, так как в качестве


// параметров в функцию передаются адреса объектов
void swap (
int *рх,
int *ру ) // Указатели
{
int temp = *рх; *рх = *ру; *ру = temp;
return;
}

Как уже указывалось ранее, в языке С+4-, в отличие от языка


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

Файл Р2 7.СРР
Однофайловый программный проект с двумя функциями. Пример
иллюстрирует использование в функции языка C++ параметров,
передаваемых по ссылке, для получения из нее измененных зна­
чений объектов
_V
^include <stdio.h> // Для функций ввода-вывода
// Прототип
void swap ( int &, int & ) ;
int main ( void ) // Возвращает 0 при успехе
{
int X = 10, у = 20;

swap ( X, у ) ;

132
printf( "\n X = %d, у = %d"r K, у ) ;
// Будет напечатано x = 20, у = 10
retujcn 0;
}

// Перестановка значений будет выполнена, так как параметры в


// функцию передаются по ссылке
void swap (
±nt &х,
±nt &у )
{
int temp = x; X = у; у = temp;
return;

6.6. Указатель как значение, возвращаемое функцией

Иногда удобно получать от функции в качестве возвращаемого


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

Файл Р28.СРР
Одно файловый программный проект с двумя функциями. Пример
иллюстрирует сцепление нескольких строк с помощью функции ко­
пирования строки, использующей указатели
V

^include <stdio.h> // Для функций ввода-вывода


// Прототип
char * strcpy( char *, char *) ;
±nt main ( void. ) // Возвращает 0 при успехе
{
char t[ 24 ], // Строка-приемник
*pt; // Указатель на конец строки-
// приемника
pt = strcpy ( t, "Персональная" ) ;
// pt = t + 12
//pt = t + 16
pt = strcpy ( pt, " ЭВМ" ) ;
// pt = t + 23
pt = strcpy ( pt, " IBM PC" ) ;
print f ( "\n Конкатенация строк: %s \n", t ) ;

133
return О;
}

/ • "

Копирует строку-источник с начальным адресом ps в строку-


приемник с начальным адресом pt и возвращает указатель на по­
следний символ строки-приемника
*/
char * St г еру (
char *pt^ // Указатель на строку-приемник
char *ps ) // Указатель на строку-источник
{
while ( ( ±nt ) ( *pt = *ps ) )
(
pt++/ ps++;
}

return pt;

6.7. Массивы указателей

Одним из распространенных производных типов данных в Си


является массив указателей. Приведем несколько иллюстрирующих
примеров.

/ / Пример 1: пять указателей на целые значения - массив


// указателей
Int *piarray[ 5 ];

Данное определение трактуется следующим образом:


[] - наивысший приоритет - массив;
* - приоритет ниже [] - указателей;
int - связывается в последнюю очередь - на целые значения
// Пример 2: указатель на массив из пяти целых значений
int ( *p±array ) [ 5 ];

Данное определение трактуется следующим образом:


О - наивысший приоритет, как у [], но выполняется слева
направо - указателъ;
[] - наивысший приоритет как у () , но выполняется слева
направо - на массив;
int - связывается с идентификатором в последнюю очередь
- целых значений
// Пример 3: инициализация массмва указателей на строки
// символов
char *stud_info[ ] =
{

134
"ФТК" ,
"Иванов И. И.
"1081/3",
"01-09-97"
}.

В памяти эти данные будут располагаться так, как это указано


на рис. 42 (значения адресов байтов - условные). Нетрудно заметить,
что при таком определении каждая строка будет занимать мини­
мально необходимую память.

char *studJnfo[ ] Сегмент данных

Адрес Адрес Содержимое

studJnfo[ 0 ] 15000 1- 15000 'Ф'


studJnfo[ 1 ] 15004 L 15001 •т
studJnfo[ 2 ] 15016 V 15002 'К'
studJnfo[ 3 ] 15003 ЛО'
15004 'И' 1
15005 'в'
15006 'а'
15007 'н'
15008 'о'
15009 'в'
15010
15011 'И'
15012
15013 'И'
15014
15015 '\0
1—•
1
Рис. 42. Размещение в памяти

Как организовать доступ к массиву строк?


/ / Печать сформированного в памяти массива строк
^include <stdio,h>
printf( " Факультет: %s \п", stud__lnfo[ О ] ) ;
printf( " ФИО: %s \п", stud_lnfo[ 1 ] ) ;

// Второй символ фамилии заменим на 'р'


*( stud_info[l ] + 1 ) = 'р';

Аргументы командной строки. По-видимому, наиболее рас­


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

135
предположим, что имеется программа prog.exe, которая должна чи­
тать исходные данные из файла prog.dat и писать результаты своей
работы в файл prog.res.
Тогда программу можно запустить из среды MS DOS с помо­
щью следующей командной строки:
prog.ехе prog,dat prog.res [Enter]

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


разделенные пробелами (в данном примере prog.exe, prog.dat и
prog.res). Затем передает функции main в качестве аргументов число
слов в командной строке (в примере 3) и массив указателей на каж­
дое слово (в первом элементе массива указателей находится адрес
"prog.exe'\ во втором — ''prog.daf\ в третьем - "prog.res'\ а значение
четвертого - NULL).

Файл Р29.СРР
Однофайловый программный проект с одной функцией. Пример
иллюстрирует передачу в программу аргументов командной стро­
ки. Программа печатает количество слов в командной строке и
сами слова
*/

#include <stdio.h> // Для функций ввода-вывода


Izit main ( // Возвращает О при успехе
±nt argCr // ARGument Counter: число слов в
// командной строке
сЬа.г *argv[ ] ) / / ARGument Value: массив указателей
// на аргументы командной строки
{
printf ( "\п Число слов в командной строке: %d \п",
а где ) ;
print f ( "\п Передаваемые аргументы: \п" ) ;
unsigned int
i = 0;
while( argv[ i ] )
{
printf( "%s \n", argvf i ++ ] ) ;
}
// Другой вариант
print f ( "\n Переда в a емые аргументы: \n" ) ;
while( *argv )
{
printf( "%s \n", *argv++ ) ;
}
jretixm 0;

136
6.8. Замена типов указателей

Основное применение замены типов указателей связано с


устранением предупреждений в выражениях присваивания.
Рассмотрим следующий иллюстрирующий пример.

Файл РЗО.СРР
Однофайловый программный проект с одной функцией. Пример
иллюстрирует замену типов указателей и производит побайтовое
копирование одной структуры в другую
V

struct STUDENT_INFO // Сведения о студенте


{
// Факультет
char fak_name[ 30 ];
char fio[ 20 ];// ФИО
// Группа
char group__name[ 7 ];
char date[ 9 ];// Дата поступления в университет
float stip/ // Размер стипендии
} s2 = // Определение объекта: источник
{
"ФТК:",
"Иванов И, И,
"1081/4",
"01-09-98",
100000.Of
}.

int main ( void ) // Возвращает О при успехе


{
STUDENT_INFO
si; // Приемник
// Адреса структур si и s2
char *psl = ( char '*' )&sl,
*ps2= (char *) &s2;
// Побайтовое копирование структуры s2 в si
fori unsigned 1 = 0; 1 < slzeof ( STUDENT_INFO ) ; i + + )
{
*psl = *ps2/ psl++; ps2++;
}
// Вообще-то следует иметь в виду, что возможно выражение
// присваивания над структурами: si = s2;
return 0;

137
Операция замены типа {char *) указывает компилятору, что
перед применением надо интерпретировать адрес структуры &s\ как
указатель на символ.
Рассмотрим еще один пример, демонстрирующий мощь и изя­
щество указателей.
/*
Файл Р31.СРР
Однофаиловыи программный проект с одной функцией. Пример
иллюстрирует "хитросплетение ссылок". Что напечатает данная
программа?

^include <std±o.h> // Для функций ввода-вывода


chai: *с[ ] = // Массив указателей на строки
{
"ENTER",
"МЕР",
"POINT",
"FIRST"
};
// Массив указателей на элементы массива указателей на строки
char **ср[ ] = { с+3, с+2, с+1, с } ;
char ***срр = ср;
// См. рис. 43 а
±nt main ( void ) // Возвращает О при успехе
{
printf( "\n%s"r **++срр ) ;
// См. рис. 43 б
printf ( "%s ", *--*-i- + cpp+3 ) ;
// См. рис. 43 в
printf ( "%s", *срр[-2]+3 ) ;
// См. рис. 43 г
printf ( "%s\n", срр[-1] [-!]+! ) ;
// См. рис. 43 д
return О;
}

Перечислим операции, используемые в программе, в порядке


убывания приоритетов:
"[ ]" - выполняются слева направо;
"++", "—", "*" - выполняются справа налево;
"+" - выполняются слева-направо.

138
а) б;

срр I срр [/)


У:

ГР| ПГ"
ГТ"
i О

[т1 F R "
Гз" \0 N
"Т1 ГТ]
\0 \0 \0
*( *( ++СРР ) ) ( *( - ( *( ++СРР ) ) ) ) + 3

1 1
2 2

Операции одинакового
приоритера, выполняются 4
справа налево. 5-
Будет напечатано: Операции одинакового
POINT приоритета, выпол­
няются справа налево.
Будет напечатано:
POINTER
Рис. 43. "Хитросплетение ссылок"

Рассмотрим еще один пример.

Файл Р32.СРР
Однофайловый программный проект с одной функцией. Пример
иллюстрирует работу с массивом с использованием указателей.
Что напечатает данная программа?
V 1

^include <stdlo.h> // Для функций ввода-вывода

int а[ 3 ] [ 3 ] == { { 1, 2, 3 } ,
( 4, 5, 6 Ь
{ 7 , 8 , 9 } } ,

139
int main ( void. ) // Возвращает 0 при успехе
{
£оз:( ±nt i = О; i < 2; i + ч- )
{
pr±ntf( "\n%d %d %d"r a[i][2-i], *a[i],
*(*(a + i)+±) ) ;

jretux-22 0;

d)

'W
срр

ср
1 ^
с
н^^^^
/

/ ( < | \ \г_
"pl гт~ Е N Р F
" Q | гг N Е О 1
"Tl ГЦ Т Р 1 R
\0 " N I ГЦ Е \0 N S
T l ГТ" R Т Т
\0 1 \о 1 \о 1
( *( срр[ -2 ] ) ) + 3 срр[ -1 ] ) [ -1 ] ) + 1
((
I I
1 1 1
1
2
3 г.
Приоритет [ ] выше, 1
чем *. 3 ого
Будет напечатано с Операции одинаков
приоритета, выпол­
учетом предыдущей
няются справа налево.
печати: Будет напечатано с
POINTER ST
учетом предыдущей
печати:
Р OIN"ГЕК STEF3
Прод. рис. 43

Так как a[i] означает адрес первого элемента строки / массива


"(з", то *л[/] есть значение этого элемента. Аналогично, a+i эквива­
лентно &а[/], *{a+i) эквивалентно a[i], *(a-^i)+i эквивалентно a[i]-^i и

140
эквивалентно &а[/][/]. Следовательно, *(*(а+/)-н/) эквивалентно
a[im.
Таким образом, программа напечатает:
3 11
5 4 5

6.9. Упражнения для самопроверки

1. Что напечатает следующая программа?


^include <stdio.h>

±nt main ( void )


{
int a[ ] = { 10, 11, 12, 13, 14, 15, 16 };
±nt i, *p;

fo2:( p = a, i = 0; p + 2*1 <= a + 6; p+ +, i++ )


prlntf( " %3d", *( p + 2*1 ) );
printf ( "\n" ) ;

fojci p = a + 5; p >= a + 1; p -= 2 )
printf ( " %3d", *p ) ;
printf ( "\л" ; /

return 0;

2. Что будет напечатано?


^include <stdio.h>

int main ( voxd )


{
int a[ ] = { 10, 11, 12, 13, 14 };
int *p[] = {a, a + 1, a + 2 , a + 3, a-i-4},
int **pp = p/

pp = pp + 4;
printf ( "pp-p =%3d *pp-a =%3d **pp =%3d\n", pp-p,
*pp-a, **pp ) ;

*pp-~;
printf ( "pp-p =%3d *pp-a =%3d **pp =%3d\n", pp-p,
*pp-a, **pp ) ;

*++pp;
printf( "pp-p ==%3d *pp-a =%3d **pp =%3d\n", pp-p,
*pp-a, **pp );

141
--^рр;
printf( "рр-р =%3d *рр-а =%3d **рр =%3d\n", рр-р,
*рр-а, **рр ) ;
j c e t u i m О;
)

Ответы можно посмотреть в разд. 18.


7. ПОЛЯ БИТОВ И ПОБИТОВЫЕ ОПЕРАЦИИ

7.1. Поля битов

в отличие от других языков высокого уровня в языке C++, как


и в ассемблерных языках, имеется развитый набор средств манипу­
лирования битами. На рис. 44 показано представление символа эк­
рана в видеопамяти:

Номера битов в байтах


7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0
Цвет
Цвет фона Код символа
символа
15 14 13 12 11 10 9 8 7 6 5 4 3 2 1 О
Номера битов в слове
Старший байт Младший байт

Интенсивность символа
Признак мерцания
Рис. 44. Представление символа экрана в видеопамяти

Поля битов объявляются как элементы структуры по правилу:


Спецификация_типа идентификатор: размер поля

В качестве "спецификации типа" задается обычно unsigned int


(для шестнадцати- или тридцатидвухразрядного процессора слово
из 16 или 32 битов), а размер поля - целая константа в диапазоне от
О до 16 или 32. Ниже будет рассматриваться случай с 16-разрядным
процессором.
/ / Представление слова видеопамяти^ представляющего символ на
// экране, в виде структуры с битовыми полями
stxract WORD
{
unsigned int blink: 1; // Мерцание
unsigned, int bkgrd: 3; // Цвет фона
unsigned int in tens: 1; // Интенсивность символа
unsigned int forgrd: 3; // Цвет символа
unsigned int ch : 8; // Код символа
} sd; // Symbol Display

В структуре поля битов можно смешивать с другими элемен­


тами, не являющимися полями битов. Если это происходит, то пер-

143
вый же элемент структуры, не являющийся полем битов, но сле­
дующий за битовым полем, размещается со следующего слова памя­
ти из 16 битов. При этом в предыдущем слове часть битов может
оказаться неиспользованной.
/ / Доступ к отдельным полям битов в структуре и присваивание
// им значений
sd.blink = 1; // Установить мерцание
// Установить цвет символа - три единичных бита
sd,forgrd = 1;
sd.ch = 'А'; // Буква «А» прописная

WORD *psd = &sd;

psd->bkgrd = 3; // Установить цвет фона

Допускаются два специальных объявления поля битов. Можно


объявлять безымянные поля для того, чтобы следующее поле заняло
заданные биты слова:
/ / Использование безымянных полей битов
stiracb CONTROL
{
unsigned int flagl: 1;
unsigned, int s__s : 4;
: 2; // Два неиспользуемых бита
unsigned int flag2: 1;
} ;

Можно также объявлять поля битов нулевой длины. При этом


размещение следующего поля битов начнется с нового слова из 16
битов и, тем самым, в текущем слове будут автоматически оставле­
ны неиспользованные биты. Так можно разделить промежутком два
поля битов.
И еще повторно сделаем важное замечание. В языках Си/С+-1-
не допускаются указатели на поля битов и на массивы полей битов.

7.2. Побитовые операции

Побитовые операции можно применять только к объектам це­


лого и символьного типа. С их помощью можно проверять и моди­
фицировать биты в данных целого и символьного типа. Побитовые
операции перечислены в табл. 21.

Операции Иу ИЛИ, исключающее ИЛИ. Эти операции дейст­


вуют на каждый бит соответствующего операнда (операндов) так,
как это показано в табл. 22. При этом если операнды имеют различ-

144
ные типы, то они перед выполнением операции приводятся к одина­
ковому (старшему) типу по правилам, аналогичным указанным в
подразд. 5.3. С помощью операции "&" удобно проверять и обну­
лять биты, операции "|" - устанавливать биты, а операции "^" - про­
верять несовпадение битов.

Табл. 21. Побитовые операции


Операция Назначение
&- бинарная И
1 - бинарная ИЛИ j
А
- бинарная Исключающее ИЛИ
« - бинарная Сдвиг влево
» - бинарная Сдвиг вправо
~ - унарная Дополнение до единицы

Л"
Табл. 22. Определение операций " & " , "|", '
Бит левого Бит правого Ы&Ьг Ы\Ьг Ы" Ьг
операнда (Ь/) операнда (Ьг)
0 0 0 0 0
1 0 0 1 1
0 1 0 1 1
1 1 1 1 0 i

Операция \: Операция ^: Операция &:


0001001101100011 0001001101100011 0001001101100011
0100001100100001 0100001100100001 0000000000000001

0101001101100011 0101000001000010 0000000000000001

Операция сдвига влево. Формат операции:


операнд « выражение

В результате биты "операнда" будут сдвинуты влево на число


битов, задаваемое значением "выражения". Освобождающиеся спра­
ва биты заполняются нулями. Допустимые значения "выражения"
изменяются в диапазоне от О до 8*5'/zeoy( операнд ).

/*
Файл РЗЗ. СРР
Однофаиловый программный проект с одной функцие й. Пример
иляюстрируе т действие операции сдвига влево (ВС+ + 3. 1)
V

iinclude <stdlo.h> // Для функций ввода-вывода

±nt main ( void. ) // Возвращает О при успехе


{
±nt к == 1;

145
print f( "\n%dr %d, %d, %d, %d, %d, %d, %d", x « I , x«2,
x«3, x«0, x«30r х«-327б8, x«-32161,
x«-32766 ) ;

те turn G;
}

// Будет напечатано 2, 4 , 8 , 1 , О, 1, 2, 4

Нетрудно заметить, что сдвиг битов "операнда" влево на одну


позицию эквивалентен умножению на два. Заметим также, что отри­
цательные значения "выражения" или значения, равные или превы­
шающие число битов в операнде, в общем случае недопустимы и
дают неопределенное значение, зависящее от реализации. Приве­
денный выше пример иллюстрирует особенности реализации языка
Borland C++ версии 3.1 для подобной ситуации.

Операция сдвига вправо. Формат операции:


операнд » выражение

В результате биты "операнда" будут сдвинуты вправо на число


битов, задаваемое значением "выражения". Допустимые значения
"выражения" изменяются в диапазоне от О до S'^sizeofl операнд ).
Операция сдвига вправо выполняется аналогично сдвигу влево, но
отличие состоит в способе заполнения освобождающихся битов:
• если "операнд" беззнаковый, то освобождающиеся биты запол­
няются нулями;
• иначе, если "операнд" знаковый, то освобождающиеся биты за­
полняются знаковым разрядом (нулями для положительного и
единицами для отрицательного "операнда").

Файл Р34.СРР
Однофайловый программный проект с одной функцией. Пример
иллюстрирует действие операции сдвига вправо (ВС++ 3.1)

^include <stdio.h> // Для функций ввода-вывода


int main ( void ) // Возвращает О при успехе
{
int X = 64;
printf( "\n%d, %d, %dr %d, %dr %dr %d, %d"r x » l , x»2,
x»3, x»Or x»30r x»-32768r x»-32767,
x>>-32 766 ) ;

return 0;

146
}

// Будет напечатано 32, 16, 8, 64, О, 64, 32, 16

Аналогично предыдущему случаю заметим, что сдвиг битов


"операнда" вправо на одну позицию эквивалентен делению на два.
Обратите внимание также, что отрицательные значения "выраже­
ния" или значения, равные или превышающие число битов в опе­
ранде, в общем случае недопустимы и дают неопределенное значе­
ние, зависящее от реализации. Приведенный выше пример показы­
вает особенности реализации языка Borland С+-ь версии 3.1 для по­
добной ситуации.

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


битов операнда на противоположные значения:
-выражение

// Рассмотрим пример
char с, d;
с = с & 'OxlF'; // Обнулить (маскировать) старший
// бит
с = с & (- ' 0x7F) '; // Маскировать все биты, кроме
// старшего

Операции присваивания. Если бинарные побитовые операции


используются в операторах присваивания вида
value = value побитовая_операция выражение

то можно использовать сокращенную запись (табл. 23):


value побитовая_операция= выражение

// Обычная форма Сокраш,енная форма


с = с & '0x7F'; с &= '0x7F'/

Приоритеты побитовых операций и порядок их выполнения


рассмотрены в табл. 16.

Табл. 23. Сокращенная запись побитового присваивания


Операция Назначение
&= Операция "И" и присваивание
1= Операция "ИЛИ" и присваивание
Л=:
Операция "^" и присваивание i
~= Дополнение до единицы и присваивание
«= Сдвиг влево и присваивание
»= Сдвиг вправо и присваивание
8. Д И Н А М И Ч Е С К О Е РАЗМЕЩЕНИЕ ОБЪЕКТОВ В
ПАМЯТИ.
ОДНОНАПРАВЛЕННЫЙ НЕКОЛЬЦЕВОЙ
Л И Н Е Й Н Ы Й СПИСОК И ОПЕРАЦИИ С НИМ

8.1. Понятие об однонаправленном линейном списке.


Динамическое размещение объектов в памяти

Сущность однонаправленного линейного списка (ЛС), предна­


значенного для хранения символов, представлена на рис. 45.

Т' 'е' 'к' 'с' 'т'

W W W W NULL

I start
Рис. 45. Однонаправленный линейный список

Каждый элемент ЛС содержит две части (два поля):


• основное поле, в котором хранится содержательная информация
(в нашем примере - символ);
• вспомогательное поле, в котором хранится указатель на следую­
щий элемент линейного списка.
Список содержит два элемента, которые являются особенны­
ми, отличными от других. Это первый (головной) элемент ЛС - его
особенность состоит в том, что он снабжен указателем (на рис. 45
таким указателем является Start), Без этого указателя нельзя рабо­
тать с линейным списком. Последний элемент ЛС также является
особенным, так как в его вспомогательном поле хранится указатель
NULL, означающий, что следующего элемента нет.
Из сказанного следует, что элемент ЛС в терминах языка С++
можно определить в виде структуры:

stxract EL ЕМ
{
char ch; // Основное поле: символ
ELEM *neKt; // Указатель на следующий элемент
} ;

ELEM *pe; // Указатель на структуру

148
в языках Си/С++ имеется возможность динамического разме­
щения некоторого объекта в оперативной памяти (функция malloc{ )
и др. в библиотеке языка Си, оператор new языка СН-+) или освобо­
ждения занятой ранее динамической памяти (функция free{ ) в биб­
лиотеке языка Си, оператор delete языка C++):

Пример размещения элемента линейного списка в динамической


памяти. Среда языка Си

^include <malloc .h> /* Для функции та Нос */


^include <stdio.h> /* Для функций ввода-вывода */
^include <stdlib.h> /'*' Для функции exit */

ре = ( struct ELEM * ) malloc ( sizGof ( struct ELEM ) ) ;


±f( pe == NULL ) /* Обработка результата размещения"^/
{
printf ( "\n Размещение элемента в динамической памяти не"
" выполнено " );
exit( 1 );
}
. . . pe->ch ... /'*' Значение поля данных структуры */
. . . pe->next ... /* Указатель на следующий элемент '^/
/* линейного списка */

Пример размещения элемента линейного списка в динамической


памяти. Среда языка C++
*/

^include <stdio.h> // Для функций ввода-вывода


#include <stdlib.h> // Для функции exit
ре = new ELEM;
±f( ре == NULL ) // Обработка результата размещения
{
printf ( "\п Размещение элемента в динамической памяти "
"не выполнено ") ;
exit ( 1) /
}
. . . pe->ch ... / / Значение поля данных структуры
. . . pe->next ... / / Указатель на следующий элемент
// линейного списка

/*
Пример освобождения динамической памяти^ занятой элементом
линейного списка. Среда языка Си
• " /

finclude <malloc.h> /* Для функции free */

149
±f( ре != NULL )
{
free ре; ре == NULL;
I

/*
Пример освобождения динамической памяти. занятой элементом
линейного списка. Среда языка С++
*/
±f( ре != NULL )
{
del&te ре; ре = NULL;
}

Рассмотрим еще один практически значимый пример разме­


щения в динамической памяти двумерного массива (матрицы) и ос­
вобождения занятой памяти.
/*
Файл DynMem, срр
Демонстра ция работы с ма трицеи в динамической памяти
*/
^include <stdio.h> // Для ввода-вывода
^include <stdlib.h> // Для exit ( )

// Ввод размеров матрицы, размещение матрицы в динамической


// памяти и заполнение ее *
srold ReadMatrix (
chetr *pFileInp,// Указатель на файл данных
unsigned &RowSize, // Строчный размер
unsigned &ColSize, // Столбцовый размер
int **&рМх ) // Указатель на матрицу
{
// Указатель на структуру со сведениями о файле данных
FILE *pStructInp;

// Открытие файла данных для чтения


if( ( pStructlnp = fopeni pFilelnp, "г" ) ) == NULL )
{
printf( "\n Ошибка 10, Файл %s для чтения не "
"открыт \п", pFilelnp );
jretuarn/

// Чтение размеров матрицы


int retcode = fscanf( pStructlnp, "%u %u",
ScRowSize, &ColSize ) ;
if( retcode != 2 )
{
printf( "\n Ошибка 20. Ошибка чтения размеров"

150
" матрицы \п" )/
return;
}

// Размещение в ДП массива указателей на строки матрицы


рМх = new ±nt * [ RowSize ];
±£( !рМх )
{
printf ( "\п Ошибка 30. Ошибка размещения в ДП "
"массива указателей на строки матрицы \п" ) /
return;
}

// Размещение в ДП строк матрицы


£ог( unsigned int 1=0; l<RowSize; 1++ )
{
рМх [ 1 ] = new int [ Col Size ];
i£( !pMx[ 1 ] )
{
printf( "\n Ошибка 40. Ошибка размещения в ДП"
" строки матрицы \п" ) ;
return;
}
}

// Заполнение матрицы
£ог( 1=0; KRowSlze; 1 ++ )
{
£ог ( unsigned int j = 0; j<ColSlze; j++ )
{
retcode = fscanf( pStructlnp^
" %1" r &pMx[l ][ j ] ) ;
i£( retcode != 1 )
{
printf( "\n Ошибка 50. Ошибка чтения "
"элемента ма трицы \п" ) ;
return;
)
}
}

fclose ( pStructlnp );// Закрытие файла данных

return;
;
int maln( void ) // Возвращает О при успехе
{
int "^^рМх; // Указатель на матрицу
unsigned г, // Число строк
с; // Число столбцов
// Ввод матрицы
ReadMatrlx ( "DynMem. Inp", г, с, рМх ) ;

151
/ / Здесь проверяем г, с , рМх
// . ..
// Освобождение динамической памяти, занятой матрицей
// Освобождение ДП, занятой строками матрицы
for( unsigned ±nt i=0; Кг; i++ )
{
delete [ ] pMx[ i ];
}
// Освобождение ДП, занятой массивом указателей на строки
// матрицы
delete [ ] рМх;
ret-arn 0;
}

Для работы с Л С используются следующие основные опера­


ции:
• инициализация;
• добавление элемента в начало ЛС;
• добавление элемента в конец ЛС;
• создание ЛС таким образом, чтобы первый занесенный элемент
оказался в начале списка;
• создание ЛС таким образом, чтобы последний занесенный эле­
мент оказался в начале списка;
• удаление элемента из начала ЛС;
• удаление элемента из конца JIC\
• разрушение ЛС с освобождением занятой им динамической памя­
ти;
• печать содержимого ЛС;
• добавление или удаление элемента после каждого элемента ЛС,
содержащего заданное значение;
• добавление или удаление элемента перед каждым элементом ЛС,
содержащим заданное значение.
Реализация перечисленных операций неоднозначна. Рассмот­
рим один из возможных вариантов программирования операций над
ЛС, который представляется более удачным.

8.2. Инициализация линейного списка

Эта операция является тривиальной и заключается в присваи­


вании указателю на начало линейного списка значения NULL^ озна­
чающего, что ЛС пуст. Инициализация списка выполняется самой

152
первой. Прототип функции Initls^ выполняющей инициализацию
ЛС, ее определение и пример вызова приведены ниже. На данном
этапе рекомендуем из примера рассмотреть только часть програм­
мы, предшествующую главной функции, и все, что относится к
функции Initls, Остальной материал будет рассмотрен далее.

Файл LS.CPP
Работа с динамической памятью. Однонаправленный линейный
список и операции с ним

^include <stdio.h> // Для функций ввода-вывода


^include <stdlib.h> // Для функции exit

struct EL // Структура для элемента списка


{
char ch; // Данные (символ)
EL *next; // Указатель на следующий элемент
} ;

// Указатель на начало списка: класс хранения внешний


// (область действия и время жизни - вся программа),
// используется во всех функциях программного проекта и
// через список параметров функций не передается
EL * start;

// Прототипы функций
void Init_ls ( void. ) ;
void Dest^^ls ( void ) ;
void Add_end ( cha.r с ) ;
void Add__beg ( cbstr с ) ;
void Del_end ( void ) ;
void Del_beg ( void ) ;
void Create_end ( void ) ;
void Create__beg( void ) ;
void Print_ls ( void ) ;
void After_Add( char find,, сЪа.г add ) ;
void Before__Add ( char find, char add ) ;
void After_Del ( char find ) ;
void Before_Del ( char find ) ;

int main( void ) // Возвращает 0 при успехе


{
Init_ls ( ) ; // Инициализация списка
// Заполнение линейного списка символами из файла LS.DAT:
// первый прочитанный символ - в начале списка
Сгеаte_beg( ) ;
Print__ls ( ) ; // Вывод содержимого списка на экран
Dest__ls ( ) ; // Разрушение списка
After_Del ( '3'); // Удаление после '3'
Print Is( ) ;

153
Add end( 'С ), // Добавление в конец списка
// элемента 'С'
After_Del ( '3'); // Удаление после '3'
Pr±nt_ls ( ) ;
Dest_ls ( ) ; // Разрушение списка
// Заполнение линейного списка символами из файла LS.DAT:
// последний прочитанный символ - в конце списка
Create_end( ) ;
Print Is( ) ;
Dest_ls ( ) ; // Разрушение списка
Add__end ( ' C ) ; // Добавление в конец списка
// элемента 'С'
Add_end ( 'D' ) ; // Добавление в конец списка
// элемента 'D'
Add beg( ^B^ ); // Добавление в начало элемента 'В
Add_beg ( 'A' ); // Добавление в начало элемента 'А
Print Is (r ) ;
Del_end( ); // Удаление последнего элемента
// списка
Del_beg( ); // Удаление первого элемента спи СК(
Print_ls ( ) ;
Dest_ls ( ) / // Разрушение списка
// Заполнение списка пятью двойками
for( Int i = 1; i <= 5; i-^+ )
Add__end( '2' ) ;
Print_ls ( ) ;
// Добавление '1 ' перед
Before_Add( ' 2 ' , Ч' ) ;
Print_ls ( ) ;
// Добавление '3 ' после
After_Add( ' 2 ' , '3' ) ;
Print_ls ( ) ;
// Добавление '1 ' перед
Before_Add( ' 1 ' , Ч' ) /
Print_ls ( ) ;
// Добавление '2 ' после
After_Add( ' 2 ' , '2' ) ;
Print_ls( ) ;
// Добавление '2 ' после
After_Add( ' 2 ' , '2' ) ;
Print_ls( ) /
// Добавление '3 ' после
After__Add( ' 3 ' , '3' ) /
Print_ls ( ) ;
After_Del ( '3'); // Удаление после
Print_ls ( ) ;
Before_Del( '1') // Удаление перед '1
Print_ls( ) ;
Dest_ls ( ) ; // Разрушение списка

return 0;

// Инициализация списка

154
void Init_ls ( void )
{
start = NULL; // Вначале список пуст
rebvLrn;
}

// Paзрушение списка
void Dest_ls ( void )
{
if( start == NULL )
{
printf( "\n Список пуст. Удалять нечего" ) ;
retuxm/
}

while ( start /- NULL )


Del__beg ( ) ; // Удаление первого элемента списка

return/
}

// Добавление элемента в конец списка


void Add_end (
char с ) // Данные добавляемого элемента
{
// Указатель на новый (добавляемый) элемент списка
EL *temp,
*сиг; // Указатель на текущий элемент
temp = new EL; // 1: динамическое размещение
// элемента
if( temp == NULL )
{
printf( "\n Элемент списка не размещен" ) ;
exit ( 1 ) ;
}
temp->ch = с; // 2: занесение данного
temp->next = NULL; // 3: новый элемент является
// последним
if( start == NULL ) // Новый список (пустой)
start = temp; // 4а: указатель на начало списка
else
{
// 46: проходим весь список от начала, пока текущий
// элемент не станет последним
сиг = start;
while( cur->next != NULL )
// Продвижение по списку
сиг = cur->next;
// 4в: ссылка последнего элемента на новый,
// добавляемый в конец списка

155
cur~>next = temp;
}

return/
}

// Добавление элемента в начало списка


void Add_beg(
char с ) // Данные добавляемого элемента
{
// Указатель на новый (добавляемый) элемент списка
EL *temp;
temp = new EL; // 1: динамическое размещение
// элемента
±f( temp == NULL )
(
printf( "\n Элемент списка не размещен" ) ;
exit ( 2 ) ;
}
temp->ch = с/ // 2: занесение данного
// 3: новый элемент ссылается на начало списка
temp->next = start;
start = temp; // 4: новый элемент становится
// первым
return;
}

// Заполнение линейного списка символами из файла LS.DAT:


// первый прочитанный символ - в начале списка
void Create^beg( void )
{
// Данное для элемента, добавляемого в конец списка
сЪаг с;
// Указатель на структуру со сведениями о файле для
// чтения
FILE *f__in;
// Открываем файл для чтения
±£( ( f_in = fopeni "Is.dat", "г" ) ) == NULL )
{
printf( "\n Файл Is.dat для чтения не открыт " ) ;
exit ( 3 ) ;
}
// Создаем список
while ( ( с = ( сНаг )fgetc( f_in ) ) != EOF )
{
Add_end( с ) ;
}
// Закрываем файл
i£( ( fclose( f in ) ) == EOF )

156
{
printf ( "\n Файл Is.dat не закрыт " ) ;
exi t ( 4 ) ;
}

return;
}

// Заполнение линейного списка символами из файла LS.DAT:


// последний прочитанный символ - в начале списка
void Create_end ( void, )
{
// Данное для элемента^ добавляемого в конец списка
сЬа.г с;
// Указатель на структуру со сведениями о файле для
// чтения
FILE *f_in;
// Открываем файл для чтения
±£( ( f__in = fopen( "Is.dat", "г" ; ; == NULL )
{
print f ( "\n Файл Is.dat для чтения не открыт " ) ;
exit(5);
}

// Создаем список
while ( ( с = ( char )fgetc( f_in ) ) != EOF )
{
Add_beg ( с ) ;
}
// Закрываем файл
±f( ( fclose( f_in ) ) == EOF )
{
printf( "\n Файл Is.dat не закрыт " ) ;
exit(6);
}
rebum;
}

/ / Удаление последнего элемента списка


void Del_end ( void )
{
EL *prev, // Указатель на предпоследний
// элемент
*end/ // Указатель на последний элемент
i£( start - = NULL )
{
printf ( "\n Список пуст. Удалять нечего" ) ; retjim;
}
// 1: поиск последнего (и предпоследнего) элементов

157
prev = NULL; end = start;
while( end->next /= NULL )
{
prev = end;
end = end->next; // Продвижение no списку
}
d&lete end; // 2: удаление последнего элемента
±f( prev != NULL )
{
// 3: бывший предпоследний элемент становится
// последним
prev~>next = NULL;
}
else
(
start = NULL; // 3: в списке был один элемент
}

return;
}

// Удаление первого элемента списка


void Del_beg ( void. )
{
EL *del; // Указатель на удаляемый элемент
if( start == NULL )
{
printf( "\n Список пуст. Удалять нечего" ) ;
return;
}
// 1: подготовка первого элемента для удаления
del = start;
start = del->next; // 2: start сдвигается на второй
// элемент
delete del; // 1: удаление первого элемента
return;
}

// Печать содержимого списка на экран


void Print_ls( void )
{
EL *prn; // Указатель на печатаемый элемент
i£( start == NULL )
{
printf( "\n Список пуст. Распечатывать нечего" ) ;
return;
}
prn = start; // Указатель на начало списка
print f ( "\п" ) ;

158
while ( prn != NULL ) // До конца списка
{
// Печать данных (символа) элемента
printf( " % с " , prn->ch ) ;
prn = prn~>next; // Продвижение по списку
}

return/
}

// Добавление элемента add после элемента find


void After_Add ( char find, char add )
{
EL *temp, // Указатель на добавляемый элемент
*cur; // Указатель на текущий элемент
if( start == NULL )
{
printf( "\n Список пуст. Нельзя найти нужный "
"элемент " ) /
return/
}
// Поиск элементов, содержащих символ find, с добавлением
// после них элемента с символом add
сиг = start/
while ( cur != NULL )
{
if( cur->ch == find )
{
// Нужный элемент найден (он является текущим)
// 1: динамическое размещение элемента
temp = new EL/
if( temp == NULL )
{
printf( "\n Элемент списка не размещен" ) /
exit ( 7 ) /
}
// 2: занесение данного
temp->ch = add/
// 3: ссылка на элемент, который стоял за текущим
temp->next = cur->next/
// 4: текущий элемент указывает на новый
cur->next = temp/
// 5: новый элемент становится текущим
сиг = temp/
)
сиг = cur->next/ // Продвижение по списку
}

return/

/ у i^:h*iir-^-k-^irTlf*^Th-^-^9r-^ir*i^-k*-*c***Thilc-^*-ki^-k-k*-!h***T^

159
// Добавление элемента add перед элементом find
void Before_Add ( char find, cba.r add )
{
EL *temp, // Указатель на добавляемый элемент
*cur, // Указатель на текущий элемент
"^prev; // Указатель на элемент стоящий
// перед текущим)
±f( start =- NULL )
{
printf( "\n Список пуст. Нельзя найти нужный "
"элемент " ) ;
jretujrn/
;
// Поиск элементов, содержащих символ find, с добавлением
// перед ними элемента с символом add
сиг = start;
while ( cur /- NULL )
{
// Нужный элемент найден (он является текущим)
±f( cur->ch == find )
{
// 1: динамическое размещение элемента
temp = new EL;
if( temp == NULL )
{
printf( "\n Элемент списка не размещен" ) ;
exit ( 8 ) ;
}
// 2: занесение данных
temp->ch = add;
// 3: новый элемент указывает на элемент с
// символом find
temp->next = cur;
// 4: если элемент с символом find был первым,
// то start смещается влево (на новый элемент)
±£( сиг == start )
start = temp;
else
// 4: элемент, стоящий перед сиг указывает на
// новый
prev->next = temp;
}
prev = cur; // Продвижение текущего и
// предыдущего элементов
сиг = cur->next; // по списку
}

return;
}

// Удаление элемента после элемента find


void After Del( char find )

160
EL *del, // Указатель на удаляемый элемент
*сиг; // Указатель на текущий элемент
±£( start == NULL )
{
printf( "\n Список пуст. Нельзя найти нужный "
"элемент " ) ;
jr&bVLJCZi;
}

±f( start->next == NULL )


{
printf ( "\n В списке только один элемент. Нельзя "
"выполнить данную операцию" ) ;
return/
}
// Поиск элементов,, содержащих символ find,, с удалением
// элементов г следуюшр1Х за найденными
сиг = start;
do
{
'±f( cur->ch == find )
( // Нужный элемент найден (он является текущем)
// 1 : указатель на элемент для удаления
del = cur->next;
// 2: связь текущего элемента с элементом,
// следуюш;им за удаляемым
cur->next = del->next;
delete del; // 3: удаление элемента
// 4: является ли теперь текущей элемент
// последним? Если "да" - выход из цикла и
// функции
±f( cur->next == NULL )
геЬит;
}
cur = cur->next; // Продвижение по списку
} while( cur->next != NULL ) ;
return;
}

// Удаление элемента перед элементом find


void. Before_Del ( chstr find )
{
// Указатель на предпредыдуш:ий элемент по отношению к
// заданному
EL '*'pprev,
'*'dei, / / Указатель на удаляемый элемент
*сиг; // Указатель на текущий элемент
lf( start == NULL )
{

161
printf( "\n Список пуст. Нельзя найти нужный "
"элемент " ) ;

}
±£( start->next == NULL )
{
printf( "\n в списке только один элемент. Нельзя "
"выполнить данную операцию" ) ;
return/
}
// Поиск элементов, содержащих символ find, с удалением
// элементов, предшествующих найденным
pprev = NULL/ cur = start->next/
while( cur != NULL )
(
±f( cur->ch == find )
{ // Нужный элемент найден (он является
// текущим)
// Найденный элемент является вторым в ЛС
±f( pprev == NULL )
{
// 1: будем удалять головной элемент
del = start/
// 2: первым элементом списка теперь будет
// бывший второй
start = start->next/
}
else
{
// 1: указатель на удаляемый элемент
del = pprev->next/
// 2: связь предпредыдущего и текущего
// элементов
pprev->next = del->next/
}
delete del/ // Удаление элемента
}
else
{ // Продвижение указателя pprev требуется, если на
// очередном шаге не было удаления элемента
±f( pprev == NULL )
pprev = start/
else
pprev = pprev->next /
}
cur = cur->next/ // Продвижение этого указателя
// требуется всегда
}
retujm/
}

Файл LS.DAT, из которого программа читает исходные дан­


ные, имеет следующий вид:

162
1234567890

Результаты выполнения программы выдаются на экран и име­


ют вид:
1234567890
Список пуст. Нельзя найти нужный элемент
Список пуст. Распечатывать нечего
В списке только один элемент. Нельзя выполнить данную операцию
С
0987654321
ABCD
ВС
22222
1212121212
123123123123123
11231123112311231123
1122311223112231122311223
11222231122223112222311222231122223
1122223311222233112222331122223311222233
11222231122223112222311222231122223
12222122221222212222122223

8.3. Добавление элемента в начало списка

Эта операция имеет не только самостоятельное значение, но и


может быть использована для создания линейного списка, когда по­
следний занесенный элемент будет находиться в начале ЛС.
Смысл операции иллюстрирует рис. 46. Прототип функции
Add beg, выполняющей добавление элемента в начало списка, ее
определение и пример вызова содержатся в вышеприведенном при­
мере. На данном этапе также рекомендуем из примера рассмотреть
только часть программы, которая относится к функции Addjbeg. Ос­
тальной материал рассмотрим далее, применяя указанный подход.

8.4. Добавление элемента в конец списка

Эта операция, как и предыдущая, имеет, как самостоятельное


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

163
До выполнения операции
Общий случай Особый случай

Start = NULL;
• h> • W NULL

t Start
После выполнения операции
2: занесение данного «с» 2 занесение данного «с»

3: temp->next =
•—Н NULL NULL
Start;

t t
1: динамическое размещение 1. динамическое размещение
temp = new EL; temp = new EL;
t t
4: Start = temp; 4: Start = temp;
Рис. 46. Добавление элемента в начало линейного списка

8.5. Создание ЛС с первым занесенным


элементом в начале

Для создания ЛС с первым занесенным элементом в начале


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

8.6. Создание ЛС с первым занесенным элементом


в конце списка

Эта операция аналогична предыдущей. Для создания ЛС с пер­


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

164
До выполнения операции
Общий случай Особый случай

Start = NULL,
•—h> NULL

t Start
После выполнения операции
2: 2: занесение данного « о

4в: 3: temp->next =
NULL NULL
# •^ NULL;

t Start t t
1- 1-
динамическое размещение temp = new EL;
t t
46: cur
4B: cur->next = temp; 4a: Start = temp;
Рис. 47. Добавление элемента в конец списка

8.7. Удаление элемента из начала списка

Данная операция, также как и операции добавления элемента в


начало или конец линейного списка, позволяет решить две задачи:
• удаление одного элемента из начала ЛС;
• разрушение линейного списка с освобождением занятой им ди­
намической памяти путем циклического выполнения операции
удаления элемента из начала списка.
Реализация операции, представленная в графической форме,
дана на рис. 48. Прототип функции Del beg, выполняющей удаление
первого элемента списка, ее определение и пример вызова содер­
жатся в программном проекте, приведенном в подразд. 8.1.

165
До выполнения операции
Общий случай Особые случаи:
а) в ЛС один элемент б)

Start = NULL;
• — — • ! •—^> • • ^ NULL NULL
I I

Start t Start

После выполнения операции

Вывод сообщения
и возврат из
NULL функции
• ^
t
1: del = Start; 1: del = Start;

t t
2: Start = del->next; 2: Start = del->next = NULL;
3: delete del; 3- delete del;
Рис. 48. Удаление элемента из начала списка

8.8. Удаление элемента из конца списка

Реализация операции, представленная в графической форме,


дана на рис. 49. Прототип функции Delend, выполняющей удаление
последнего элемента списка, ее определение и пример вызова со­
держатся в программном проекте, приведенном в подразд. 8.1.

8.9. Разрушение ЛС с освобождением занятой


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

166
До выполнения операции
Общий случай Особые случаи:
а) в ЛС один элемент б)

Start = NULL;
•—и • — — • NULL 1 I NULL

t Start t Start

После выполнения операции

Вывод сообщения
L__ и возврат из
NULL функции

t t t t
start 1: prev end 1: prev = NULL;
end
2: delete end; 2: delete end;
3 prev->next = NULL; 3: Start = NULL;
Рис. 4 9 . Р а з р у ш е н и е л и н е й н о г о с п и с к а

8.10. Печать содержимого ЛС

Печать содержимого линейного списка может быть выполнена


путем циклического выполнения печати содержимого текущего
элемента списка и продвижения по списку от начала до конца. Опе­
рация тривиальна и не требует особых пояснений.
Прототип функции P r i n t l s , выполняющей печать содержимого
линейного списка на экран, ее определение и пример вызова содер­
жатся в программном проекте, приведенном в подразд. 8.1.

8.11. Добавление элемента после каждого элемента


ЛС, содержащего заданное значение

Реализация операции, представленная в графической форме,


дана на рис. 50.
Прототип функции AfterAdd, выполняющей добавление эле­
мента с данным add после каждого элемента ЛС, содержащего за­
данное значение y?«(i, ее определение и пример вызова содержатся в
программном проекте, приведенном в подразд. 8.1.

167
До выполнения операции
Общий случай Особые случаи

find
Start = NULL,
w
NULL
,, .^
t Start
После выполнения операции
2:
Вывод
find add
сообщения и
4: 3: возврат из
W • w NULL
^ функции

t t t
Start cur 1: temp = new EL;
2: temp->ch = add;
3: temp->next = cur->next,
4. cur->next = temp;
t 5: cur = temp;
Рис. 50. Добавление элемента после каждого элемента ЯС,
содержащего заданное значение

8.12. Добавление элемента перед каждым элементом


ЛС, содержащим заданное значение

Реализация операции, представленная в графической форме,


дана на рис. 5 1 .
Прототип функции BeforAdd, выполняющей добавление эле­
мента с данным add перед каждым элементом ЛС, содержащим за­
данное значение find, ее определение и пример вызова содержатся в
программном проекте, приведенном в подразд. 8.1.

8.13. Удаление элемента после каждого элемента ЛС,


содержащего заданное значение

Реализация операции, представленная в графической форме,


дана на рис. 52.

168
До выполнения операции
Общий случай Особые случаи

find a) Start = NULL;


6) cur = Start;
•—ь> NULL

t Start
После выполнения операции
2:
a) Вывод
add find сообщения и
4: 3: возврат из
• \-> •• W
' W NULL функции
—•
б) См.
t prev t cur
реализацию
шага 4
start
1: temp = new EL;
2: temp->ch = add;
3: temp->next = cur;
T 4: Start = temp при cur = Start
или prev->next = temp в остальных
случаях

Рис. 51. Добавление элемента перед каждым элементом ЛС,


содержащим заданное значение

Прототип функции After_Del, выполняющей удаление элемен­


та после каждого элемента ЛС, содержащего заданное значение find,
ее определение и пример вызова содержатся в программном проек­
те, приведенном в подразд. 8.1.
При реализации операции удаления производится просмотр
элементов линейного списка, начиная с первого элемента до пред­
последнего элемента списка.
Отличительной особенностью операции является то, что если
после удаления элемента текущий элемент является последним, то
необходимо выйти из цикла просмотра элементов и из данной функ­
ции. Тем самым предотвращается ошибка, связанная с выполнением
продвижения в ЯС на следующий элемент сиг — cur->next и после­
дующей проверкой условия повтора цикла cur->next != NULL.

169
8.14. Удаление элемента перед каждым элементом ЛС,
содержащим заданное значение

Реализация операции, представленная в графической форме,


дана на рис. 53.

До выполнения операции
Общий случай Особые случаи

find а) Start = NULL,


б) Start->next =
^ NULL
w w NULL,
1
t start
После выполнения операции
а) Вывод
find сообщения и
2: возврат из
w
NULL функции
w w
б) Вывод
t t t сообщения и
start возврат из
cur 1 • del = cur->next;
функции
2: cur->next = del->next;
3. delete del;
4: если после удаления текущий элемент -
последний, то выполняется выход из
функции

Рис. 52. Удаление элемента после каждого элемента ЛС,


содержащего заданное значение

Прототип функции B e f o r D e l , выполняющей удаление элемен­


та перед каждым элементом ЛС, содержащим заданное значение
find, ее определение и пример вызова содержатся в программном
проекте, приведенном в подразд. 8.1,
При реализации операции удаления производится просмотр
элементов линейного списка, начиная со второго элемента до по­
следнего элемента списка.
Отличительной особенностью операции является то, что если
удаление элемента выполнено, то продвижение указателя на пред-
предыдущий элемент не требуется. Указатель же на текущий эле­
мент должен продвигаться всегда.
Другой особенностью операции является то, что реализация
шагов 1: и 2: в случае, когда текущий элемент является вторым в
ЛС, отличаются (см. рис. 53).

170
8.15. Зачем нужен линейный список
Для хранения и обработки информации наряду с ЛС можно
использовать и массивы. Например, вместо ЛС, приведенного на
рис. 45, можно использовать символьный массив из пяти элементов,
причем это сэкономило бы оперативную память. Значит ли это, что
ЛС не следует использовать? Конечно же, нет!

До выполнения операции
Общий случай Особые случаи

find а) Start = NULL,


б) Start->next =
,^ NULL NULL,
"• ... ^ w

t Start
После выполнения операции
а) Вывод
find сообщения и
2: возврат из
NULL функции
w w
б) Вывод
t t t t сообщения и
возврат из
start pprev cur
функции
1 • del = pprev->next или
del = Start, если текущий
элемент в ЛС второй
2: pprev->next = del->next или
Start = Start->next, если текущий
элемент в ЛС второй
3: delete del,

Рис. 53. Удаление элемента перед каждым элементом ЛС,


содержащим заданное значение

Например, использование ЛС обеспечивает следующие пре­


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

171
на наихудший случай).

8.16. Упражнения для самопроверки


Определены следующие данные:
stjract ELEM // Структура для элемента списка
{
±nt dat; // Данное
struct ELEM
*next; // Указатель на следующий элемент
} *сиг, // Указатель на текущий элемент
// списка
*start/ // Указатель на начало списка

Во входном файле Is.dat содержится некоторое количество це­


лых чисел, разделенных символами пробельной группы ( ' ', '\^', '\«' ).

1. Написать прототип, определение и пример вызова функции


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

2. Дополнительно написать прототип, определение и пример


вызова функции, которая в процессе просмотра списка выводит
данные (числа) в файл на магнитном диске is.out. Требования к
оформлению функции и обработке ошибок аналогичны указанным в
п. 1 требованиям.

3. Дополнительно написать прототип, определение и пример


вызова функции, которая разрушает линейный список. Требования к
оформлению функции и обработке ошибок аналогичны указанным в
пункте 1 требованиям.
9. ПРЕПРОЦЕССОР ЯЗЫКА Си/С++

Перед собственно компиляцией программы к ней применяется


процедура предварительной обработки. Она выполняется програм­
мой, называемой препроцессором:
ПРЕдварительный ПРОЦЕССОР

Препроцессор расширяет возможности языка следующим.


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

9.1. Директивы препроцессора

Указания препроцессору вставляются в программу в виде ди­


ректив. Директивой служит строка, в первой позиции которой ука­
зан символ диеза "#". Допускается, хотя и не рекомендуется, нали­
чие предшествующих символу диеза пробелов и табуляторов. За
символом диеза следует название директивы. Между ними допуска­
ется, хотя и не рекомендуется, произвольное число пробелов и/или
табуляторов.

Директивы можно размещать в любом месте программы и их


действие остается в силе вплоть до конца того файла, в котором они
находятся.

9.2. Подстановка имен

Подстановка имени (макроопределение) представляет собой


символическое имя, которое присваивается фрагменту программы
(строке элементов языка). Когда впоследствии препроцессор обна-

173
руживает это символическое имя в программе, то он заменяет имя
соответствующей строкой.
Для подстановки имен предусмотрены две директивы препро­
цессора:
• создать макроопределение (Ude/ine);
• удалить макроопределение, т.е. сделать его неопределенным
{Uundef).
Пример использования директивы Udefine приведен на рис. 54.

Признак директивы - его рекомендуется размещать


в начале строки. Допускаются, но не рекомендуются,
предшествующие пробелы и/или табуляторы

Служебное слово препроцессора.

По крайней мере один пробел или табулятор.

#define NULL ЛО'

I — Конец строки - завершает макроопределение


(макроопределение должно размещаться в одной
строке).

Текст макроопределения - любое число символов в


пределах одной строки.

По крайней мере один пробел или табулятор

Имя макроопределения - любой идентификатор Для


более легкого «узнавания» макроопределения
рекомендуется использовать в нем только прописные
буквы.

Между знаком диеза и служебным словом


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

Рис. 54. Структура директивы на примере директивы Udeflne

Приведем несколько примеров:


#define SUCCES 1
^define NULL '\0'
^define MAX_SIZE 50
^define UNIX // Пустое макроопределение
^define printf myprintf

Приведенные примеры показывают, что, как указывалось вы­


ше, запись имени макроопределения прописными буквами не обяза-

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

После определения имени макроопределения всякое его вхоэю-


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

Табл. 24. Примеры замещения макроопределений


До препроцессора После препроцессора
if (scanner 0 = =SUCCES)pnntf("Cmon\n "); if (scanner 0==l)myprintf("Cmon \n ");
struct INDEXJNFO index[MAX_SIZE]; struct INDEX JNFO index[50];
ifOine[pos]=-^NULL) if(line[pos]=='\0')
printf("3mo NULLW): myprintf("3mo NULL\n"):

Обратите внимание, что строка NULL внутри строковой кон­


станты не подверглась замене.
Для отмены макроопределения можно воспользоваться
директивой Uundef, действие которой иллюстрирует табл. 25.
Отмена подстановки имени макроопределения с помощью ди­
рективы i^undef остается в силе до конца файла, в котором оно
встретилось, если только это имя не будет заново определено но­
вой директивой i^define.
При использовании директивы Udefine можно указывать пара­
метры при имени макроопределения:
^include <stdio.h>

^define AREA (г) (3 . 14* (г) * (г) )

175
// Таблица площадей кругов
±nt main ( void. )

float radius = 1.Of;


printf ( "\n Радиус Площадь \n" ) ;
while( radius < 10.5f )
{
printf( "%f %f \ л " , radius, AREA( radius ) ).
radius -h= 1 . Of ;

геЬмхпл 0;

Табл. 25. Отмена макроопределения директивой i^undef


До препроцессора После препроцессора
include <stdio h> Текст файла stdio.h после обработки его
Ые/гпе print/myprintf препроцессором
int main( void ) ini main( void)
\{ {
printf( "Введите дату: "); myprintf( "Введите дату: "),
^undefprintf
printf( "Введите время: "), printf( "Введите время- ");
return 0, return 0;
J }

В этом примере второй аргумент AREA( radius ) в вызове функ­


ции ргш^/'заменяется на (Ъ.\4*(radius)*(radius)). Обратите внимание
также, что в макроопределении не только параметр г, но и весь текст
макроопределения заключены в круглые скобки. Эти скобки
позволяют избежать ошибок из-за возможных побочных эффектов,
связанных с приоритетами выполнения операций:
^include <stdio.h>

^define AREA (г) 3.14*r*r

2.Of/AREA(radius) заменяется на
2.0f/3.14*radius*radius, ошибка
AREA(start-end) заменяется на
3.14*start-end*start-end, также ошибка

Директива Udefine моэюет содер^юатъ в круглых скобках не


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

176
9.3. Включение файлов

Иногда один и тот же фрагмент программы встречается в не­


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

или
^include <путь_к_файлу>

Здесь путь_к_файлу означает корректную запись вида


диск: \ путь_по__ка талогам\ имя__файла_с_ра сширением

Для включаемых файлов принято использовать расширение .И


или .hpp.
Если указанный в директиве файл будет найден, то строка с
директивой Uinclude будет заменена содержимым этого файла. По­
иск включаемого файла выполняется в каталоге, указанном в дирек­
тиве ^include. Если
диск: \путь_по_каталогам\

отсутствует, то при использовании записи в форме:


• " п у т ь к ф а й л у " поиск ведется сначала в текущем каталоге, а за­
тем в каталогах включаемых файлов, определенных в интегриро­
ванной среде программирования;
• <путь к_файлу> поиск ведется сразу в каталогах, определенных в
интегрированной среде программирования.
Между названием директивы и путем к файлу может нахо­
диться любое число пробелов и табуляторов, в том числе и не одно­
го, но рекомендуется использовать между ними один пробел.

Во включаемые файлы помещают директивы Uinclude; прото­


типы функций; определения встроенных {inline) функций; объявле­
ния {extern) данных, определенных в другом файле; определения
{const) констант; перечисления {епит), директивы условной транс­
ляции {#ifndef, Uendif и др.), макроопределения {^define), имено­
ванные пространства имен {namespace), определения типов {class,
struct), объявления и определения шаблонов {template), общие для
нескольких исходных файлов, составляющих одну программу.

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

9.4. Условная компиляция

Нередко, например, при отладке, требуется иметь возможность


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

Табл. 26. Директивы условной компиляции


Директива Функция
Ш/<константное выраэюение> Компилировать строки, следующие за
директивой, если <константное выражение>
отлично от нуля ("истина")
Mfdef идентификатор Компилировать строки, следующие за
директивой, если "идентификатор" определен
с помощью Udefine
Mfndef идентификатор Компилировать строки, следующие за
директивой, если "идентификатор" не
определен с помощью директивы define или
определение отменено с помощью Uundef
Uelse Используется в сочетании с директивами Ш/,
Uifdef, Uifndef как отрицание условия
Uendif Заверщает область действия директив #if,
m/def, m/ndef, Uelse

Эти директивы подобны традиционной конструкции if-then-


else. Иллюстрирующий пример приведен в табл. 27.
Если в этом примере удалить директиву
#define TRACE

ТО после обработки препроцессором будем иметь текст файла в сле­


дующем виде:
void getline ( sroid ) /

±nt main ( void )


{
get line( ) ;
return 0;

178
void, get line ( void. )
(
return;

Табл, 27. Использование директив условной компиляции


До препроцессора После препроцессора
#include <stdio.h> Текст файла stdio.h после обработки его
Mefine TRACE препроцессором
void gedine( void ) ; void getline( void);
int main( void) int main( void )
иmfdef TRACE
{

printf( "Main \n"); printfC'Main \n");


Uendif
getline(); getline();
return 0; return 0;
} } -
void getlinef void) void getline( void )
{ {
mfdefTRACE
printf( "Getline \n"); printf( "Getline \n");
^endif
return; return;
} }

Очень часто директивы условной трансляции используются


для предотвращения многократного включения заголовочных фай­
лов:
/-"
stdlo h
Definitions for stream Inpu t/OL itput.
V

#lfndef STDIO_H
^define STDIO_H
// Текст включаемого файла
#en'dlf

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


тельные директивы препроцессора. Например, в Visual C++ б име­
ются также следующие директивы:
• #е///'(относится к директивам условной компиляции);
• #Нпе (позволяет включать номера строк исходного кода заим­
ствованных файлов);

179
• i^error (обычно включается между директивами #(/'- if^endif для
проверки какого-либо условия на этапе компиляции; при выполне-
ниии такого условия компилятор выводит сообщение, указанное в
terror и останавливается);
• i^pragma (позволяет настраивать компилятор с учетом специ­
фических особенностей конкретной машины или операционной
системы - указанные особенности индивидуальны для каждого
компилятора);
• import (имеет отношение к включению библиотек типов в
cow-технологии).
Их обсуждение выходит за рамки данного пособия.

9.5. Указания по работе с препроцессором

Всякий раз, когда модифицируется включаемый файл имя. И,


требуется заново компилировать каждый файл, в который файл за­
головка включен с помощью директивы Uinclude (среда программи­
рования не всегда контролирует изменение включаемого файла, хо­
тя в последних версиях это предусмотрено).
Препроцессор можно использовать для того, чтобы изменить
внешний вид программы. Например, блок операторов можно офор­
мить в стиле языка Паскаль следующим образом:
^define begin {
^define end }

i n t main ( void. )
begin

rGturn 0;
end

Иногда случается, что текст макроопределения не помещается


на одной строке. В подобных случаях признаком продолжения стро­
ки с текстом макроопределения является символ "\", например:
ifdefine SUM_ZERO_ARRAY( array, size, sum )
{ int i^O;
sum = 0;
while( i < size )
{
sum += array[ I ];
array[ i J = 0;
i + +;
}
}

180
Большинство компиляторов языков Си/С++ поставляется вме­
сте с набором заголовочных файлов. Одним из примеров такого ро­
да является файл stdio,h. При использовании стандартных заголо­
вочных файлов следует посмотреть их содержимое, чтобы случайно
не переопределить стандартное имя. Стандартные заголовочные
файлы разработаны квалифицированными программистами и по
этой причине их также целесообразно посмотреть.
10. РЕДКО ИСПОЛЬЗУЕМЫЕ СРЕДСТВА
ЯЗЫКОВ СИ/С++

10.1. Объявление имени типа typedef

с помощью typedef ыожпо приписать имя существующему ти­


пу данных. Примеры использования объявления имени типа приве­
дены в табл. 28.

Табл. 28. Объявление имени типа


Объявление имени типа Пример применения Значение
typedef int INTEGER, INTEGER a, b, int a, b;
typedef int SHORT, SHORT c,d, int c, d;
typedef long LONG, LONGe,f longe,f.
typedef char * STRPTR; STRPTR ^, h, char *g, *h,
typedef struct COMPLEX k. struct
{ {
double r; double r;
double i; double i.
}
COMPLEX:

Из приведенной таблицы следует, что объявление имени типа


в общем виде записывается следующим образом:
typedef <type definition> <identifier>;

Заметим, что в объявлении имени типа <type definition> и


<identifier> можно поменять местами, хотя делать это не рекоменду­
ется. Чтобы можно было легче обнаружить в программе введенное
имя типа лучше использовать в identifier прописные буквы, как это
сделано в вышеприведенной таблице.
Из табл. 28 также следует, что простейшая форма typedef по­
хожа на директиву препроцессора ^define. Отличие заключается в
том, что typedef обрабатывается компилятором, а директива Udefine
- препроцессором. При этом компилятору доступны дополнитель­
ные проверки, обеспечивающие более глубокий уровень выявления
ошибок.
Имя, объявленное в typedef можно использовать в том же кон­
тексте, что для спецификации типа, например, как аргумент опера­
ции sizeof.
Основными целями использования (ype<ie/являются:

182
• повышение удобства чтения программы;
• повышение мобильности программы.
Если typedef находится внутри функции, то его область дейст­
вия локальна и ограничена этой функцией. Если же объявление име­
ни типа расположено вне функции, то его область действия гло­
бальна. В последнем случае typedef часто помещают во включаемые
файлы.

10.2. Объекты перечислимого типа

Объект перечислимого типа представляет собой объект, значе­


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

Определение объекта
о
Константа
перечислимого типа
{ >* перечисли­ ^ }
мого типа
Идентификатор
—•fenum V w перечислимого
типа

Идентификатор объекта
перечислимого типа

Константа <:>
перечислимого типа

Идентификатор
Константное выражение с
целочисленным значением
Рис. 55. Определение объектов перечислимого типа

Пример определения объекта перечислимого типа:


епгпа languages { с , разе, ada ^ modula2^ forth } master;

или В эквивалентной форме

183
enum languages{ с , разе, ada, modula2, forth };
languages master;

Здесь languages - новый перечислимый тип, a master - объект


типа languages.
Значением master может быть один из идентификаторов:
с, pasc, adaг modula2, forth

Например, можно написать:


master = с;
±f( master == с )
printf( "\п Я знаю язык См" ) ;
switch( master )
{

сазе с:

break;

сазе forth:
break;
cie£aul t:

Используя идентификатор перечислимого типа можно опреде­


лить дополнительные объекты, например:
languages о1, о2;

Теперь имена o l , о2 обозначают объекты типа languages.


Внутренним представлением каждой константы перечислимо­
го типа служит целое значение (типа int). При объявлении перечис­
лимого типа
envaa languages { с , pasc^ ada, modula2, forth };

его константам (слева направо) автоматически присваиваются воз­


растающие целые значения О, 1,2, 3, 4.
Как следует из рис. 55, при объявлении перечислимого типа
можно задать явное присваивание его константам целых значений,
например:
enum languages { с = -1, разе = 4, ada, modula2, forth = 4);

Тем константам, значения которых явно не задано, присваива­


ется значение предшествующей константы, увеличенное на единицу.

184
Таким образом, константе ada соответствует значение 5, а константе
modula2 - значение 6. Разным константам перечислимого типа мо­
жет соответствовать одно и то же значение {pasc^ forth).
Рассмотрим следующий пример:
#include <stdlo.h>
±nt mam ( vaid )
{
enxna t{ c==-l^ pasc=4^ ada, modula2, forth=4 };
t m, ml;
m = ada;
// Следующее присваивание не вполне корректно -
// компилятор формирует предупреждение, но программа
// выполняется
ml = 5 /
printf ( "\п т = %d, ml = %d", т, ml ) ;
// Данные присваивания также не вполне корректны -
// компилятор формирует предупреждение, но программа
// выполняется
т = О; ml = 6;
printf( " \ л т = %d, ml = %d, ada = %d", m, ml, ada ) ;

return 0;

Результаты выполнения программы имеют вид:


т =^ 5, ml = 5
т = О, ml = б, ada = 5

Объекты перечислимого типа, которым присвоены значения


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

Объектам перечислимого типа можно присваивать любой


класс хранения, кроме register. Область действия и время жизни для
них определяются таким же образом, как и для рассмотренных ранее
объектов других типов.
Приведем еще несколько примеров перечислимых типов из
файла .. \include \graphics. h:
епит COLORS
{
BLACK, /* dark colors */
BLUE,
GREEN,

185
CYAN,
RED,
MAGENTA,
BROWN,
LIGHTGRAY,
DARKGRAY, /* light colors */
LIGHTBLUE,
LIGHTGREEN,
LIGHTCYAN,
LIGHTRED,
LIGHTMAGENTA,
YELLOW,
WHITE

enum graphics_errors
{ graphresult error return codes */
grOk = 0,
grNoIni t Graph = -i.
grNotDetected = -2,
grFileNotFound -^ -3,
grInvalidDriver = -4,
grNo Loa dMem = -5,
grNoScanMem = -6,
grNoFlооdMem = -7,
gr Font Not Found = -8,
grNoFon tMem = -9,
grInvalidMode = -10,
generic error */
grError = -11.
grIOerror = -12,
grInvalidFont = -13,
grinvalidFontNum = -14,
grInvalidVersion = -18

10.3. Объединения
Подобно структуре, объединение представляет собой агрегати-
рованный тип данных. Синтаксис объявления объединения иденти­
чен синтаксису объявления структуры, только вместо служебного
слова struct используется служебное слово union. Различие между
структурой и объединением состоит в том, что каждому элементу
объединения выделяется одна и та эюе область памяти^ а не раз­
личные области, как в структуре.
Синтаксис объединения поясняется следующей записью:
vLn±oTi [<идентификатор типа объединения>]
{
<тмп> <идентифмкатор>;

186
} [<список объектов-объединений>]/

Примеры:
union INT__OR_LONG
{
int 1;
long- 1;
} а_питЬег, b_number;

или в эквивалентной форме


union INT__OR_LONG // Объявление типа объединения
(
int 1;
long 1;
};
// Определение объектов-объединений с типом INT_OR_LONG
INT_OR_LONG a_number, Ь_питЬег;

Для объекта-объединения апитЬег или b_number можно легко


выполнить преобразование целого значения в длинное целое или
наоборот. Для преобразования целого значения в длинное целое
достаточно выполнить следующие действия:
а_питЬег. 1 = 7/ // Теперь а_питЬег. 1 имеет
// значение 11

Наряду с преобразованием типов с помощью объекта-


объединения можно получить доступ к отдельным байтам объекта,
как это показано в следующем примере.
union
{
long lvalue;
dovibl e dvalue;
char chvalue;
char cvaluel 8 ];
} value;

Определен объект с именем value, размер которого равен 8


байтам (наибольший из размеров для типов long, double, char, char[
8]).
/ / Доступ к 4 байтам как к объекту типа long
value.lvalue
// Доступ к 8 байтам как к объекту типа double
value.dvalue
value.chvalue // Доступ к байту как к объекту типа
// char

187
к каждому из этих байтов можно осуществить доступ по от­
дельности, используя массив символов value.cvalue:
value, cvalue[ 3 ] // Доступ к 4 байту

Наряду с объектами-объединениями можно работать и с указа­


телями на эти объекты, как показано ниже:
union
(
long lvalue;
dovLble dva lue;
сЬлг chvalue;
сЬа.з: cvalue[ 8 ];
} valuef upvalue = &value;
pvalue->lvalue /* эквивалентно */ value.lvalue

Объектам с типом объединения можно присваивать любой


класс хранения, кроме register. Область действия и время жизни для
них определяются таким же образом, как и для рассмотренных ранее
объектов других типов.
11. МОДЕЛИ ПАМЯТИ

Материал данного раздела в основном освещает вопросы ис­


пользования оперативной памяти процессора, относящиеся к при­
ложениям для шестнадцатибитной среды DOS и WINDOWS с уче­
том особенностей процессоров INTEL 80x86.
Модель памяти для программ на языках Си/С++, работающих
в шестнадцатибитной среде, определяет, как программа использует
память компьютера. Модель памяти связана с архитектурой процес­
сора.
Процессоры INTEL 80x86 используют сегментную организа­
цию памяти, позволяющую адресовать 1 Мбайт памяти. Так как все
регистры процессора шестнадцатиразрядные, то прямой доступ
имеют только 64 Кбайта памяти (диапазон шестнадцатиразрядных
беззнаковых адресов О, 1, 2, ..., 2^^-1). Эти 64 Кбайта памяти назы­
ваются сегментом. Для того чтобы адресовать 1 Мбайт памяти, тре­
буется двадцатибитовый адрес. Поэтому для представления двадца­
тибитового адреса используются два регистра (32 бита). Один ре­
гистр содержит адрес сегмента (регистр CS - указатель сегмента ко­
да, DS - указатель сегмента данных, SS - указатель сегмента стека,
ES - указатель дополнительного сегмента), а второй регистр содер­
жит смещение в сегменте.
Полный двадцатибитовый адрес, адресующий все адресное
пространство процессора, вычисляется следующим образом (рис.
56).

ЮРА : 01С2

16-ричный код смещения


16-ричный код сегмента

Сегментный регистр 0001 0000 1111 1010 (ЮРА)


Сегментный регистр
после сдвига 0001 0000 1111 1010 0000 (ЮРАО)
Смещение 0000 0001 1100 0010 (01С2)

0001 0001 0001 0110 0010 (11162)


Рис. 56. Получение полного двадцатиразрядного адреса

189
Значение сегментного регистра сдвигается влево на четыре
разряда (на одну шестнадцатиричную цифру) и к полученному зна­
чению добавляется смещение. Как следует из рис. 56, начальный ад­
рес сегмента всегда является двадцатибитовым числом, а так как
сегментный регистр имеет только шестнадцать бит, то недостающие
младшие четыре бита всегда подразумеваются равными нулю. Это
означает, что сегменты всегда начинаются на границе шестнадцати
байт или параграфа (отрезок памяти из смежных шестнадцати байт
называется параграфом).
Сегменты памяти могут быть смежными, разделенными, пере­
крываться полностью или частично. Так как сегменты могут пере­
крываться, то одна и та же ячейка памяти может быть адресована
более чем одним адресом. Например,

10F0 : 0262 И 10Е0 : 0362

указывают на один и тот же адрес памяти.


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

11.1. Адресация near, far и huge

Специальные ключевые слова


near - ближний г
tan: - дальний,
hugre - огромный

используются в программах на языках Си/С+н- для модификации оп­


ределений переменных и функций и определяют их размещение в
памяти, отличное от стандартного размещения.
Когда эти ключевые слова используются с указателями, то они
изменяют размер указателя, который определяется выбранной моде­
лью памяти. Имеется три типа указателей (три типа адресации): near
(16 бит), far (32 бита) и huge (32 бита).

Адрес near. Доступ внутри сегмента по умолчанию возможен


через шестнадцатибитовое смещение, так как адрес сегмента по
умолчанию всегда известен. Например, адрес объекта в сегменте
данных по умолчанию получается сложением содержимого шестна­
дцатибитовой величины указателя на объект (смещения) с содержи­
мым регистра сегмента данных DS, сдвинутым влево на четыре би­
та. Это шестнадцатибитовое смещение называется адресом near.

190
Аналогично формируется адрес команды в сегменте команд по
умолчанию (вместо регистра DS используется регистр CS).
Доступ к данным или командам из сегментов по умолчанию в
языках Си/С++ осуществляется через указатели near:
тип near *near_pointer/

Например,
int near *^_Р^'

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

Адрес far. Когда данные или код программы выходят за преде­


лы сегментов по умолчанию, адрес должен состоять из двух частей:
адреса сегмента и адреса смещения. Такие адреса называются адре­
сами ^аг. Доступ вне сегментов по умолчанию осуществляется через
указатели Уаг:
тип far *far_pointer;

Например,
±nt far '^f_p;

Указатели y^fr позволяют адресовать всю память, но имеют сле­


дующие особенности.
1. Пусть имеются три указателя/аг - ptrl ^ ptr2, ptr3 - на одну и
ту же ячейку памяти:
ptrl -- 5F20 ;: 0210,
ptr2 -- 5F21 ;: 0200,
ptr3 -- 5F41 :: 0000.

Над указателями допустимы операции сравнения и правомер­


ны следующие выражения:
ptrl === ptr2 ptrl == ptr3 ptr2 == ptr3

Однако результатом всех трех сравнений будет значение


"ложь", так как операции "==" и "!=" над указателями у^гг использу­
ют все 32 бита указателя как unsigned long int, а не как фактический
адрес памяти.
С другой стороны, операции сравнения "<", "<=", ">", ">=" при
сравнении указателей у^г используют только 16 бит смещения и для

191
указателей far также не гарантируется правильность выполнения
этих операций. Например, вычисление выражений
ptrl > ptr2 ptrl > ptr3 ptr2 > ptr3

Приводит к неожиданному результату: значением выражений будет


"истина", хотя в действительности все три указателя адресуют одну
и ту же ячейку памяти.
2. Если добавить единицу к указателю У^г 1000:FFFF, то ре­
зультатом будет 1000:0000, а не 2000:0000. Если вычесть единицу из
указателя 1000:0000, то результатом будет 1000:FFFF, а не
OFFF:OOOF. Таким образом, при увеличении или уменьшении указа­
т е л я / а г изменяется только смещение. Следовательно, указателемУаг
нельзя адресовать данные или код программы, размер которых пре­
вышает 64 Кбайта.

Адрес huge. Адрес huge, так же как и адрес/аг, состоит из ад­


реса сегмента и смещения и занимает 32 бита. Адрес huge в языках
Си/С++ задается указателем huge:
тип huge *huge_pointer;

Указатель huge имеет два отличия от указателя Уаг.


1. Указатель huge нормализован и содержит максимально до­
пустимое значение адреса сегмента для определяемого им адреса.
Так как сегмент всегда начинается на границе, кратной 16 байтам,
то значение смещения для нормализованного указателя будет в пре­
делах от О до F. Например, нормализованной формой указателя
35D2:1253 (определяемый адрес 36F73) будет 36F7:0003. Операции
сравнения с указателями huge оперируют со всеми 32 битами и дают
правильный результат.
2. Для указателей huge нет ограничений на изменение значения
указателя. Если при изменении указателя huge происходит переход
через границу 16 байт, то изменяется адрес сегмента.
Например, увеличение на единицу указателя 25B0:000F дает
25В 1:0000 и, наоборот, уменьшение на единицу указателя 2531:0000
дает 25B0:000F. Эта особенность указателя huge позволяет адресо­
вать данные, размер которых превышает 64 Кбайта (занимают более
одного сегмента). В языках Си/С++ указатели huge применяют для
адресации массивов размером более 64 Кбайт.

192
11.2.Стандартные модели памяти
для шестнадцатибитной среды DOS

Системы программирования Си/С++ для 16-битной среды DOS


предоставляют пять стандартных моделей памяти:
• крошечную (tiny);
• малую (small);
• среднюю (medium);
• компактную (compact);
• большую (large);
• сверхбольшую (huge).
Метод стандартных моделей памяти является наиболее про­
стым способом управления доступом к коду и данным в основной
памяти. В этом случае управление памятью осуществляется через
режимы (опции) компилятора.

Крошечная модель памяти. Данные, стек, динамическая па­


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

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


большинства обычных программ на языках Си/С++. Программа с
малой моделью памяти занимает только два сегмента по умолчанию:
до 64 Кбайт для кода программы и до 64 Кбайт для данных, стека и
динамической памяти программы. Для адресации кода, данных, сте­
ка и динамической памяти используются только адреса near, что
убыстряет выполнение программы.

Средняя модель памяти. Используется в программах с боль­


шим объемом кода программы (более 64 Кбайт) и небольшим объе­
мом данных, стека и динамической памяти (не более 64 Кбайт).
Средняя модель памяти обеспечивает один сегмент для данных, сте­
ка и динамической памяти программы и отдельный сегмент для ка­
ждого исходного модуля (файла) программы. Это значит, что про­
грамма может занимать до 1 Мбайта л^я кода и до 64 Кбайт для
данных, стека и динамической памяти. Поэтому в программах со
средней моделью памяти для адресации кода используются адреса
far, а для адресации данных - адреса near.

193
Компактная модель памяти. Используется в программах с
большим объемом данных и стека программы (более 64 Кбайт до 1
Мбайта) и небольшим объемом кода (не более 64 Кбайт). Компакт­
ная модель памяти обеспечивает один сегмент для кода программы
и несколько сегментов для данных и стека программы. Поэтому в
программах с компактной моделью памяти для адресации кода ис­
пользуются адреса near, а для адресации данных - адреса far.

Большая модель памяти. Используется в программах с боль­


шим объемом кода, данных и стека программы. Обеспечивает не­
сколько сегментов для кода, данных и стека программы. Это гаран­
тирует до 1 Мбайта суммарной памяти. При этом отдельный элемент
данных не может превышать 64 Кбайта. Используются только адре-
са far.

Сверхбольшая модель памяти. Модель аналогична большой


модели памяти за исключением того, что в сверхбольшой модели
памяти снято ограничение на размер отдельного элемента данных.
Для адресации кода адреса far, а для адресации данных - адреса
huge.

11.3. Изменение размера указателей в стандартных


моделях памяти для шестнадцатибитной среды DOS

Одним из недостатков концепции стандартных моделей памя­


ти является то, что при изменении модели памяти меняются размеры
адресов данных и кода. Однако можно подавить задаваемый по
умолчанию способ адресации для конкретной модели, используя
служебные слова near, far, huge.
Данные можно определять с ключевыми словами near, far,
huge. При этом модифицируется либо размещение данных, либо
размер указателей на данные.
Функции можно объявлять и определять только с ключевыми
словами near и far (ключевое слово huge нельзя применять к функ­
циям). Если ключевое слово near или far предшествует имени функ­
ции, оно определяет, будет ли функция размещаться как near (в сег­
менте кода по умолчанию) или как far (за пределами кода по умол­
чанию).
Если ключевое слово near или far предшествует указателю на
функцию, то оно определяет, будет ли для вызова функции исполь­
зоваться адрес near (16 бит) или адрес far (32 бита).
Для определения массивов размером более 64 Кбайт следует
использовать ключевое слово huge:

194
^include <stdio.h>
// Массив huge из 70000 байтов
char hugre h_arr[ 10000 ] ;

Использование операции sizeof для массивов huge имеет осо­


бенности.
printf( "\п Размер массива h_arr: %ld " , sizeof ( h_arr ) ) ;

Напечатается:
Размер массива h_arr: 44 64

(неверный ответ, так как ^/z^o/'возвращает unsigned int в диапазоне


0...65535, а у нас 70000).
Правильный вариант:
printf( "\п Размер массива h_arr: %ld ",
(unsigned, long inb) sizeof ( h_arr ) ) ;

11.4. Макроопределения для работы с указателями


в шестнадцатиразрядной среде DOS

Заголовочный файл DOS.H определяет три макроса, облег­


чающих работу с указателями:
• FP_OFF(fp) - возвращает смещение у к а з а т е л я ^ ;
• FP_SEG(fp) - возвращает сегмент у к а з а т е л я ^ ;
• MK_SEG( S, о ) ~ возвращает длинный указатель, составленный из
сегмента s и смещения о , переданных в качестве аргументов.

В качестве а р г у м е н т о в ^ в приведенных выше макросах мож­


но использовать не только указатели, но и адреса переменных.
/ / Применение макросов FP__OFF и FP_SEG
^include <stdlo,h>
^include <dos.h>

int mam ( -void. )


{
int 1;

print f ( "Адрес локальной переменной: %p \ л " , &i ) ;


printf( "Адрес локального значения: %04X:%04X \n"^
FP_SEG( &i ; , FP_OFF( &1 ) ) ;

195
return О;

11.5. Работа с памятью для среды WINDOWS

Приложения для шестнадцатибитной среды Windows (EXE) и


Windows (DLL) при компиляции вместо шести могут использовать
только одну из следующих четырех стандартных моделей памяти:
• малую {smaH)\
• среднюю {medium)\
• компактную {compact)',
• большую {large).
Отличием стандартных моделей памяти для шестнадцатибит­
ной среды Windows (DLL) от среды Windows (EXE) является то, что
для данных и динамической памяти используется адресация far во
всех моделях памяти.
Другой отличительной особенностью всех приложений
Windows для шестнадцатибитной среды является то, что сегмент не
содержит реальный адрес памяти. Вместо этого сегмент содержит
индекс (селектор), указывающий на строку в таблице (таблице де­
скрипторов), где этот адрес хранится. Для шестнадцатиразрядной
же среды DOS процессор аппаратно суммирует значение сегментно­
го регистра с указанным смещением, чтобы получить линейный ад­
рес в оперативной памяти.

Работа с памятью в тридцатидвухбитной среде


WINDOWS. В тридцатидвухразрядных программах всегда использу­
ется сплошная (непрерывная) память. Управление этой памятью
осуществляют интегрированная среда программирования и опера­
ционная система.
12. НОВЫЕ в о з м о ж н о с т и ЯЗЫКА C+-i-,
НЕ СВЯЗАННЫЕ С ОБЪЕКТНО-
ОРИЕНТИРОВАННЫМ ПРОГРАММИРОВАНИЕМ
[3,4]

Язык C++ отличается от языка Си, в первую очередь, под­


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

Комментарии. Как уже указывалось ранее, в языке C++ мож­


но использовать два вида комментариев: обычные, оформленные по
правилам языка Си, и однострочные, начинающиеся с символов // и
продолжающиеся до конца строки. Многочисленные примеры их
применения были рассмотрены выше.

Размещение определений данных внутри блока. Напоминаем,


что в языке Си все определения локальных данных внутри блока
помещаются перед первым исполняемым оператором. В языке же
C++ можно (и часто это оказывается более удобным) определять
данные в любой точке блока перед их использованием:

/'^
Файл Р36. СРР (расширение .СРР принято для файлов с текста-
ми программ на C+-h) . Ра змещение определении данных внутри
блока
V
^include <stdio.h>
int mam ( void ) // Возвращает 0 при успехе
{
// В языке C-h-h "модно" таким образом определять и
// присваивать начальное значение управляющей
// переменной цикла for
fori xnt counterl = 0; counterl < 2; counterl++ )
// Переменная counterl "видна"^ начиная с этой строки и
// до конца та±п, а не только внутри блока for. Ей
// присваивается значение О перед входом в цикл
{
// Автоматической переменной 1 присваивается значение
// О при каждом проходе тела цикла,
±пЬ i =- О;
// а внутренняя статическая переменная j

197
// инициализируется нулем
static ±nt
J = О/
for( Int counter2 = 0; counter2 < 5; counter2++ )
pr±ntf( "\n ± = %d j = %d", i-h+r J++ ) ;
}
// counter2 "существует" до предыдущей фигурной скобки
char quit_message[ J = "\n До свидания! \n";
printf ( "%s", quit_message ) ;
ret-am 0;
}

В качестве упражнения рекомендуем определить, что напеча­


тает данная программа, и проверить результаты Вашего анализа с
помощью ЭВМ.

12.1. Прототипы функций. Аргументы по умолчанию

в языке Си наличие прототипов функций необязательно. Такая


"снисходительность" часто порождает массу трудно обнаруживае­
мых ошибок, поскольку компилятор не может проверить, соответст­
вуют ли при вызове функций типы передаваемых аргументов и тип
возвращаемого значения определению данной функции. Язык Сн-+
более строг: он требует, чтобы в файле, где происходит обращение к
функции, причем обязательно до обращения к функции, присутство­
вало либо определение этой функции, либо ее объявление с указани­
ем типов передаваемых аргументов и возвращаемого значения, или,
по терминологии языков Си/С+н-, прототип. В последнем случае оп­
ределение функции может находиться в другом файле. Обычно про­
тотипы функций помещают в заголовочный файл, который включа­
ется в компилируемый файл директивой ^include.
В языке C++ в прототипах функций моэюно задавать значе­
ния аргументов по умолчанию. Предположим, что написана функция
DrawCircle, которая рисует на экране окружность заданного радиуса
с центром в данной точке, и задан ее прототип:
void DrawCircle( ±nt х=100, Int у=100, Int radius=100 ) ;

Тогда вызовы этой функции будут проинтерпретированы, в за­


висимости от количества передаваемых аргументов, следующим
образом:
/ / Рисуется окружность с центром в точке (100, 100) и
// радиусом 100
DrawCircle ( ) ;

198
/ / Рисуется окружность с центром в точке (200, 100) и
// радиусом 100
DrawCircle ( 200 );
// Рисуется окружность с центром в точке (200, 300) и
// радиусом 100
DrawCircle( 200, 300 );
// Рисуется окружность с центром в точке (200, 300) и
// радиусом 4 00
DrawCircle( 200, 300, 400 );
// Ошибка: аргументы можно опускать только справа
DrawCircle ( , , 400 );

Значения аргументов по умолчанию можно задавать не для


всех аргументов, но начинать надо обязательно "справа":
/ / ОшиОочный прототип
void DrawCircle( Int х, int у=100, Int гad );
// Ниже даны правильные варианты
void DrawCircle( int к, int у=100, int radius=100 );
void DrawCircle( int к, int y, int radius^lOO );

12.2. Доступ к глобальным переменным,


скрытым локальными переменными с тем же именем

Оператор разрешения области видимости "::" позволяет вос­


пользоваться глобальной переменной в случае, если она скрыта ло­
кальной переменной с тем же именем:
^include <stdio.h>

int i = 2;

int mam ( void )


{
float i = 5.3f;
{
cJiax- *i = "Hello!";
printf( "i-строка = %s i-целое = %d \n", i, ::i );
}

zr&tuxm 0;
}

В результате выполнения программы получим:


1-строка = Hello! i-целое = 2

199
12.3. Модификаторы const и volatile

Модификатор const, как и в языке Си, запрещает изменение


значений данных. Разумеется, константа должна быть инициализи­
рована при описании, ведь в дальнейшем ей ничего нельзя присваи­
вать. Кроме того, в языке C+-I- данные, определенные как const, ста­
новятся недоступными в других файлах программного проекта, по­
добно статическим переменным:

Файл Р37,СРР
Модификатор const делает данное недоступным в других фай­
лах программного проекта.
Состав проекта: Р37.СРР
CONST,СРР
*/

^include <stdio,h>

ехЬезпл floatt PI;

±nt main ( void ) // Возвращает 0 при успехе


{
printfi "\n PI=%f'\ PI ) ;
return 0;
}

Файл CONST. CPP. Используется в программном проекте P3 7.PRJ

const float PI = 3.14159;

Раздельная компиляция файлов из приведенного примера


пройдет успешно, но компоновщик сообщит, что в файле Р37.СРР
имеется не разрешенная внешняя ссылка.
В большинстве случаев компилятор языка C++ трактует
описанное как const данное, не локальное ни в одном блоке
(областью действия его является файл), точно так же, как и
макроопределение, созданное директивой препроцессора Udefine,
т.е. просто подставляет в соответствующих местах величину,
которой данное инициализировано. Однако const обладает тем
преимуществом перед Udefine, что обеспечивает контроль типов,
поэтому его использование может уберечь от многих ошибок.
Модификатор volatile, напротив, сообщает компилятору, что
значение данного может быть изменено каким-либо фоновым про­
цессом - например, при обработке прерывания. С точки зрения ком-

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

12.4. Ссылки

в большинстве языков программирования параметры переда­


ются в подпрограмму (функцию) либо по ссылке, либо по значению.
В первом случае подпрограмма (функция) работает непосредственно
с аргументом, переданным ей, а во втором случае - с копией аргу­
мента. Различие здесь очевидно: аргумент, переданный по ссылке,
подпрограмма (функция) может модифицировать, а переданный по
значению - нет.
Как уже отмечалось выше, в языке Си аргументы передаются в
функцию только по значению и общепринятый способ обеспечить
функции непосредственный доступ к какому-либо данному из вы­
звавшей программы состоит в том, что вместо самого данного в ка­
честве аргумента передается его адрес. При работе на языке C++ нет
необходимости прибегать к таким ухищрениям - в языке C++ реали­
зованы оба способа передачи параметров. Многочисленные приме­
ры, иллюстрирующие сказанное рассмотрены выше.
Ссылки в языке C++ можно использовать не только для пере­
дачи параметров в функции, но и для создания псевдонимов данных:

int X ^ 1;
int &xr = X / / Ссылка хг становится псевдонимом
// к
xr = 2; // Все равно, что к = 2;
ХГ+ + ; // Все равно, что х++;
Однако,
int X = 1;
// Так как типы х и хг не совпадают, то компилятор создает
// переменную типа char, для которой хг будет псевдонимом,
// и присваивает ей (char)х
char &ХГ = х;
хг = 2; // Значение х не изменяется

201
12.5. Подставляемые функции

в языке Си для уменьшения расходов на вызовы небольших,


часто вызываемых функций, принято использовать макроопределе­
ния с параметрами. Однако их применение сильно запутывает про­
грамму и служит неиссякаемым источником трудноуловимых оши­
бок.
Что же предлагает взамен язык C++? Достаточно описать
функцию как inline и компилятор, если это возможно, будет под­
ставлять в соответствующих местах тело функции, вместо того, что­
бы осуществлять ее вызов. Конечно же, определение подставляемой
функции должно находиться перед ее первым вызовом:
inline int InlineFunctionCube( int x )
{
return x*x*x/
}

int b = InlineFunctionCube( a ) ;
int с = InlineFunctionCube( a++ ) /

Вот теперь можно повысить эффективность программы, поль­


зуясь при этом всеми преимуществами контроля типов и не опасаясь
побочных эффектов. Невозможна подстановка функций, содержа­
щих операторы case, for, while, do-while, goto. Если для данной
функции, определенной как inline, компилятор не может осущест­
вить подстановку, то он трактует такую функцию как статическую,
выдавая, как правило, соответствующее предупреждение.

12.6. Операции динамического распределения памяти

Так как занятие и освобождение блоков памяти является очень


распространенной операцией, в языке C++ введены два "интеллек­
туальных" оператора new и delete, освобождающих программиста от
необходимости явно использовать библиотечные функции malloc,
calloc и free. Примеры использования этих операторов приведены
выше. Остается добавить, что в программе, использующей new и
delete, не запрещается применять также функции библиотеки языка
Си malloc, calloc, free и им подобные.

202
12.7. Перегрузка функций

Предположим, что по ходу программы часто необходимо печа­


тать значения типа ш/, double и char *. Почему бы не создать для
этой цели специальные функции?

/ / в языке Си для вывода значений разного типа каждой из


// функций придется дать особое имя
void. print_int ( ±xit 1 )
printf( "%d", i ;/ z-etuxn/

voxd print_double ( dovible x )


printf( "%lg"r X ) ; ) ; x-etum/

•sroxdL print_str±ng ( chsir "^s )

printf( "%s", s ) ; ) ; x-etixm/

±nt j =5/

print_lnt ( J ) ;
print_double( 3,14159 ) ;
print_string( "Hello" ) ;

В стандартном языке Си потребовалось дать этим трем функ­


циям различные имена, а вот в языке C++ можно написать "умную"
функцию print, существующую как бы в трех ипостасях:
i^include <stdio. h>

void, print ( Int i )

pr±ntf( "%d", i ) ; return;

void print ( double x )

printf( "%lg", X ) ; return;

void print ( сЪа.г *s )

printf( "%s", s ) ; retuim;

int mam ( void )

203
±nt j = 5;

print( J );
print( 3.14159 );
print( "Hello" );

iretuxTi 0;
}

Компилятор сам выбирает, какую из трех перегруженных


функций с именем print вызвать в каждом случае. Критерием выбора
служат количество и типы аргументов, с которыми функция вызыва­
ется, причем, если не удается найти точного совпадения, компиля­
тор выбирает ту функцию, при вызове которой "наиболее легко" вы­
полнить для аргументов преобразование типа.
Обратите внимание на два существенных обстоятельства.
• Перегруженные функции не могут различаться только по типу
возвращаемого значения:
void. f( ±nt^ int );
izit f( int^ int ); // Ошибка!

• Перегрузка функций не должна приводить к конфликту с аргу­


ментами, заданными по умолчанию:

void f ( int = о ) ;
void f( void ) ;

f(); // Какую функцию вызвать?

Компилятор языка C++ позволяет давать различным функциям


одинаковые имена. Поэтому, помещая имена функций в объектный
файл - результат компиляции, он должен их каким-то образом мо­
дифицировать, чтобы сделать уникальными. Модифицированные
компилятором имена содержат информацию о количестве и типе па­
раметров, так как именно по этому признаку перегруженные функ­
ции различаются между собой. Такая модификация получила назва­
ние "декорирование имен".
В некоторых ситуациях, например, при необходимости ском­
поновать программу на языке C++ с объектными файлами или биб­
лиотеками, созданными "обычным" Си-компилятором, декорирова­
ние имен нежелательно. Чтобы сообщить компилятору языка C++,
что имена тех или иных функций не должны декорироваться, их
следует объявить как extern "С":

// Отдельная функция

204
exteim "С" ±nt fund ( ±nt ) ;
extern "C" // Несколько функций
{
void. func2 ( ±nt ) ;
±nt funcS ( void. ) ;
double fun с 4( double ) ;
}

Модификатор extern "C" можно использовать не только при


объявлении, но и при описании функций. Естественно, что функции
с модификатором extern "С" не могут быть перегруженными.

12.8. Шаблоны функций

При написании программ на языке C++ часто приходится соз­


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

1 ^^Файл Р38.СРР, Шаблоны функций


""/
^include <stdio.h>
iinclude <string.h>
// Замена местами переменных "а <~> Ь". Компилятор создаст
// подходящую функцию, когда "узнает", какой тип аргументов
// Т подходит в конкретном случае
template < class Т >
void swap( Т &а, Т &Ь )
(
Т с; // Для обмена

с = Ь; b = а; а = с/

return/
}
int mam ( void ) // Возвращает О при успехе
{
Int i = О, J = 1;
double X = 0.0, у = 1.0/
char *sl = "Строка!", *s2 = "Строка2" /
print f ( "\n Перед обменом: \n i = %d j = %d \n x=%lg "
"y==%lg \n sl=%s s2=%s", 1, J, X, y, si, s2 ) /
swap ( i , J ) / swap ( x, у ) / swap ( si, s2 ) /
printf ( "\n После обмена: \n i = %d j = %d \n x=%lg "

205
"у=%1д \п sl = %s s2^%s", i, j , x, y, si, s2 ) ;

rebvim 0;
}

Аргументы, помещаемые в угловые скобки после служебного


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

12.9. Перегрузка операций

Если в языке C++ можно самому определять новые типы дан­


ных, например, структуры, то почему бы не заставить привычные
операторы выполнять те же действия над определенными нами ти­
пами, которые мы хотим? И такая возможность есть.
Пусть @ есть некоторый оператор языка C++, кроме следую­
щих операторов:
, .* :: ?: sizeof

Тогда достаточно определить функцию с именем operator@ с


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

Файл Р39.СРР. Перегрузка операторов

^include <stdlo.h>
^include <string.h>
// Максимальная длина строки +1
const ±nt MAX_STR_LEN = 80;

stiract STRING // Структурный тип для строки


{
chsr s[ MAX_STR_LEN ];
// Строка
±nt str_len; // Текущая длина строки
} ;

// Переопределение ("перегрузка") оператора сложения для


// строк - выполняет сцепление (конкатенацию) строк
STRING орега,Ьог+ ( // Возвращает конкатенацию строк
STRING &sl, // Первый операнд
STRING &s2) // Второй операнд
{
STRING TmpStr; // Для временного хранения

206
// Длина строки результата равна сумме длин складываемых
// строк. Позаботимся также о том, чтобы не выйти за
// границу массива-суммы
±f( ( TmpStr. str_len = si . str_len + s2. str__len ) >=
MAX_STR_LEN )
{
TmpStr, s[ 0 ] = '\xO'/ TmpStr. Str__len = 0;
re turn TmpStr;
}

// Выполним конкатенацию (сложение) строк


strcpy( TmpStr.sг sl.s ) ; strcat ( TmpStr.s, s2.s ) ;
rebvLrn TmpStr;
}

int main ( void ) // Возвращает 0 при успехе


{
STRING strl, str2, str3;
strcpy ( strl.Sr "Перегрузка операторов - " ) ;
strl.str_len = strlen ( strl.s ) ;
strcpy( str2.Sr "это очень здорово!" ) ;
str2. str__len = strlen ( str2.s ) ;
printf( "\n Первая строка: длинa=%d^ coдepжимoe=%s",
strl . str__len ^ strl.s ) ;
printf( "\n Вторая строка: длинa=%d, coдepжимoe=%s",
str2. str_len,^ str2.s ) ;
str3 = strl + str2;
print f( "\n Конкатенация строк: длина = %d,. содержимое^%s ",
str3. str__len, strJ.s ) ;

rebvLrii. 0;
13. ТЕХНОЛОГИЯ СОЗДАНИЯ ПРОГРАММ [5]

К настоящему моменту рассмотрен весь спектр средств языка


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

13.1. Кодирование и документирование программы

С течением времени в процессе работы каждый программист


вырабатывает собственные правила и стиль программирования. При
этом полезно учиться не только на собственном опыте, но и разумно
следовать приведенным ниже рекомендациям, основанным на дос­
тижениях ведущих программистов, которые, де-факто, стали не­
гласным стандартом программирования. Это поможет избежать
многих распространенных ошибок и неоправданно больших затрат
времени на проектирование программных продуктов. Вместе с тем
отметим, что, конечно же, что на все случаи жизни советы дать не­
возможно - ведь не зря программирование, особенно на заре его
развития, считалось искусством.
Главная цель, к которой нуэюно стремиться, - получить легко
читаемую программу возможно более простой структуры [5]. В
конечном итоге, все технологии программирования направлены на
достижение именно этой цели, поскольку только таким путем можно
добиться надежности и простоты модификации программы. В соот­
ветствии со сказанным, предпочтение при программировании следу­
ет отдавать не наиболее компактному и даже не наиболее эффектив­
ному способу программирования, а такому способу, который легче
для понимания. Особенно важно это в случае, когда программу пи­
шут одни программисты, а сопровождают другие, что является ши­
роко распространенной практикой [5].
Первый шаг в написании программь/ - запись ее в так назы­
ваемой текстуальной форме, возможно, с применением блок-схем.
Текстуальная форма должна показать, что именно и как программа
должна делать. Если же не можете записать алгоритм решения зада­
чи в текстуальной форме, то велика вероятность того, что алгоритм
плохо продуман. Текстуальная запись алгоритма полезна по не­
скольким причинам — она позволяет детально продумать алгоритм,
обнаружить на самой ранней стадии некоторые ошибки, разбить
программу на логическую последовательность функционально за-

208
конченных фрагментов, а также обеспечить комментарии к про­
грамме.
Каждый функционально законченный фрагмент алгоритма в
соответствии с технологией модульного программирования следует
оформить в виде функции. Каждая функция должна решать только
одну задачу (не надо объединять два коротких независимых фраг­
мента в одну функцию). Предельные параметры функции (количест­
во строк исходного текста и число параметров) определяются рас­
смотренным ранее правилом "семь плюс-минус два".
Если некоторые действия встречаются в программе хотя бы
дважды, их также нужно оформить в виде функции. Однотипные
действия оформляются в виде перегруженных функций или функций
с параметрами. Короткие, простые функции следует оформлять как
подставляемые функции.
Необходимо тщательно выбирать имена объектов (пере­
менных, функций и т.п.). Рационально выбранные имена могут сде­
лать программу в некоторой степени самодокументированной. Не­
удачные имена, наоборот, служат источником проблем. Не увлекай­
тесь сокращениями - они ухудшают читаемость текста. Общая тен­
денция состоит в том, что чем больше область видимости объекта,
тем более длинным именем его надо снабжать. Перед таким именем
часто ставится префикс типа (одна или несколько букв, по которым
можно определить тип объекта). Для управляющих переменных ко­
ротких циклов, напротив, лучше использовать однобуквенными
именами типа /, у, или к. Имена макросов предпочтительнее записы­
вать прописными буквами, чтобы отличать их от других объектов
программ. Не рекомендуется использовать имена, начинающиеся с
одного или двух символов подчеркивания, имена типов, оканчи­
вающиеся на "_/" и т.п.
Переменные желательно инициализировать при их опреде­
лении^ а определять как можно ближе к месту их непосредственного
использования. Но нет правил без исключений. Поэтому, с другой
стороны, все определения локальных переменных блока лучше рас­
полагать в начале блока, чтобы их легко можно было найти.
Локальные переменные предпочтительнее глобальных. Если
глабальная переменная все же необходима, то лучше определить ее
статической. Это ограничит область действия такой переменной од­
ним исходным файлом.
Всю необходимую функции информацию нужно стремиться
передавать через список параметров, а не через глобальные пере­
менные, изменение которых трудно отследить.
Входные параметры функции, которые не дол:и€ны в ней
изменяться, следует передавать по ссылке с модификатором
const, а не по значению. Кроме улучшения читаемости программы и

209
уменьшения случайных ошибок, этот способ позволяет экономить
память при передаче сложных объектов. Исключение составляют
параметры, размер которых меньше размера указателя - их лучше
передавать по значению.
Нельзя возвращать из функции ссылку на локальную пере­
менную, потому что она автоматически уничтожается при выходе из
функции, которая является ее областью действия. Не рекомендуется
также возвращать ссылку на объект, созданный внутри функции с
помощью функции malloc{ ) или операции new, так как это приводит
к трудно контролируемым утечкам динамической памяти.
Следует избегать использования в программе чисел в явном
виде. Константы должны иметь осмысленные имена, заданные через
const или епит (последнее предпочтительнее, так как память под
перечисление не выделяется). Символическое имя делает программу
более понятной. Кроме того, при необходимости изменить значение
константы это можно сделать всего лишь в одном месте программы.
Для записи каждого фрагмента программы необходимо ис­
пользовать наиболее подходящие языковые средства. Любой цикл
можно, в принципе, реализовать с помощью операторов if и goto, но
это было бы нелепо, поскольку с помощью операторов цикла те же
действия легче читаются, а компилятор генерирует более эффектив­
ный код. Ветвление на три или более направлений предпочтитель­
нее программировать с использованием оператора switch, а не с по­
мощью гнезда операторов if.
Следует избегать лишних проверок условий. Например, вме­
сто операторов
±f( strstr( a,b ) > О ) { ... }
else ±f( strstr( a,b ) < 0 ) { . . . ;
else ( ... }

лучше записать
±nt ls_equal = strstr ( a,b ) ;
±f( is_equal > 0 ) { ... }
else ±f( is_equal < 0 ) { ... }
else { ... }

Бессмысленно использовать проверку на неравенство нулю


(или, что еще хуже, на равенство true или false):
bool is_busy;

±f( is_busy == true ) { } // Лучше ±f( is_busy ) { }


±f( is_busy == false ) { } // Лучше ±f( !is_busy ) { }
while ( a == 0 ) { } // Лучше while ( !a ) { }

210
Если одна из ветвей условного оператора короче, чем другая,
то более короткую ветвь условного оператора лучше поместить
сверху, иначе вся управляющая структура может не поместиться на
экране, что затруднит отладку.
В некоторых случаях тернарная операция лучше условного
оператора:
if( Z ) 2.-J; else i=k; // i = z ? j : k;

При использовании циклов надо стремиться объединять


инициализацию^ проверку условия повторения и приращение в
одном месте. Сказанное означает, что лучше использовать цикл/or.
Необходимо проверять коды возврата для выявления оши­
бок и предусматривать печать сообщений в тех точках про­
граммы, куда управление при нормальной работе программы пе­
редаваться не доллсно. Например, в переключателе должен исполь­
зоваться вариант default с обработкой ситуации по умолчанию, осо­
бенно, если в переключателе перечислены все возможные варианты.
Сообщение об ошибке дол:н€но быть информативным и под­
сказывать пользователю, как ее исправить. Например, при вводе не­
верного значения в сообщении должен быть указан допустимый
диапазон.
Операции выделения и освобо^исдения динамической памяти
следует помещать в одну и ту Jtce функцию. Утечки памяти, когда
ее выделили, а освободить забыли, создают большие проблемы в
программах, продолжительность работы которых велика или не ог­
раничена (на серверах баз данных, в операционных системах и т.п.
программах).
После написания программу надо тщательно отредакти­
ровать — убрать ненужные фрагменты, сгруппировать описания, оп­
тимизировать проверки условий и циклы, проверить, оптимальное
ли разбиение на функции и т.п. Подходить к написанию программы
надо так, чтобы ее в любой момент можно было передать другому
программисту.
Комментарии имеют очень важное значение, поскольку
программист чаще выступает как читатель, а не как писатель. Даже
в том случае, если программу сопровождает автор программы, раз­
бираться через год в плохо документированном тексте удовольствие
сомнительное.
По использованию комментариев и использованию форма­
тирования текста мо:>§€но дать следующие рекомендации (они
иллюстрируются многочисленными примерами исходных текстов
программ, приведенными в этой книге).
• Программа, если она используется, живет не один год, по-

211
требность в каких-то ее новых свойствах появляется сразу же после
ввода программы в эксплуатацию и сопровождение программы за­
нимает гораздо больше времени, чем ее разработка. По этой причи­
не основная часть документации долэюна находиться в тексте про­
граммы. Хорошие комментарии написать почти так же сложно, как
и хорошую программу.
а Комментарии долэюны представлять собой правильные
предложения без сокращений и со знаками препинания и не должны
подтверждать очевидное. В данной книге иногда есть отступы от
этого в учебных целях.
а Если комментарий к фрагменту программы занимает не­
сколько строк, его лучиге разместить до фрагмента, чем справа от
него, и выровнять по вертикали. Абзацный отступ комментария
долэюен соответствовать отступу комментируемого блока.
• Для разделения функций и других логически законченных
фрагментов пользуйтесь пустыми строками и комментариями вида:

//* ii^ic-kic*i(i<irk*i(iciri(:*iricici(*i(**i^ic-kic*icif9c*i(-k^ic**ic-kiricici<*-k-^iri(i(icici^*ic*if*

• Вложенные блоки долэюны иметь отступ в 3 - 4 символа


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

13.2. Проектирование и тестирование программы [5]

Вначале рассмотрим, как не следует проектировать и тестиро­


вать программы. Начинающие программисты, особенно студенты,
часто, получив задание, сразу садятся за компьютер и начинают ко­
дировать те части алгоритма, которые им удается придумать сходу.
Объектам программы даются первые попавшиеся имена и т.п. Когда
компьютер "зависает" или получаются "загадочные" результаты, по-

212
еле некоторого перерыва написанные фрагменты стираются и все
повторяется заново.
В процессе работы неоднократно изменяются структуры дан­
ных, разбиение на модули (функции) делается только тогда, когда
просматривать текст программы становится неудобно и утомитель­
но. При этом комментарии к программе не пишутся, а ее текст не
форматируется. Из-за многочисленных неудач периодически выска­
зываются сомнения в правильности работы компилятора, компьюте­
ра и операционной системы.
Когда программа впервые доходит до стадии выполнения (а
это случается не скоро), в нее вводятся произвольные исходные
данные, после чего экран компьютера и файл результатов становят­
ся объектами пристального удивленного изучения. "Работает" такая
программа обычно только на одном наборе исходных данных, а вне­
сение в них даже небольших изменений приводит автора к потере
веры в себя и портит его настроение.
Задача настоящего программиста состоит в том, чтобы нау­
читься подходить к программированию профессионально. Профес­
сионал отличается тем, что может достаточно точно оценить, сколь­
ко времени займет разработка программы, которая будет работать в
точном соответствии с поставленной задачей. Для достижения по­
добных результатов требуется склонность к программированию,
опыт, а также знание основных принципов, выработанных програм­
мистами в течение более чем полувека развития этой дисциплины.
Даже к написанию самых простых программ нужно подходить по­
следовательно, проходя в соответствии со структурным подходом
ряд последовательных этапов.
Структурный подход к программированию охватывает все
стадии разработки проекта — спецификацию, проектирование, соб­
ственно программирование (кодирование) и тестирование. При этом
стремятся к уменьшению числа возможных ошибок, их более ран­
нему обнаружению и упрощению процесса исправления ошибок. В
рамках структурного подхода используются нисходящая разработка,
структурное и модульное программирование и нисходящее тестиро­
вание.
Рассмотрим этапы создания программ, рассчитанные на дос­
таточно большие программные проекты, разрабатываемые коллек­
тивом программистов [5]. Для неболыиих программ каждый из та­
ких этапов упрощается, но содержание и последовательность эта­
пов не изменяются,

13.2.1. Этап 1: постановка задачи

Создание любой программы начинается с постановки задачи,

213
Изначально задача ставится в терминах некоторой предметной об­
ласти и необходимо перевести ее в термины, более близкие к про­
граммированию. Поскольку программист редко досконально разби­
рается в предметной области, а заказчик - в программировании, то
постановка задачи может стать весьма непростым итерационным
процессом. Отметим, что здесь весьма полезным является использо­
вание объектно-ориентированного подхода, средства реализации
которого в языке C++ будут рассмотрены во второй части книги.
Постановка задачи заканчивается созданием технического за­
дания, а затем и внешней спецификации программы, которая вклю­
чает в себя.
а Описание исходных данных и результатов (виды,
представление, точность, ограничения и т.п.).
• Описание задачи, реализуемой программой,
а Способ обращения к программе.
• Описание возможных особых и аварийных ситуаций и оши­
бок пользователя.
На этом этапе программа рассматривается как "черный ящик",
для которого определена выполняемая им функция, входные и
выходные данные.

13.2.2. Этап 2: разработка внутренних структур данных

Большинство алгоритмов решения задач зависит от того, ка­


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

13.2.3. Этап 3: проектирование структуры программы


и взаимодействия модулей

На этом этапе применяется технология нисходящего проек­


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

214
низком уровне абстракции и т.д. Очень важной на этом этапе явля­
ется спецификация интерфейсов, т.е. способов взаимодействия
подзадач.
Для каждой подзадачи создается внешняя спецификация, ана­
логичная указанной для этапа 1. Здесь же решаются вопросы раз­
биения программы на модули. Декомпозиция выполняется таким
образом, чтобы минимизировать взаимодействие модулей. В резуль­
тате может оказаться так, что одна задача реализуется с помощью
нескольких модулей и, наоборот, в одном модуле может решаться
несколько задач. На более низкий уровень проектирования перехо­
дят только после окончания проектирования верхнего уровня. Алго­
ритм записывают в обобщенной форме — например, текстуальной, в
виде обобщенных блок-схем или другими способами.
На этапе проектирования следует учитывать возможность бу­
дущих модификаций программы и стремиться проектировать про­
грамму таким образом, чтобы вносить изменения было возможно
проще. Процесс проектирования является итерационным, поскольку
в программе реального размера трудно продумать все детали с пер­
вого раза.

13.2.4. Этап 4: структурное программирование

Процесс программирования (кодирования) также организуется


по принципу "сверху вниз": вначале кодируются модули самого
верхнего уровня и составляются тестовые примеры для их отладки.
При этом на месте еще не написанных модулей следующего уровня
ставятся "заглушки" - временные программы. "Заглушка" в про­
стейшем случае просто выдает сообщение о том, что ей передано
управление, а затем возвращает его в вызывающий модуль. В других
случаях "заглушка" может выдавать значения, заданные заранее или
вычисленные по упрощенному алгоритму. Таким образом, сначала
создается логический скелет программы, который затем обрастает
"плотью" кода. Такая технология программирования получила на­
звание нисходящей технологии программирования, В литературе
[5] показано, что эта технология по сравнению с восходящей техно­
логией программирования имеет целый ряд преимуществ, что и обу­
словило ее широкое распространение.
Рекомендации по записи алгоритмов в терминологии языка
C++ приведены в подразд. 13.1. Ввиду важности, напомним еще раз,
что главные цели — читаемость и простота структуры программы в
целом и любой из составляющих ее функций.
При программировании следует отделять интерфейс
(функции, модуля, класса) от его реализации и ограничивать
доступ к ненулсной информации. Небрежное, даже в мелочах, про-

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

13.2.5. Этап 5: нисходящее тестирование

Хотя этот этап рассматривается последним, это не значит, что


тестирование не должно проводиться на предыдущих этапах. Про­
ектирование и программирование должны обязательно сопровож­
даться написанием набора тестов — проверочных исходных дан­
ных и соответствующих им наборов эталонных реакций.
Необходимо различать процессы тестирования и отладки про­
граммы. Тестирование представляет собой процесс, посредством
которого проверяется правильность функционирования программы
и соответствие всем проектным спецификациям. Таким образом,
тестирование носит позитивный характер. Отладка - процесс ис­
правления ошибок в программе. При отладке, в частности, исправ­
ляют ошибки, обнаруженные при тестировании. Следует заметить,
что процесс обнаружения ошибок подчиняется экспоненциальному
закону, т.е. большинство ошибок обнаруживается на ранних стадиях
тестирования и, чем меньше в программе осталось ошибок, тем
дольше придется искать каждую из них.
Для исчерпывающего тестирования программы необходимо
проверить каждую из ветвей алгоритма. Общее число ветвей оп­
ределяется числом комбинаций всех альтернатив на последователь­
ных участках алгоритма. Это конечное число, но оно может быть
очень большим. Поэтому при тестировании программа разбивается
на фрагменты, после исчерпывающего тестирования которых они
рассматриваются как элементарные узлы более длинных ветвей.
Тесты, в числе прочих возможностей, должны содержать проверку
граничных условий (например, переход по условию л:>10 должен
проверяться для значений 9, 10 и 11). Отдельно проверяется реакция
программы на оигибочные исходные данные.
Идея нисходящего тестирования предполагает, что к тестиро­
ванию программы приступают еще до того, как завершено ее проек­
тирование и кодирование. Это позволяет раньше опробовать основ­
ные межмодульные интерфейсы, а также убедиться в том, что про­
грамма в основном удовлетворяет требованиям пользователя. Толь­
ко после того, как логическое ядро испытано настолько, что появля­
ется уверенность в правильности реализации основных интерфей-

216
сов, приступают к кодированию следующего уровня программы.
Естественно, что полное тестирование программы, пока она
представлена в виде скелета, невозможно, однако добавление каж­
дого следующего уровня позволяет постепенно расширять область
тестирования.
Этап комплексной отладки на уровне системы при нисходя­
щем проектировании занимает меньше времени, чем при восходя­
щем, поскольку вероятность появления серьезных ошибок, затраги­
вающих большую часть системы, гораздо ниже. Кроме того, для ка­
ждого следующего, подключаемого к системе модуля уже создано
его окружение и выходные данные отлаженных модулей можно ис­
пользовать как входные для тестирования других. Но это, конечно,
не значит, что модуль можно подключать к системе совсем "сырым"
- бывает удобным провести часть тестирования автономно, по­
скольку сгенерировать на входе системы все варианты, необходи­
мые для тестирования отдельного модуля, трудно.
В приложении П.4 рассмотрена более подробно методика от­
ладки программы применительно к возможностям двух популярных
разновидностей интегрированных сред проектирования программ.
ЧАСТЬ 2. ПРИКЛАДНОЕ ПРОГРАММИРОВАНИЕ

В число классических задач прикладного программирования


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

14. ДИНАМИЧЕСКИЕ СТРУКТУРЫ ДАННЫХ [5]

Любая программа предназначена для обработки данных. По


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

218
оперативной памяти должен быть известен до начала выполнения
программы и задан в виде константы. Заметим, что это возможно не
всегда и тогда используют динамическое размещение данных в опе­
ративной памяти. Динамическое размещение данных в памяти про­
исходит во время выполнения программы с помощью операции new
(только для языка С-+"Ь) или функции malloc (языки Си/С++). При
этом необходимый объем требуемой памяти должен быть известен
лишь к моменту динамического распределения памяти, а не до нача­
ла выполнения программы.
Как уже было сказано, если до начала работы с данными не­
возможно определить, сколько памяти потребуется для их хранения,,
то память выделяется по мере необходимости отдельными блоками,
связанными друг с другом с помощью указателей. Такой способ ор­
ганизации получил название динамических структур данных, по­
скольку их размер изменяется во время выполнения программы. В
качестве таких структур в программах часто используются линейные
списки (см. разд. 8), бинарные деревья и очереди, частным случаем
которых являются стеки. Они отличаются способами связи отдель­
ных элементов друг с другом и допустимыми операциями. Отметим,
что динамическая структура данных может занимать несмежные
блоки оперативной памяти.
Динамические структуры данных часто применяют и для бо­
лее эффективной работы с данными, размер которых известен. К
такого рода случаям можно отнести решение задач сортировки и
поиска элементов. При сортировке упорядочивание динамических
структур не требует перестановки элементов, а сводится к измене­
нию указателей на эти элементы. Это особенно эффективно, если
сортируемые элементы большого размера. При решении задачи по­
иска элемента в тех случаях, когда важна скорость, данные лучше
всего представлять в виде бинарного дерева.
Элемент любой динамической структуры данных представляет
собой структуру {struct), содержащую, по крайней мере, два поля -
для хранения данных и для указателя (см. разд. 8). В общем случае
полей данных и указателей может быть несколько. Поля данных мо­
гут быть любого типа: стандартного (основного), составного или
типа указатель.

14.1. Линейные списки

Самый простой способ связать множество элементов - сделать


так, чтобы каждый элемент содержал ссылку на следующий. Такой
список называется однонаправленным (односвязным). Исчерпываю­
щий пример такого рода рассмотрен в разд. 8. Если добавить в каж-

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

14.2. Бинарные деревья

Бинарное дерево представляет собой динамическую структуру


данных, состоящую из узлов (вершин), каждый из которых содер­
жит, кроме данных, не более двух ссылок на различные бинарные
деревья. На каждый узел имеется ровно одна ссылка. Начальный
узел называется корнем дерева.
На рис. 57 приведен алгоритм построения и пример бинарного
дерева (его корень обычно располагается сверху).
Узел, не имеющий поддеревьев, называется листом. Исходя­
щие узлы называются предками, входящие — потомками. Высота
дерева определяется количеством уровней, на которых располага­
ются его узлы. Использование двоичного дерева для сортировки
массива рассмотрено в разд. 15.
Если дерево организовано таким образом, что для каждого уз­
ла все ключи его левого поддерева меньше ключа этого узла, а все
ключи его правого поддерева - больше, то оно называется деревом
поиска. В дереве поиска можно найти элемент по ключу, двигаясь от
корня и переходя на левое или правое поддерево в зависимости от
значения ключа в каждом узле. При этом время поиска определяется
высотой дерева, которая пропорциональна двоичному логарифму
количества узлов. Пример использования дерева поиска рассматри­
вается в разд. 17.
Дерево является рекурсивной структурой данных, поскольку
каждое поддерево также является деревом. Действия с такими
структурами лучше всего описываются с помощью рекурсивных ал­
горитмов. Например, обход всех вершин дерева можно выполнить с
помощью рекурсивной функции. Пример такой функции имеется в
разд. 15.

220
Корень

J1=2*i

_Z^X J
2, 3, 4, 5 - вершины
10 6, 7, 8, 9, 10-листья
8

Пример двоичного дерева для size =10


Рис. 57. Алгоритм построения и пример бинарного дерева

14.3. Очереди и их частные разновидности

Универсальную очередь можно рассматривать как частный


случай однонаправленного или двунаправленного линейного списка,
когда определены только операции добавления или выборки эле­
ментов с любого из двух концов и, может быть, просмотра элемен­
тов очереди. Иные операции с очередью не определены. При выбор­
ке элемент исключается из очереди. В качестве применений очереди
можно назвать моделирование систем массового обслуживания, со­
ставной частью которых являются очереди, диспетчеризация задач
операционной системой, буферизация ввода-вывода и др.
На практике часто используются более простые, частные слу­
чаи универсальной очереди - очереди типа FIFO и L1FO (стек). Оче­
редь типа FIFO (First Input - First Output : первым занесен ~ первым
извлечен) получается, если операция добавления элемента в очередь
разрешена только для одного конца очереди, а операция выборки
элемента - только для другого конца очереди.
Стек - это частный случай однонаправленного линейного спи­
ска, добавление элементов в который и выборка выполняются
только с одного конца, называемого вершиной стека. Говорят, что
стек реализует дисциплину обслуживания LIFO (Last Input - First
Output : последним занесен — первым извлечен). Стеки широко при­
меняются в программировании, компиляторах, рекурсивных алго­
ритмах и т.п.

221
14.4. Реализация динамических структур
с помощью массивов

Операции динамического выделения и освобождения памяти -


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

/-ЫЙ элемент вспомогательного массива содержит для каждого /-го


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

Аналогичным образом, для создания бинарного дерева можно


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

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

Обращаем Ваше внимание на то, что при работе с такими


структурами необходимо контролировать возможный выход индек­
сов за границы массива (нарушение индексации).
И, наконец, важное заключительное замечание. Рассмотрен­
ный способ реализации позволяет использовать преимущества ди­
намических структур (например, сортировать структуры из гро­
моздких элементов данных без их физического перемещения в па­
мяти) и при этом не расходовать время на выделение и освобожде­
ние динамической памяти для каэюдого элемента данных.
15. СОРТИРОВКА

Под сортировкой понимают процесс перестановки объектов


данного множества в определенном порядке. Цель сортировки - об­
легчить последующий поиск элементов в отсортированном множе­
стве. В этом смысле элементы сортировки присутствуют во многих
задачах прикладного программирования.
Зависимость выбора алгоритмов решения задачи от структуры
данных - явление довольно частое. В случае сортировки эта зависи­
мость настолько сильна, что методы сортировки обычно разделяют
на две категории:
• сортировка массивов;
• сортировка последовательных файлов.
Эти две разновидности сортировок часто называют соответст­
венно внутренней (сортировка массивов) и внешней (сортировка
файлов) сортировками. Это объясняется тем, что массивы распола­
гаются во "внутренней" (оперативной) памяти ЭВМ и для нее харак­
терен быстрый произвольный доступ (прямой доступ). Файлы же
хранятся в более медленной, но более вместительной "внешней" па­
мяти, т.е. на запоминающих устройствах с механическим передви­
жением (магнитных дисках и лентах). Указанное существенное раз­
личие можно наглядно продемонстрировать на примере сортировки
пронумерованных карточек.
1. Представление карточек в виде массива с прямым доступом
(рис. 58) означает, что все карточки одновременно видны и равно­
доступны.

0 8 9 1

2 6 4 5

Рис. 58. Произвольный (прямой) доступ

2. Представление карточек в виде последовательного файла


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

224
ir
Рис. 59. Последовательный доступ

Терминология и обозначения. Будем считать, что нам даны


элементы

Сортировка означает перестановку этих элементов в таком порядке

что при заданной функции упорядочения f справедливо отношение


f(a,,)<f(a,,)<...<f(a,J
В частном случае, функция упорядочения может не вычис­
ляться по какому-либо специальному правилу, а может содержаться
в каждом элементе в виде явной компоненты (поля). Ее значение на­
зывается ключом элемента. Следовательно, для представления эле­
мента а,, особенно хорошо подходит структура.
В соответствии со сказанным, определим структурный тип
ELEMENT, который будем использовать в последующих алгоритмах
сортировки, следующим образом:
/ / Структурный тип для элемента массива
struct ELEMENT

Int key; // Ключ сортировки


// Описание других компонентов элемента
};

Здесь "другие компоненты" - это все существенные данные об эле­


менте, а поле key - ключ служит лишь для идентификации элементов
при сортировке.
Таким образом, в алгоритмах сортировки ключ - единственная
учитываемая компонента и при учебном рассмотрении алгоритмов
нет необходимости определять остальные поля элемента массива.
Следует иметь в виду, что выбор в качестве типа ключа целого типа
произволен. Ясно, что точно также можно использовать и любой
другой тип, на котором задано отношение всеобщего порядка (упо-

225
рядоченный тип).
Метод сортировки называется устойчивым, если относитель­
ный порядок элементов с одинаковыми ключами не меняется при
сортировке. Устойчивость сортировки часто бывает желательной,
если элементы упорядочены по каким-то вторичным ключам, т.е. по
свойствам, не отраженным в первичном ключе.

15.1. Сортировка массивов

Основное требование к методам сортировки массивов - эко­


номное использование памяти. В этом смысле говорят, что сорти­
ровку нужно выполнять in site (на том же месте) и другие методы,
использующие копирование массива, для нас не представляют инте­
реса.
Удобной мерой эффективности алгоритмов сортировки "на
месте" является число
с (Compare)
необходимых сравнений ключей и число
М (Move)
необходимых пересылок элементов.
Хотя эффективные алгоритмы сортировки требуют порядка

сравнений, где N - число элементов сортируемого массива, все же


сначала обсудим несколько более простых методов сортировки, ко­
торые требуют порядка
С-TV'
сравнений, по следующим трем причинам.
1. Простые методы особенно хорошо подходят для разъясне­
ния свойств большинства принципов сортировки.
2. Программы, основанные на этих методах, легки для пони­
мания и коротки (следует помнить, что программы также занимают
память!).
3. Хотя сложные алгоритмы требуют меньшего числа опера­
ций, но эти операции более сложны. Поэтому при достаточно ма­
лых значениях Л^ простые методы работают также быстро, но их не
следует использовать при больших N,
Методы сортировки массивов "на месте" можно разбить на три
основных класса:

226
• сортировка выбором;
• сортировка вставками;
• сортировка обменом.
Рассматриваемые программы будут работать с указателем агг
на массив, компоненты которого нужно отсортировать "на месте", и
структурным типом ELEMENT^ определенным выше.
Указатель агг на массив можно определить так:
±пЬ / / Размер массива
// Указатель на сортируемый массив
ELEMENT *агг = new ELEMENT[ s } ;

В простейшем случае элементы массива


агг[ О ]г arrf 1 ], ..., arr[ s-1 ]
будем располагать в порядке не убывания их ключей:
arr[k^],key < arr[k2^.key < ... < arr[k^]Леу

Сортировка выбором. Выбирается элемент с наибольшим


значением ключа и меняется местами с последним. Затем то же са­
мое повторяется для 5-1 первого элемента, найденный элемент с
наибольшим значением ключа меняется местами с предпоследним
элементом и т.д. (рис. 60).

max s-1

max s-2 s-1


и т.д.
Рис. 60. Сортировка простым выбором

Сортировка включениями. Элементы разделяются на уже


готовую последовательность (упорядоченную) и неупорядоченную
(рис. 61). В начале упорядоченная часть содержит только один эле­
мент. Очередной элемент из начала неупорядоченной части вставля­
ется на подходящее место в упорядоченную часть. При этом упоря­
доченная часть удлиняется на один элемент, а неупорядоченная
часть — укорачивается. Сортировка заканчивается при исчезновении
неупорядоченной части.

227
1 1-1 S-1
—\ \ 1 г
i= 1,2, ..., s-1

Вставим на подходящее место


или оставим на месте
Рис. 61. Сортировка простыми включениями

Сортировка обменом. Основная характеристика процесса -


обмен местами двух соседних элементов (перестановка), если они
расположены не так, как требует отсортированный массив (рис. 62).
На приведенном рисунке изображен только один шаг (просмотр).
Сортировка массива гарантируется после s-\ просмотра.

S-2 S-1
и т.д.
Рис. 62. Сортировка простым обменом

15.2. Сортировка массива простым выбором

Метод основан на следующем правиле.


1. Выбирается элемент с наибольшим значением ключа.
2. Он меняется местами с последним элементом агг[ 5-1 ]. Эти
операции затем повторяются с оставшимися первыми ^-1 элемента­
ми, затем - с s-2 первыми элементами и т.д. до тех пор, пока не ос­
танется только один первый элемент - наименьший. Пример сорти­
ровки массива простым выбором приведен на рис. 63, в соответст­
вии с которым, программу можно представить следующим образом:
/ / Просмотр неотсортированных начальных сегментов массива
// (вначале - весь массив, затем - сегмент из первых slze-1
// элементов и т,д.)
£о1т( L -= size-1; L >= 1; L— ;
{
// Присвоить indmax и max индекс и значение элемента
// массива с наибольшим значением ключа из
// агг[ О ]..arrf L ]
// . . .
// Поменять местами агг[ indmax ] и агг[ L ]
arrf indmax ] = arrf L ]; arrf L ] = max;

228
Начальные значения 44 55 12 42 94 18 06 67
ключей элементов
массива 44 55 12 42 67 18 06 1 94
м
44 55
^
12 42 06
«"1 67 94

44 18 12 42 06 1 55 67 94
м—
Об 18 12 42 1 44 55 67 94
Ф
06 18
."1 42 44 55 67 94

06 18 42 44 55 67 94

06 12 18 42 44 55 67 94
Рис. 63. Сортировка массива простым выбором

Полный текст программы, иллюстрирующей все разработан­


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

/*
Файл TestSortArr.срр. Тестирование сортировки динамически
размещенных массивов с использованием различных методов.
Определение методов сортировки приведено в файле
SortArr.срр.
Определение Функций размещения массива в динамической па­
мяти и освобождения занятой динамической памяти находится в
файле А1 ocFreeDM. срр,
Определение функций ввода и печати значений элементов мас­
сива дано в файле SortlnOut.срр.
Заголовочный файл программного проекта дан в файле Sor­
tArr. h.
Давыдов В.Г. Консольное приложение. Visual C++ 6
Ч

// Включаемый файл программного проекта для сортировки


// массивов
^include "SortArr.h"
int main ( // Возвращает О при успехе
±nt ArgCr // ARGument Counter: число
// аргументов в командной строке
// ARGument Value: массив указателей на аргументы
// командной строки (ArgV[ О ] - .ехе файл, в
// интегрированной среде программирования известен и не

229
// задается; ArgV[ 1 ] - файл ввода; ArgVf 2 ] - файл
// вывода)
сЪаг *ArgV[ ] )
{
// Проверка числа аргументов командной строки
±f( ArдС /= 3 ;
{
printf(
"\п Ошибка 5. В командной строке должно быть три аргумента: "
" \ л Имя_проекта. ехе имя_файла_ввода имя_файла__вывода \п" ) ;
ех11 ( 5 ) ;
}

// Размещение массивов в динамической памяти:


// а, al - указатели на начало массивов в динамической
// памяти; S, 9 - размеры массивов
AllocArrDMf аггг 8 ) ;
// Для сортировки простыми включениями
AllocArrDMl( arrl, 9 ) ;

// Сортировка массива простым выбором


ReadArr ( arr, ArgV[ 1 ] ) ;
WriteArr ( arrг "\n Сортировка массива простым "
"выбором. Массив до сортировки \п '\ ArgV[ 2 ],
"w" ) ;
SelectSort ( arr ) ;
WriteArr ( arr, "\n Массив после "
"сортировки \n ", ArgVf 2 ], "a" ) ;

// Сортировка массива простыми включениями


ReadArrl ( arrl, ArgV[ 1 ] ) ;
WriteArrl ( arrl, "\n\n Сортировка массива простыми "
"включениями. Массив до сортировки \п ", ArgV[ 2 ],
"а " ) ;
InsertSort ( arrl ) ;
WriteArrl ( arrl, "\n Массив после "
"сортировки \п ", ArgV[ 2 ], "а" ) ;

// Сортировка массива методом "пузырька"


ReadArr ( arr, ArgV [ 1 ] ) ;
WriteArr ( arr, "\n\n Сортировка массива методом "
"\"пузырька\". Массив до сортировки \п ", ArgV[ 2 ],
"а " ) ;
BubbleSort( arr ) ;
WriteArr( arr, "\п Массив после "
"сортировки \п ", ArgV[ 2 ], "а" ) ;

// Сортировка массива сложным выбором


ReadArr ( arr, ArgV [ 1 ] ) ;
WriteArr( arr, "\n\n Сортировка массива сложным "

230
"выбором. Массив до сортировки \п ", ArgVf 2 ],
"а " ) ;
TreeSort( arr ) ;
WriteArr( arr, "\n Массив после "
"сортировки \п ", ArgV[ 2 ], "а" ) ;

// Сортировка массива методом Шелла


ReadArr( arr, ArgV[ 1 ] ) ;
WriteArr ( arr, "\n\n Сортировка массива методом "
"Шелла. Массив до сортировки \п ", ArgV[ 2 ], "а" ) ;
ShellSort ( arr ) ;
WriteArr ( arr, "\n Массив после "
"сортировки \п ", ArgV[ 2 ], "а" ) ;

// Сортировка массива методом Хоора (не рекурсивный


// вариант)
ReadArr ( arr, ArgV[ 1 ] ) ;
WriteArr ( arr, "\n\n Сортировка массива методом "
"Хоора. Массив до сортировки \п ", ArgV[ 2 ], "а" ) ;
Quicksort( arr ) ;
WriteArr ( arr, "\n Массив после "
"сортировки \п ", ArgVl 2 ], "а" ) ;

// Сортировка массива методом Хоора (рекурсивный вариант)


ReadArr ( arr, ArgV[ 1 ] ) ;
WriteArr ( arr, "\n\n Рекурсивная сортировка "
"Хоора. Массив до сортировки \п ", ArgV[ 2 ], "а" ) ;
QuickSortl ( ) ;
WriteArr ( arr, "\п Массив после "
"сортировки \п ", ArgV[ 2 ], "а" ) ;
//ккккккккккккккккккккккккккккккккккккккккккккккккккккккк
// Освобождение динамической памяти, занятой массивами
FreeArrDM( arr ) ;
FreeArrDM( arrI ; ;

xetujrn 0;
;
/*
Файл SortArr.h. Подключение стандартных заголовочных фай­
лов, объявление объектов с описателями класса хранения "внеш­
ний", типов элементов сортируемого массива, стека и прототи­
пов функций. Используется как заголовочный файл в программном
проекте для сортировки массивов.
Давыдов В.Г. Консольное приложение. Visual C++ 6
V
// Предотвращение возможности многократного подключения
#ifndef SORTARR__H
^define SORTARR Н

231
^include <stdio.h> // Для ввода-вывода
^include <stdlib.h> // Для exit ( )

const ±nt M = 16; // Размер стека отложенных


// сегментов: >= (1од2(size)+1)

// Структурный тип для элемента массива


struct ELEMENT
{
±nt key; // Ключ сортировки
// Описание других компонент элемента
} ;

// Объявления внешних объектов


extern ELEMENT
*arr; // Указатель на сортируемый массив
extern ±nt size; // Размер сортируемого массива
// Указатель на сортируемый массив для сортировки
// простыми вставками
extern ELEMENT
*arrl;
extern ±nt sizel; // Увеличенный на единицу размер
// сортируемого массива

// Структурный тип для элемента стека


struct STACK
{
±nt 1; // Левая граница сегмента
±nt г; // Правая граница сегмента
} ;

// Прототипы функций (имена параметров в прототипе не


// используются и, поэтому, мы их не записываем
void AllocArrDM( ELEMENT *&, int );
void. AllocArrDMl ( ELEMENT *&, int );
void ReadArr( ELEMENT [ ], char * );
void ReadArrl ( ELEMENT [ ] , char * ) ;
void SelectSort( ELEMENT [ ] ) Г
void WriteArr ( ELEMENT [ ], char *, char *, char * );
void WriteArrl( ELEMENT [ ], char *, char *, char * );
void InsertSort ( ELEMENT [ ] ) ;
void BubbleSort( ELEMENT [ ] );
void TreeSort ( ELEMENT [ ] );
void Sift( int, int, ELEMENT [ ] );
void ShellSort ( ELEMENT [ ] );
void Quicksort ( ELEMENT [ J );
void Push ( int, int, STACK [ ], int & );
void Pop ( int Sc, int &, STACK [ ], int & );
void QuickSortl ( void ) ;
void Split ( i n t , int );
void FreeArrDM( ELEMENT *& );

232
iendlf
_
Файл AlocFreeDM. cpp. Размещение одномерного массива в ди­
намической памяти и освобождение динамической памяти, занятой
массивом.
Используется в программном проекте для сортировки масси­
вов.
Давыдов В.Г. Консольное приложение. Visual C-h-h 6
V
// Включаемый файл программного проекта
Unciude "SortArr.h"

// Размещение сортируемого массива в динамической памяти


void AllocArrDMi
ELEMENT *&arr, // Указатель на начало массива в
// динамической памяти (передаем
// по ссылке - это ответ)
±пЬ S ) // Число элементов массива
{
// Контроль корректности размера массива
±f( S < 2 )
{
printf ( "\п Предупреждение 10. Массив должен "
"содержать более двух элементов \п (задан размер, ''
" равный %d) . Принимается размер массива, равный'^
" 2. \п Выполнение программы продолжается ", s ) /
S = 2/
}
// Размещение массива в динамической памяти
агг = new ELEMENT[ s ] ;
±£( arr -= NULL )
{
printf( "\n Ошибка 20. Размещение массива в "
"динамической памяти не выполнено ");
exit ( 20 ) ;
}

// Инициализация массива нулевыми значениями


£ог ( int 1 = 0; i<s; i4-+ )
arr [ 1 ] . key = 0;

// Инициализация размера массива


size = s;

2retuz*n/
}

// Размещение массива в динамической памяти для сортировки


// простым выбором

233
void AllocArrDMl (
ELEMENT *&arrl, // Указатель на начало массива в
// динамической памяти (передаем
// по ссылке - это ответ)
±nt S ) // Число элементов массива
{
// Контроль корректности размера массива
±£( S < 3 )
{
print f ( "\п Предупреждение 10. Массив должен "
"содержать более трех элементов \п (задан "
"размер, равный %d) . Принимается размер "
"массива г равный 3,\п Выполнение программы
"продолжается " ) ;
S = 3;
}

// Размещение массива в динамической памяти


arrl = new ELEMENT[ s ];
±f( arrl == NULL )
(
printf( "\n Ошибка 20. Размещение массива в "
"динамической памяти не выполнено " ) ;
exit ( 20 ) ;
}

// Инициализация массива нулевыми значениями


for( int i = 0; i<s; i-h-h )
arrl[ i ] . key = 0;

// Инициализация размера массива


sizel = s;

return;
}

// Освобождение динамической памяти, занятой массивом


void FreeArrDM (
ELEMENT *&arr ) // Указатель на начало массива в
// динамической памяти (передаем
// по ссылке - это и ответ)
{
±f( arr != NULL )
{
delete [ ] arr; arr NULL;

retvLrn;

Файл SortlnOut.срр. Функции чтения значений элементов мас­


сива из файла данных и печати их значений в файл результатов.

234
Используется в программном проекте для сортировки масси­
вов.
Давыдов В.Г. Консольное приложение^ Visual C++ 6
V
// Включаемый файл программного проекта
§ include "SortArr.h"

// Чтение значений элементов массива из файла данных


void ReadArr (
ELEMENT arr[ ] , // Сортируемый массив
// Указатель на имя файла с исходными данными
cha.r *pInpFile )
{
FILE *pStrInp; // Указатель на структуру со
// сведениями о файле ввода
±nt i, // Индекс элемента массива
RetCode, // Возвращаемые значения fscanf( )
// или их сумма
RetCodel;// Возвращаемое значение fclose( )

// Открытие файла ввода данных


pStrlnp = fopen( pInpFiler "г" ) ;
±£( pStrlnp -= NULL )
{
printf( "\n Ошибка 30. Файл %s для чтения не открыт"
" \п", pInpFile ) ;
exit ( 30 ) ;
}

// Ввод значений элементов массива


Ret Code --= О;
for( i = 0; 1 < size; i++ )
{
RetCode += fscanf( pStrlnp,
" %d", &( arr[ i ].key ) ) ;
}
±f( RetCode < size )
{
printf( "\n Ошибка 40. Ошибка чтения данных из файла"
" %s \п", pInpFile ) ;
exit ( 40 ) ;
}

// Закрытие файла ввода


RetCodel = fclose( pStrlnp ) ;
±f( RetCodel == EOF )
{
printf( "\n Ошибка 50. Файл %s не закрыт \n ",
pInpFile ) ;
exit ( 50 ) ;
}

235
return/
}

// Чтение значений элементов массива из файла данных для


// сортировки простыми включениями
void. ReadArrl (
ELEMENT arrl [ ], // Сортируемый массив
// Указатель на имя файла с исходными данными
char *pInpFile )
{
FILE *pStrInp; // Указатель на структуру со
// сведениями о файле ввода
int i, // Индекс элемента массива
RetCode^ // Возвращаемые значения fscanf( )
// или их сумма
RetCodel; // Возвраш.аемое значение f close ( )
\
// Открытие файла ввода данных
pStrlnp = fopen( pInpFile, "г" ) ;
±f( pStrlnp -= NULL )
{
printf( "\n Ошибка 30. Файл %s для чтения не "
"открыт \л", pInpFile ) ;
exit ( 30 ) ;
}

// Ввод значений элементов массива ~ обратите внимание на


// то, что первый элемент массива (служебный -
// "барьер") не заполняется
Ret Code = О;
fori i = 0; 1 < size; i++ )
{
RetCode += fscanf( pStrlnp, " %d",
&( arrl[ i+1 J.key ) ) /
}
±f( RetCode < size )
{
prmtf ( "\n Ошибка 40. Ошибка чтения данных из "
"файла %s \п", pInpFile ) ;
exit ( 40 ) ;
}
// Закрытие файла ввода
RetCodel = fclose ( pStrlnp ) ;
±f( RetCodel =- EOF )
{
prmtf( "\n Ошибка 50. Файл %s не закрыт \n ",
pInpFile ) ;
exit ( 50 ) ;
}

return/

236
// Печать значений элементов массива в файл результатов
void. Wri teArr (
ELEMENT arr[ ]^ // Сортируемый массив
char '^'pMSG^ // Указатель на строку-сообщение о
// печатаемом массиве
char *pOutFile^// Указатель на имя.расширение
// файла результатов
char *Mode ) // Указатель на режим открытия файла
{
FILE *pStrOut; // Указатель на структуру со
// сведениями о файле результатов
int 2, // Индекс элемента массива
RetCodel; // Возвращаемое значение fclose( )
// Открытие файла вывода
pStrOut = fopen ( pOutFlle, Mode ) ;
±£( pStrOut = - NULL )
{
printf( "\n Ошибка 60. Файл %s для вывода не '
"открыт \л", pOutFile ) /
exit( 60 ) ;
}

// Печать значений элементов массива с заголовком


fprintf( pStrOut, pMSG ) ;
tor( 1 = 0; i < size/ i++ )
{ // Элементы выводятся по 6 в каждой строке из
// расчета по 10 позиций на каждый элемент
fprintf( pStrOut, "%10d", arr[ i ].key ) ;
xf( ( ( i-hl ) % 6 ) == 0 )
{
fprintf( pStrOut, "\n " ) ;
}
}

// Закрытие файла вывода


RetCodel = fclose( pStrOut ) ;
±f( RetCodel == EOF )
{
printf( "\n Ошибка 70. Файл %s не закрыт \n
pOutFile ) ;
exit ( 70 ) /

return;
}

// Печать значений элементов массива в файл результатов для


// сортировки простыми включениями
void WriteArrl(
ELEMENT arrl [ ] г // Сортируемый массив

237
char *pMSG, // Указатель на строку-сообщение о
// печатаемом массиве
cha.2: *pOutFile, // Указатель на имя .расширение файла
// результатов
cbajT *Mode ) // Указатель на режим открытия файла

FILE *pStrOut; // Указатель на структуру со


// сведениями о файле результатов
±пЬ i, / / Индекс элемента массива
RetCodel; // Возвращаемое значение fclose( )

// Открытие файла вывода


pStrOut = fopen ( pOutFile, Mode ) ;
±f( pStrOut == NULL )
{
printf( "\n Ошибка 60. Файл %s для вывода не "
"открыт \л", pOutFlle ) /
exit ( 60 ) ;
}

// Печать значений элементов массива с заголовком


fprintf( pStrOut, pMSG ) ;
for( i = 1; i < sizel; i-h-h )
{ // Элементы выводятся по 6 в каждой строке из расчета
// по 10 позиций на каждый элемент
fprintf( pStrOut, "%10d", arrlf i ].key ) ;
±f( ( 1 % 6 ) == 0 )
{
fprintf ( pStrOut, "\л " ) /
}
}

// Закрытие файла вывода


RetCodel = fclose( pStrOut ) ;
±f( RetCodel == EOF )
{
printf( "\n Ошибка 10. Файл %s не закрыт \n ",
pOutFile ) ;
exit ( 70 ) ;
}

return/
}

Файл SortArr.cpp.
Функции сортировки динамически размещенного массива по не­
убыванию:
* простая сортировка массива выбором;
* простая сортировка массива обменом;
* простая сортировка массива вставками;
* сортировка массива с помощью двоичного дерева;
* сортировка Шелла;
* не рекурсивная сортировка Хоора;

238
'*' рекурсивная сортировка Хоора.
Используется в программном проекте для сортировки масси­
вов.
Давыдов В.Г. Консольное приложение, Visual C++ 6
V
// Включаемый файл программного проекта
^include "SortArr.h"

// Определения объектов с описателем класса хранения внешний.


// Их объявление имеется в заголовочном файле проекта и эти
// объекты доступны в других файлах проекта
ELEMENT *arr/ // Указатель на сортируемый массив
±nt size; // Размер сортируемого массива
ELEMENT *arrl; // Указатель на сортируемый массив
// для простой сортировки
// вставками
int sizel; // Увеличенный на единицу размер
// сортируемого массива для
// простой сортировки вставками

// Эти объекты определяем в данном месте, чтобы при


// рекурсивных вызовах в сортировке Хоора они не
// создавались заново
ELEMENT сору, // Копия элемента массива
median/ // Медиана разделяемого сегмента в
// сортировке Хоора
int 1, // Индекс кандидата на обмен слева
J/ // Индекс кандидата на обмен справа
// в сортировке Хоора

// Сортировка массива простым выбором - по неубыванию


void SelectSort (
ELEMENT arrf ] ) // Сортируемый массив
{
JLXm •LJr // Индекс последнего элемента из
// пока неупорядоченных
indmax. // Индекс наибольшего элемента среди
// 1..L

к; // Индекс анализируемого элемента


ELEMENT max; // Для наибольшего элемента среди
// 1. .L

// Просмотр неотсортированных начальных сегментов массива


// (вначале - весь массив, затем - сегмент из первых
// size-1 элементов и т.д.)
£ог( L ^ size-1; L >= 1; L-- )
{
// Присвоить indmax индекс элемента массива
// наибольшим значением ключа из агг[ О ]. .агг[ L ]
indmax = О; max = arr[ О ];
for( к = 1; к <= L; к++ )

239
±f( arr[ к ] , key > max. key )
(
indmax = k; max = arrf к ];
}
}

// Поменять местами arr[ indmax ] и arrf L ]


arrf indmax ] = arrf L ]; arrf L ] = max;
}

return;
)

// Сортировка массива простыми включениями - по неубыванию


void InsertSort(
ELEMENT arrlf ] ) // Сортируемый массив
{
// Используются следующие глобальные объекты:
// i - индекс вставляемого элемента;
// J - индекс элемента в упорядоченном сегменте;
// сору = arrf i ]
// Перебор вставляемых элементов
£ог( i=2; 1 <= size; i++ )
{
copy = arrlf 1 ];
arrlf 0 ] = copy;// Установка "барьера"
// Вставка copy на нужное место
j = i-1 ;
while( copy.key < arrlf j J.key )
{
// Сдвиг
arrlf j+1 ] = arrlf j ]; j--;
}
arrlf j+1 ] = copy;
}

return;
}

// Сортировка массива простым обменом - по неубыванию (метод


// "пузырька")
void. BubbleSort (
ELEMENT arrf ] ) // Сортируемый массив
{
int к; // Индекс анализируемого элемента
ELEMENT temp; // Для перестановки элементов
int sorted^ // 1=0 (отсортирован)
change; // !=0 (были перестановки)

240
sorted = О;
// Цикл проходов
while ( !sorted )
{
change = О;
fori к = 1; к < size; к++ )
{
±f( arr[ k-1 ].key > arr[ к ].key )
{
temp = arr[ к ]; arr[ к J = arr[ k-1 J;
a r r / " k-1 ] = temp;
change = 1;
}
}
sorted = !change;
}

retvum;
}

// Сортировка массива сложным выбором с использованием


// пирамиды - двоичного дерева
// Просеивание
void Sift(
Int rooty // Корень дерева или поддерева
Int last у // Последняя вершина в дереве
ELEMENT arr[ ] ) // Сортируемый массив
{
// 1 - позиция "дырки" (объект определен на внешнем
// уровне)
Int jl, // j1 = 2*1 -- следующая вершина
// снизу и слева для i
j2; // j2 = 2*1 + 1 - следующая вершина
// снизу и справа для 1
// j - претендент из jl и j2 на заполнение "дыры"
// (объект определен на внешнем уровне)
// сору - просеиваемый элемент (объект определен на
// внешнем уровне)
Int found; // 1 (нашли место для вставки сору)
// Подготовка
сору = агг[ root-1 ]; 1 = root; found = 0;
while ( .'found )
{
// Определение jl и j2 для зафиксированного i
jl = 2*i; j2 - jl-hl;
// Анализ вариантов заполнения "дыры"
lf( jl > last )
{ // Следующего уровня внизу нет
found = 1;
}

241
etlse
{ // Следующий внизу уровень есть
±£( jl == last )
{
J = Jl/
}
else
{
j = ( arrf J1-1 J.key >= arrf j2-l ] . key )
? jl : j2;
}
// Выяснение, кто заполняет "дыру"
±f( arr[ j-1 ],key <= copy.key )
{
found = 1;
}
else
{
arr[ i-1 ] = arr[ j-1 ]; i=j;
}
}
}
arrf i-1 ] = copy;
return;
}

// Сортировка
void TreeSort (
ELEMENT arrf ]) // Сортируемый массив
{
±nt temproot, // Индекс корня частичного поддерева
templast; // Последний элемент в
// неупорядоченном поддереве
ELEMENT tempcopy; // Для перестановки элементов

// Начальная подготовка дерева


for( temproot = size/2; temproot > 0; temproot-- )
{

Sift( temproot, size, arr ) ;


,^
// Сортировка
for( templast = size; templast >= 2; templast-- )
{
// Переставить максимум из корня дерева на
// окончательное место
tempcopy = arrf О ]; arrf О ] = arrf templast-1 ];
arrf templast-1 ] = tempcopy;
// Просеять новый корень на место - восстановить
// дерево
Sift( 1, templast-1, arr ) ;

242
}

xretuxm/
}

// Сложная сортировка массива вставками (метод Шелла)


void ShellSort (
ELEMENT arr[ ] ) // Сортируемый массив
{
izit d, // Дистанция Шелла
fillpos; // Местоположение "дыры"
// i - индекс анализируемого элемента (объект определен
// на внешнем уровне)
// J - индекс претендента слева на заполнение "дыры"
// (объект определен на внешнем уровне)
// сору = arrfi] (объект определен на внешнем уровне)
int found; // 1 (нашли место для вставки сору)

d = size;
while ( d > 1 )
{
d = d/2;

// Отсортировать вставками при текущем d


£or( 1 = d; i < size; i++ )
(
copy = arr[ 1 ]; fillpos = i;

// Найти место вставки copy


found = 0;
do
{ .
j = fillpos - d;
±f( j < 0 )
{ // Претендента слева нет
found = 1;
}
else
{ // Претендент слева больше - сдвиг
if( arr[ j ].key <= copy,key )
{
found = 1;
}
else
{
arr[ fillpos ] = arr[ j ];
fillpos ^ j ;
}
}
}
while( !found ) ;
// Вставка copy
arrf fillpos ] = copy;

243
retuxm/
}

// Быстрая сортировка Хоора - нерекурсивный вариант

// Занесение в стек сегментов


void. Push (
±пЬ left^ // Левая граница сегмента
int rights // Правая граница сегмента
STACK s[ 7/ // Стек границ сегментов
int &sp ) // sp - указатель вершины стека
{
// В стек заносятся только сегменты из двух или более
// элементов
±f( ( right-left ) >= 1 )
{
sp+ + / s[ sp ],1 = left; s[ sp J.r = right/
}

return;
}

//
// Извлчение сегмента из стека
void Pop (
int &i, // Указатель на левую границу
// сегмента
int &r, // Указатель на правую границу
// сегмента
STACK s[ ], // Стек границ сегментов
int &sp ) // Указатель вершины стека
(
1 = s[ sp J.l; г = s[ sp ].r; sp--;
re trim;
}
//
// Быстрая сортировка массива - нерекурсивный вариант
void Quicksort(
ELEMENT arr[ ] ) // Сортируемый массив
{
int left, // Левая граница разделяемого
// сегмента
right; // Правая граница разделяемого
// сегмента
// i - индекс кандидата на обмен слева - направо (объект
// определен на внешнем уровне)
// j - индекс кандидата на обмен справа - налево (объект
// определен на внешнем уровне)
// median - медиана разделяемого сегмента (объект

244
// определен на внешнем уровне)
// сору - для перестановки кандидатов (объект определен
// на внешнем уровне)
STACK s[ М ]; // Стек границ сегментов
±Tib sp; // Указатель вершины стека

sp = -1 ; // Вначале стек пуст


Push ( О, size-lr S, sp );
while ( sp >= О )
{
// Подготовка верхнего сегмента из стека для
// разделения
Pop ( left, right г s , sp );
median = arr[ ( left+rlght )/2 ]; i = left;
J = right;

// Разделение текущего сегмента


while ( 1 <= J )
{
// Найти кандидата на обмен слева
while ( arr[ i ].key < median.key ) i ++;
// Найти кандидата на обмен справа
while ( median.key < arr[ j ],key ) j - - ;

// Обмен, если кандидаты находятся в разных


// подсегментах
if( 1 <= j )
{
copy = arr[ 1 ]; arr[ i ] = arr[ j ];
arr[ j ] = copy; 1++; j - - ;
I
}

// Поместить в стек сначала белее длинный подсегмент,


// а затем - более короткий
if( ( j-left ) < ( right-1 ) )
{ // Леваый подсегмент - короче
Push ( 1, right, s , sp );
Push ( left, J, s , sp );
}
else
{ // Правый подсегмент - короче
Push ( left, J, s , sp );
Push ( 1, right, s , sp );
}
}

return;
}

// Быстрая сортировка Хоора - рекурсивный вариант

void Quicksort 1 ( void )

245
(
Split ( Or size-1 );

return;
}
//
// Функция разделения сегментов
void Split (
±nt leftr // Левая граница сегмента
int right ) // Правая граница сегмента
{
xf( ( right-left ) <= О )
(
return;
}
else
(
// Подготовка
median = arr[ ( left + right )/2 ]; i = left;
j = ri gh t;

// Разделение
while( i <= j )
f
// Найти кандидата на обмен слева
while ( arr[ i ].key < median.key ) i + + ;
// Найти кандидата на обмен справа
while ( median.key < arrf j J.key ) j - - ;

// Обменr если кандидаты находятся в разных


// подсегментах
if( i <= J )
I
copy = arrf 1 J; arrf i ] = arrf j ];
arrf J ] = copy; i++; j - - ;
}
}

// Финал - разделить сначала более короткий сегмент


if( ( j-left ) < ( rlght-1 ) )
{ // Леваый сегмент - короче
Split ( leftr J ); Split ( 1, right );
}
else
I // Правый сегмент - короче
Split ( 1, right ); Split ( left, j );
}
}

return;

Для файла данных, приведенного ниже.

246
44 55 12 42 94 18 6 61

файл результатов имеет следующий вид:

Сортировка массива простым выбором. Массив до сортировки


44 55 12 42 94 18
6 61
Массив после сортировки
6 12 18 42 44 55
61 94

Сортировка, массива простыми включениями. Массив до сортировки


44 55 12 42 94 18
6 61
Массив после сортировки
6 12 18 42 44 55
61 94

Сортировка массива методом "пузырька". Массив до сортировки


44 55 12 42 94 18
6 61
Массив после сортировки
6 12 18 42 44 55
61 94

Сортировка массива сложным выбором. Массив до сортировки


44 55 12 42 94 18
6 61
Массив после сортировки
6 12 18 42 44 55
61 94

Сортировка массива методом Шелла. Массив до сортировки


44 55 12 42 94 18
6 61
Массив после сортировки
6 12 18 42 44 55
61 94

Сортировка массива методом Хоора. Массив до сортировки


44 55 12 42 94 18
6 61
Массив после сортировки
6 12 18 42 44 55
61 94

Рекурсивная сортировка Хоора. Массив до сортировки \


44 55 12 42 94 18
6 61
Массив после сортировки
6 12 18 42 44 55
61 94 1
В приведенной программе на данном этапе заслуживают также
внимания следующие решения.
1. Размещение сортируемого массива в динамической памяти
и освобождение динамической памяти.
2. Механизм передачи сортируемого массива в функции сор­
тировок.

247
3. Оформление включаемого файла программного проекта.

Эффективность сортировки простым выбором. Число


сравнений ключей не зависит от начального порядка ключей. Опе­
рация сравнения выполняется в теле цикла с управляющей перемен­
ной к и средним числом повторений size/2. Этот цикл, в свою оче­
редь, находится в теле цикла с управляющей переменной L и числом
повторений size-\. Таким образом, число сравнений
С = {size -1) • size 12
Число пересылок, напротив, зависит от начального порядка
ключей. Если принять, что операция сравнения в теле цикла по к да­
ет результат "истина" в половине случаев, то среднее число пересы­
лок в этом цикле равно size!А. Цикл по L, как указывалось выше, вы­
полняется sizeA раз и в теле цикла выполняется три пересылки и
цикл по к. С учетом этого число пересылок
М = (3 + size 14) • {size -1)

Получаем, что при сортировке простым выбором и число сравнений,


и число пересылок пропорционально size'^.

15.3. Сортировка массива простыми включениями

Идея алгоритма пояснена на рис. 61, а пример сортировки мас­


сива данным методом иллюстрирует рис. 64. Алгоритм сортировки
простыми включениями выглядит следующим образом:
for ( 2=2/ JL < sizel/ l+-h )
{
copy = arr [ i ];
// Вставка copy на нужное место среди отсортированных
// элементов массива агг[ О ], . . . , arrf i-l ]
}

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


пересылки, т.е. как бы "просеивать" сору, сравнивая его с очеред­
ным элементом arr[J ] и, либо вставляя сору, либо пересылая arr[j ]
направо и передвигаясь налево. Заметим, что "просеивание" может
закончиться при двух различных условиях.
1. Найден элемент arr[j ] с ключом, меньшим, чем у сору.
2. Достигнут левый конец упорядоченного сегмента и, следо­
вательно, сору нужно вставить в левый конец упорядоченного сег­
мента.

248
44 1 55 12 42 94 18 06 67 i = 2
ключей элементов
44
^ 1
55 1 12 42 94 18 06 67 1=3
массива
1
12 44 55 1 42 94 18 06 67 i = 4
^ 1
12 42 44 55 1 94 18 06 67 1 =5

12 42 44 55 94 1 18 06 67 1 =6

12 18 42 44 55 94 1 06 67 i = 7

06 12 18 42 44 55 941 67 i = 8

Массив отсортирован 06 12 18 42 44 55 67 94

Рис. 64. Пример сортировки массива простыми включениями

Это типичный пример цикла с двумя условиями окончания.


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

0 1 2 ... sJze-1
I г 1 1 ~ "Г
агг
«Барьер» Сортируемый массив
Рис. 65. Использование "барьера" при сортировке массива
ростыми включениями

Прототип функции сортировки массива простыми включения­


ми, ее определение и пример вызова даны в примере, приведенном в
подразд. 15.2. Теперь наступила пора познакомиться с ними.
Обратите внимание на то, что при использовании метода "ле­
вого барьера" размер массива, подлежащего сортировке, увеличен
на один элемент. При этом элемент массива с нулевым индексом яв­
ляется вспомогательным и не сортируется. Таким образом, в масси­
ве сортируются элементы с индексами 1, 2, ..., size 1-1. По этой при­
чине для сортировки простым выбором используются функции раз­
мещения сортируемого массива в динамической памяти, заполнения
его значениями из файла и печати значений элементов массива в
файл, отличающиеся от аналогичных функций для других методов.
Эффективность сортировки. Число С,, сравнений ключей

249
при i-oM просеивании составляет самое большее /, а самое меньшее
- 1. Число М, пересылок (присваиваний) элементов при /-ом
просеивании равно
(С,-1) + 3 = С,+2
Это объясняется тем, что тело цикла while выполняется на
один раз меньше, чем число проверок условия повтора цикла. Три
других пересылки при /-ом просеивании есть:
сору = агг1[ 1 ]; arrl [ О ] = сору/ arrl [ j+1 ] = copy;

Поэтому обш^ее число сравнений и пересылок есть

где (size-2) - число повторов цикла по /,


v/rc'-l

^МАХ ~ 2 1 ' ~ (size +1) • (size - 2) / 2,

QpEfl = (CM,N + С ^ ) / 2 = ((size - 2) + (size +1) • (size - 2) / 2) / 2,


uze-\

^MiN = 3 • (size - 2), MMAX = X (' "^ ^) = ^^^^^ "^ ^) * ^^^^^ " 2) / 2,

Л^сРЕд=(Л^м1м+^мАх)/2 = (3-(^/2е-2) + С9/2еч-5)-(^/^^-2)/2)/2.


C^^iM и Л/^д,^, имеют место, если элементы массива с самого на­
чала упорядочены, а С^^дх и Л/^АХ встречаются, если элементы масси­
ва расположены в обратном порядке.

15.4. Сортировка массива простым обменом


(метод "пузырька")

Данный алгоритм основан на принципе сравнения и обмена


пары соседних элементов до тех пор, пока не будут отсортированы
все элементы массива. Пример сортировки массива методом "пу­
зырька" приведен на рис. 66.
Очевидно, что в наихудшем случае, когда минимальное значе­
ние ключа элемента имеется у самого правого элемента, число про­
смотров равно size-\.
Прототип функции сортировки массива простым обменом, ее
определение и пример вызова даны в примере, приведенном в под-
разд. 15.2. Внимательно изучите их.

250
Начальные значения ключей 44 55 12 42 94 18 06 6
элементов массива (size = 8)
12 55 18 94

42 55 06 94
<— — •
Конец первого просмотра 44 12 42 55 18 06 67 I 94
Обратите внимание как «пузырек» 94 «всплыл» вправо!
< • м •
12 44 18 55
42 44 06 55
Конец второго просмотра 12 42 44 18 06 55 I 67 94
18 44
06 44
Конец третьего просмотра 12 42 18 06 44 I 55 67 94
-•
18 42
06 42
Конец четвертого просмотра 12 18 06 42 ( 44 55 67 94
06 18
Конец пятого просмотра 12 06 18 I 42 44 55 67 94
^4-
06 12
Конец шестого просмотра 06 12 | 18 42 44 55 67 94
И конец сортировки, так как больше перестановок нет!
Рис. 66. Пример сортировки массива простым обменом

Эффективность сортировки. За один проход среднее число


сравнений С^СРЕД равно size/2 (на первом проходе - size-\, а на по­
следнем - 1). При этом среднее число возможных пересылок
А/ксРЕд =l-5*QcpEA (в предположении, что проверяемое условие вы­
полняется в половине случаев). Минимальное количество проходов
равно 1, максимальное - size-l, а среднее - size/2. Следовательно,
СсРЕд =size^ /4, Л/сРЕд =1.5-5/ze^ / 4

15.5. Выводы по простым методам сортировки

1. в простых методах сортировки массивов время сортировки


пропорционально size^.
2. Более точные оценки производительности простых методов

251
сортировки массивов показывают, что наиболее быстрой является
сортировка вставками, а наиболее медленной - сортировка обме­
ном.
3. Несмотря на плохое быстродействие, простые алгоритмы
сортировки следует применять при малых значениях size.
4. Наряду с простыми алгоритмами сортировки массивов су­
ществуют сложные алгоритмы сортировки, обеспечивающие время
сортировки, пропорциональное не size^, а size-logjisize). При больших
значениях size они обеспечивают существенный выигрыш. К их рас­
смотрению мы и переходим.

15.6. Сортировка массива сложным выбором


(с помощью двоичного дерева)

Данная сортировка, по сравнению с сортировкой простым вы­


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

/ . Идея алгоритма. Исходное состояние двоичного дерева


(ключи исходного массива структур агг с числом элементов size)
показано на рис. 67.
а Вначале двоичное дерево подготавливаем таким образом,
чтобы элемент массива с максимальным значением ключа (94)
находился в корне дерева.
• Выбираем элемент с максимальным значением ключа, меня­
ем его местами с последним элементом массива и затем восстанав­
ливаем двоичное дерево (рис. 68). Вновь найденный элемент с мак­
симальным значением ключа (67), который после восстановления
двоичного дерева оказывается опять в его корне, меняется местами с
предпоследним элементом массива и т.д. (всего size-\ раз).
Таким образом, получаем общее число сравнений
С = {size -1) • log 2 {size)

и Какие в связи с этим возникают проблемы?


Как построить двоичное дерево (пирамиду) без дополнитель­
ных затрат памяти с тем, чтобы обеспечить сортировку "на месте",
т.е. обойтись в дереве size вершинами вместо (5/z^*2-l) вершин?
Как организовать дерево в самом начале работы?

252
Всего ( 2*size-1 ) вершин

94

.55^ .42, ,94 67


/ \ / \ / \ / \
44 55 12 42 94 18 06 67

Рис. 67. Исходное состояние двоичного дерева

Вместо выбранного элемента с максимальным значением ключа (94)


появились «дырки»

На восстановление двоичного дерева потребовалось 1од2( size )


сравнений претендентов на заполнение «дырок»: 1, 2, 3

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

2. Построение пирамиды (двоичного дерева) **на месте'* и ее


начальное заполнение,
2.1. Представление двоичного дерева из size элементов в од­
номерном массиве из size элементов. Из вершины двоичного дерева,
в обш;ем случае, идут две дуги вниз (см. выше рис. 57), отсюда тер­
мин - двоичное дерево.
В двоичном дереве могут встретиться следующие случаи:
• у 1 > size - вершина / есть лист дерева;
• j \ ~ size - нет7*2;
• yi < size - есть у 1 иу2.
Таким образом, для представления массива из size элементов

253
требуется дерево с size вершин. У каждой вершины может быть О, 1
или 2 дуги вниз (О -лист, 1 или 2 дуги вниз - промежуточная верши­
на или корень).
Для рассмотренного выше примера мы получаем следующее
дерево из size вершин (рис. 69).

44

55 12
^ ^ о \..^^
42 94 18 06

/ А с:
67

Рис. 69. Двоичное дерево из size вершин

В отличие от первого дерева из (2*^/ze-l)=15 вершин ключ 94


встречается один, а не четыре раза. Ключ 55 встречается один, а не
три раза и т.д. Выигрыш налицо!
2.2. Как подготовить дерево в самом начале, когда в массиве
царит беспорядок?
В первую очередь рассмотрим поддеревья-листья 5, 6, 7, 8
(нижний слой на рис. 69). Каждое из поддеревьев-листьев из одной
вершины и, следовательно, они упорядочены.
Вершина с максимальным номером, у которой есть следующие
снизу вершины, имеет номер size/2 (при size=^ такая вершина имеет
номер 4). Поэтому подготовку дерева надо начинать с вершины с
номером size/2, потом - продолжать с вершины size/2'l и т.д., закон­
чив вершиной 1 (см. рис. 70-73).
Рассмотренный на рисунках процесс будем называть "просеи­
ванием дыры" (sift).

254
Size/2=4
В корне соответствующего поддерева образуем «дыру» путем
копирования соответствующего корню элемента в сору. Ищем
подходящее место для вставки сору и помещаем в него сору.
«Дыра»
1) Г — ;
42 -• j 1 1 42 j 67
4 сору 4 ^^^^ сору 4
67 W 67 3) ^ 42

8 8
Рис, 70. Подготовка двоичного дерева для вершины 4

Size/2 - 1 = 3
1) Г 1
12 -• 1 1 12 18
/ 3 \ сору 3 сору
18 06 ^ 18 <;
3)
06 w 12 06
6 6
Рис. 7 1 . Подготовка двоичного дерева для вершины 3

Size/2 - 2 = 2
2 сору
1) Г :
55 1 55 1 94

67
X 2 \
94
^ сору ^_д

W
X 2)^\Ч 3)
67 94 ^
/4"^
67 55

Рис. 72. Подготовка двоичного дерева для вершины 2

В ходе первоначальной подготовки дерева и в ходе восстанов­


ления дерева после выбора элемента с максимальным значением
ключа из корня выполнялась одна и та же операция, которую мы на­
звали "просеивание". После выбора максимального ключа за каждый
шаг просеивания "дырка" перемещается на один уровень вниз, а
число уровней есть \og2{size). Поскольку выбор элемента с макси­
мальным значением ключа и последующее восстановление дерева
проводятся {size-\) раз, то, как уже указывалось выше, общее коли­
чество сравнений ключей элементов массива пропорционально
{size -1) • log 2 {size).

255
3. функция просеивания
3.1. Идея функции (см. рис. 57)
±ль root^ // Корень дерева ими поддерева
last; // Номер последней вершины дерева
// или поддерева

Возможны следующие ситуации:


а у 1 > last - претендент на заполнение дыры есть сору\
а yi = last - j2 нет и претендент на заполнение дыры есть
тах{ сору.key., arr[J\ ].кеу };
• yi < last - претендент на заполнение дыры есть
тах{ с ору. key, arr[j\ ].key, arr[j2 ].key }.

Size/2 - 3 == 1
a) 6) в)
copy copy
1) Г--1 ( i
44 i 44 i 94 1 44 1
1 copy к 1 • ^ " ^ l " ^ " " ^

94 18 94 18 ^ 18
1 1 1 1 1 1 1 1 ^)ш\ L 1 1
2 3 2 3 - ^ 2 ^;^ 3
67 55
с)) в итоге nonv Ч И М

67

2
44 55

/ 4 fS
42

Рис. 73. Подготовка двоичного дерева для вершины 1

256
3.2. Функции просеивания и сортировки сложным выбором.
Прототипы функций просеивания (Sift), сортировки сложным
выбором с помощью двоичного дерева (TreeSort), их определения и
примеры вызовов содержатся в примере программы, приведенном
выше в подразд. 15.2. Обратите внимание на то, что функция про­
сеивания используется только для внутренних целей и, таким обра­
зом, не является интерфейсной (вызывается из функции сортировки
сложным выбором). Функция же сортировки сложным выбором, на­
против, является интерфейсной. Это означает, что она вызывается
пользователем для сортировки массива.

Эффективность сортировки. Ранее было показано, что чис­


ло сравнений пропорционально {size-\)'\og2isize). Проанализируем
эффективность сортировки более детально.
В функции просеивания Sift цикл while в среднем выполняется
{logjisize))/2 раз, содержит одну пересылку в теле цикла и две пере­
сылки за пределами цикла. В функции TreeSort сортировки сложным
выбором в теле цикла по templast делается три пересылки и пере­
сылки в функции sift, а сам цикл выполняется (size-\) раз. В функ­
ции TreeSort в теле цикла по temproot выполняется функция sift, а
тело цикла повторяется (size/2) раз. Таким образом, число пересы­
лок
Л/ = (3 + 2 + (log2 (size)) 12) - (size -1) + (2 + (logj (size))/2) • size/2
пропорционально size • logj (size).
В функции просеивания Sift в цикле while производится в сред­
нем 2'(\og2(size))/2 = \og2(size) сравнений. Поэтому общее число сравне­
ний
С = (log 2 (size))' (size -1) + (log 2 (size)) • size 12,
что также пропорционально size*\og2(^ize).

15.7. Сложная сортировка вставками


(сортировка Шелла)

Идея алгоритма,
1. Используется несколько проходов.
2. На первом проходе отдельно группируются и сортируются
вставками элементы, отстоящие друг от друга на (i = size/2 позиций.
3. На втором проходе аналогично группируются и сортируют­
ся вставками элементы, отстоящие друг от друга иг. d = d/2 позиций.
4. Аналогично выполняются последующие проходы и сорти-

257
ровка заканчивается последним проходом при d - I (как при про­
стой сортировке вставками).
Иллюстрирующий пример дан на рис. 74.
Следует подчеркнуть, что ускорение сортировки происходит
на первых этапах, когда сортировка вставками производится среди
элементов, отстоящих друг от друга на болыиие расстояния. По
этой причине на втором и последующих этапах перестановки эле­
ментов почти отсутствуют.

Текст функции. Прототип функции ShellSort сложной сорти­


ровки вставками, ее определение и пример вызова приведены выше
в программе, текст которой дан подразд. 15.2.

О выборе последовательности значений d. У нас в примере


использовалась последовательность значений d, равная 1, 2, 4, ...,
d<size (в обратном порядке) и число этапов составляло \og^{size).

Начальные значения ключей 44 55 12 42 94 18 06 67


элементов массива
Первый этап при d=size/2=4

18 55

06 12
В результате получаем 44 18 06 42 94 55 12 67
Второй этап при d = d/2 = 2 • •
06 44

06 12 44 94
В результате получаем 06 18 12 42 44 55 94 67
Третий этап при d = d/2 = 1 (последний): сортировка производится как
простая сортировка вставками
В результате получаем 06 12 18 42 44 55 67 94
Рис. 74. Иллюстрирующий пример для сортировки Шелла

Вместе с тем выявлено, что лучшие результаты получаются,


когда последовательные значения d не кратны друг другу. По этой
причине Д. Кнут (Кнут Д. Искусство программирования для ЭВМ.
Т. 3. - М.: Мир, 1978. С. 342) указывает, что дистанцию нужно вы­
бирать так:

258
^,=1, J, = 3 - ^ , _ , + 1 (A: = 2,3,...)
1,4, 13, 40, 121, ... (в обратном порядке)
Анализ показывает, что число сравнений при этом пропорцио­
нально size^'^, а не size^'^ ^ как в нашем варианте. И то, и другое при
больших size хуже, чем size-Xogj^size),

15.8. Сложная сортировка обменом


(сортировка Хоора)

/ . Идея алгоритма (рис. 75). Исходный массив разбивается на


два сегмента - левый, элементы которого агг[ i ].кеу <= median.key, и
правый сегмент с элементами arr[j [.key >= median.key. Далее, каж­
дый из полученных сегментов можно сортировать автономно, ана­
логично предыдущему, до получения сегментов единичной длины.
Тогда массив в целом будет отсортирован.

Исходные значения ключей left hpht


элементов массива агг[ i ].кеу 44 55 12 1 42 1 94 18 06 67
( i = О, 1, 2, . ., size-1 ), size = 8

п
median
median = arr[ (left+right )/2 ]
Рис. 75. Идея алгоритма сортировки Хоора

2. Как выполнить разделение на сегменты? Решение этой


задачи представлено на рис. 76.
Анализ рисунка показывает, что за size сравнений {size - число
элементов массива) исходный массив разделяется на два сегмента,
которые можно сортировать автономно. Всего потребуется Xo^^i^ize)
таких разделений, в результате которых получатся сегменты еди­
ничной длины. Следовательно, общее число сравнений пропорцио­
нально size • 1о§2 {size).

259
a)
i = left, while( arr[ i ].key < median.key ) i++;
left
Всегда ли закончится цикл? Да, всегда,
поскольку, как минимум, справа есть median.
После выхода из цикла получим

п
median
агг[ I ] key >= median.key

Q)
j = right; while( arr[ j ] key > median.key ) j—;
right
По тем же причинам, цикл будет
заканчиваться всегда и после его завершения

п
получим агг[ j ].кеу <= median key

median в)
Если i <= j , то divrl i ] и arr[ j ] меняем местами: copy = arr[ i ]; arr[ i ] = arr[ j ];
arr[ j ] = copy; i++; j - ;

г)
Перейти к a), если i <= j

Рис. 76. Разделение исходного сегмента на подсегменты

J. Примеры, Пример 1 для общего случая представлен на рис.


77.
Примеры 2, 3 и 4 (частные случаи) приведены на рис. 78. Их
анализ предлагается выполнить самостоятельно.
left right
Исходные значения ключей 44 55 12 42 94 18 06 67
элементов массива arr[i] key
(1 = 0 , 1 , .., size-1), size = 8 4 5 6 7
median=arr[(left+right)/2]
a) Разбиение исходного массива
1=0, j=7,6; arr[0] меняем местами с arr[6], i++; j - - , i<=j (продолжаем обмен)
1=1, j=5; arr[1] меняем местами с arr[5], i++, j - - , i<=j (продолжаем обмен)
1=2,3, j=4,3; arr[3] меняем местами с arr[3]; i++, j - - ; i>j (разбиение на
сегменты закончено) В результате получаем:
left
' " right
06 18 12 42 94 55 44 67
4 5 6 7
median=arr[(left+right)/2]
J
Сегменты. левый left J
правый » nght и
(большего размера)
б) Каждый из полученных сегментов также разбиваем на сегменты, причем
это выгоднее начать с меньшего сегмента (см. обоснование ниже) Поэтому
больший из полученных сегментов запоминаем в стеке для последующего
разбиения
left jight \=o,^^, j=2; arr[1] меняем местами с агг[2],
агг[|].кеу 06 18 12 •++; J--; i>J (разбиение на подсегменты
0 1 2 закончено) В результате получаем
Н Н nned lan
left rightt
агг[|].кеу 06 12 18
0 1 2
med lan
1
Левый сегмент left j и правый i .right (меньшего размера)
Рис. 77. Пример сортировки Хоора для общего случая

4. В каком порядке сортировать полученные сегменты?


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

struct STACK // Тип элемента стека


(
±пЬ 1; // Левая граница сегмента
±пЬ г; // Правая граница сегмента
} s[ М ];
int sp; // Указатель вершины стека

261
в) Больший из полученных сегментов (левый) запоминаем в стеке для
последующего разбиения. Работа с правым (меньшим) сегментом
закончена, так как в нем один элемент. Извлекаем из стека и разбиваем
очередной сегмент
left right
arr[j] key 06 i=0; j=1,0; arr[0] меняем местами с arr[0]; i++, j -
12 i>j (разбиение на сегменты закончено). Оба
полученных сегмента единичной или нулевой
median А^'^чь! и работа с ними закончена

г) Извлекаем из стека и разбиваем очередной сегмент


left right i=4; j=7,6; arr[4] меняем местами с агг[6];
агг[1].кеу 94 55 44 67 i++; j — ; i<=j (продолжаем обмен)
6 7 i=5; j=5; агг[5] меняем местами с arr[5];
median i++; j — ; i>j (разбиение на сегменты
закончено).
left right Получаем левый left..j и правый 1..right
arr[i].key 44 55 94 67 (большего размера) сегменты. Больший
сегмент запоминаем в стеке для
6 7 последующего разбиения, а работа с
median меньшим сегментом закончена (в нем один
элемент).

д) Извлекаем из стека и разбиваем очередной сегмент


left right
arr[i].key 94 i=6; j=7; агг[6] меняем местами с агг[7]; i++; j — ;
67
i>j (разбиение на сегменты закончено). Оба
7 полученных сегмента имеют единичную длину и
median работа с ними закончена.
Так как в стеке нет больше сегментов для
разбиения,то и сортировка закончена:
06 12 18 42 44 55 67 94
0 1 2 3 4 5 6 7
Прод. рис. 77 •

Какой должна быть глубина стека М? Величина М зависит от


порядка разбиения полученных сегментов. Н. Вирт показал, что ес­
ли в стек помещать более длинный из получившихся сегментов, а с
коротким сегментом сразу "расправляться", то
М = \0g2isize),

где size - размер сортируемого массива.

262
Пример 2
left nght
Исходные значения ключей элементов 3 2 - 7 5 4
массива агг[ i ].кеу (i = О, 1, .. , size-1), size = О

median=arr[(left+right)/2]
Пример 3
left right
Исходные значения ключей элементов 3 2 5 - 7 4
массива агг[ i ].кеу (i = О, 1, ..., size-1), size = 1

median=arr[(left+right)/2]
Пример 4
left right
Исходные значения ключей элементов 3 2 4 - 7 5
массива агг[ 1 ].кеу (1 = О, 1, ..., slze-1), size = 0 1

, median=arr[(left+right)/2]
Рис. 78. Частные случаи для сортировки Хоора

Если, в целях унификации, границы исходного массива также


заносить в стек, то
Л/ = 1 + \og2(size).
При занесении в стек сделаем так, что операция занесения не
будет выполняться, если длина заносимого сегмента меньше или
равна единице.
Прототипы функций для занесения границ сегментов в стек
(Push) и извлечения границ сегментов из стека (Pop), определения
функций и примеры их вызова даны в примере программы из под-
разд. 15.2. Эти функции являются служебными для нерекурсивной
функции Quicksort. Подобные функции, как указывалось выше, на­
зывают неинтерфейсными функциями.

5. Нерекурсивная сортировка Хоора, Прототип функции


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

6. Рекурсивная сортировка Хоора.


Напомним ваэюнейшие особенности рекурсии:
1. При рекурсивных вызовах функции создаются поколения

263
вызовов-функций. Это означает, что имеются вложенные друг в дру­
га активные экземпляры рекурсивной функции.
2. Каждый рекурсивный вызов помещает в системный стек:
копии параметров рекурсивной функции, передаваемых по
значению;
адреса аргументов из вызова рекурсивной функции, соответст­
вующих параметрам, передаваемым по ссылке;
ее автоматические переменные;
адрес возврата из функции;
возвращаемое значение, если оно имеется.
По этой причине в рекурсивной функции лучше иметь меньше па­
раметров и автоматических переменных (помните, что поколений
активных экземпляров рекурсивной функции может быть много).
3. На рекурсивный вызов тратится больше времени, но зато
программа получается нагляднее и проще.

С учетом этих особенностей и была спроектирована рекурсив­


ная сортировка Хоора. Прототип функции QuickSortl для рекурсив­
ной сортировки Хоора, ее определение и пример вызова даны в
примере программы из подразд. 15.2. Эта функция является интер­
фейсной и предназначена для сортировки массивов. Для разделения
исходного сегмента на подсегменты функция Quicksort 1 использует
служебную рекурсивную функцию Split. Ее прототип, определение и
пример вызова даны также в примере программы из подразд. 15.2.
Функция Split является рекурсивной и именно при ее разработке бы­
ли учтены перечисленные выше важные особенности рекурсивных
функций.

15.9. Сравнительные показатели производительности


различных методов сортировки массивов

Приводимые ниже в табл. 29 данные получены для программы,


написанной на языке Паскаль (ЭВМ SDS6400) для неупорядоченных
массивов.
Из приведенных в таблице данных следует, в частности, что
даже для массива относительно небольшого размера из 512 элемен­
тов:
1. Худшая по производительности из простых сортировок
(простая сортировка обменом) работает в 35 раз медленнее быстрой
сортировки Хоора.
2. Самая быстрая из простых сортировок (простая сортировка
вставками) работает медленнее в 4,2 раза, чем самая худшая по про-

264
изводительности из сложных сортировок (сортировка Шелла).
При увеличении размеров массива, указанные в пп. 1 и 2 эф-
фекты проявляются еще в большей степени.

Табл. 29. Сравнительные показатели производительности различных


методов сортировки массивов
Простые методы сортировки
Время Время сорти­ Соотношение методов
Метод сортировки ровки по производительности
сортировки для для (относительное время
size==256, size=512, ми­ сортировки)
милисекунд лисекунд
Вставками 356 1444 1

Выбором 509 1956 1.3

Обменом 1026 4054 3

Сложные методы сортировки


Обменом ( Х о о р а ) 60 116 1 1
В ы б о р о м (с п о м о щ ь ю НО 241 1.7
д в о и ч н о г о дерева)
Вставками ( Ш е л л а ) 127 349 2.1
16. ГРАФЫ- ТРАНСПОРТНАЯ ЗАДАЧА
(ЗАДАЧА КОММИВОЯЖЕРА)

16,1. Терминология

Граф - это пара (К, /?), где V - конечное непустое множество


вершин, а R -множество неупорядоченных пар <а, Ь> вершин из
множества К, называемых ребрами. Говорят, что ребро г — <а, Ь>
соединяет вершины "а" и "6". Ребро 'V" и вершина "<з", ребро 'V" и
вершина "Z?" называются инцидентными. Вершины "а" и "6" являют­
ся смеэюными. Ребра, инцидентные одной и той же вершине, также
называют смеэюными. Степень вершины равна числу ребер, инци­
дентных ей.
Для простоты будем ограничиваться классом графов без пе­
тель, т.е. без таких ребер <а, 6>, что а = Ь.

Пример графа._ Города и связывающие их дороги можно пред­


ставить с помощью графа, показанного на рис. 79.

Рис. 79. Пример графа, представляющего города и дороги между


ними

В данном примере граф задает объекты (города) и отношения между


объектами (дороги). В приведенном графе содержатся пять вершин
и семь ребер. Степень вершин 1, 2, 3, 4 равна трем, а вершины 5 -
двум.
В ориентированном или направленном графе каждое ребро
имеет направление (например, дорога с односторонним движением,
рис. 80 а). В направленном графе отношения не симметричны.
В неориентированном графе отсутствует ориентация ребер
(рис. 80 б), т.е.

266
{a,b) G R тогда и только тогда, когда {Ь,а) е R

а b а b

О Ю о о
а - предшественник вершины "Ь"
b - преемник вершины "а"

а) б)
Рис. 80. Граф:
а) ориентированный (направленный);
б) неориентированный

Путь в неориентированном графе, соединяющий вершины "а"


и "Z?", - это последовательность вершин Vo,v,,...,i/„(«>0) такая, что
v/Q=a,v„=6, а для любого /(0</<«-1) вершины v, и v,^,, соединены
ptGpoM. Длина пути Vo,v,,...,v„ равна количеству его ребер, т.е. п. Для
примера, показанного на рис. 76, путь 1-4-3-2 между вершинами 1 и
2 имеет длину 3.
Путь замкнут, если v^ =v„. Путь называется простым, если все
его вершины различны. Замкнутый путь, в котором все ребра раз­
личны, называется циклом. Простой цикл - это замкнутый путь, все
вершины которого, кроме вершин VQ И V„, попарно различны.
Расстояние между двумя вершинами - это длина кратчайшего
пути, соединяющего эти вершины. Например, на рис. 79 расстояние
между вершинами 1 и 2 равно единице, а между вершинами 3 и 5 -
двум.
Обод - это граф, вершины которого Vo,v,,...,v/„ при п>2 можно
занумеровать так, что для всех i{\<i<n-\) вершина v, соединена
ребрами с v,_, и v,^,, вершина VQ С V„, а других ребер нет.
Граф называется связным, если лля любой пары вершин суще­
ствует соединяющий их путь.
Во взвешенном графе, в дополнение к графу, задана функция
W = f{R), определяющая вес или длину ребра в графе. Обычно взве­
шенные графы являются неориентированными, а веса ребер - поло­
жительными. Пример взвешенного неориентированного графа при­
веден на рис. 81.
В этом примере в качестве веса ребра можно выбрать расстоя­
ние между городами. Из примера со всей очевидностью следует, что
при поиске пути с суммарным минимальным весом не обязательно,
что минимальный вес имеет путь с минимальным числом ребер (в
примере лучшим путем от start ло finish является путь по трем доро­
гам 1-4-5-2 с суммарным весом 30.0).

267
100 10.0

10.0
Рис. 81. Пример взвешенного неориентированного графа

16.2. Формы задания графа

Используются две основные формы:


1. С помощью матрицы инциденций (соединений):
а) для неориентированного взвешенного графа (рис. 82 а) мггт-
риц|^ инциденций имеет число строк и столбцов, равное числу вер­
шин, и симметрична;
б) для ориентированного взвешенного графа матрица соедине­
ний не симметрична (рис. 82 б).

а 20 b а 20 b

о -О о -ю
а О 20 а 0 20
ь| 20 I О I b 0 0
а b а
а) б)
Рис. 82. Способы задания графа

2. С помощью списка ребер:

±nt Num Top , // Число вершин


NumArc; // Число ребер
\сЬ А // Ребро графа

±nt first; // 1-я вершина ребра


±nt last / // 2-я вершина ребра
float weight/ // Вес ребра
};

268
/ / Адрес первого элемента массива структур с информацией о
// ребрах графа
А *рАгс;

Задание графа на основе списка ребер удобйо свести в одну


структуру:
/ / Структурный тип для графа
struct GRAPH
{
±nt NumTop; // Число вершин
Izit NumArc; // Число ребер
А *рАгс; // Указатель на начало массива ребер
// в динамической памяти
} ;

Сопоставление указанных форм задания графа позволяет


заключить следующее.
1. Использование матрицы соединений требует хранения в па­
мяти NumTop*NumTop элементов.
2. Использование списка ребер требует хранения в памяти
Ъ"^NumArc элементов.
3. При Ъ'^NumArc < NumTop"^NumTop эффективнее использо­
вать задание графа с помощью списка ребер.
4. Использование списка ребер алгоритмичнее. Это означает,
что алгоритмы решения задач с использованием графов, заданных
списком ребер, проще и эффективнее.
Для примера, приведенного на рис. 81, информация для списка
ребер имеет следующий вид:
1г 2, 80. .0
i , 4г 10. .0
2 , 3 , 20. ,0
2г 5 , 10. .0
3 , 4, 20. О
4, 5 , 10. 0

Здесь в первой строке 1 и 2 - номера вершин, а 80.0 - вес соединяю­


щего их ребра.

16.3. Почему для решения задачи подходит


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

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

Вершина

Путь
Ребро •

Рис. 83. Рекурсивный перебор путей

16.4. Представление кратчайшего пути


до каждой вершины
Сведения о любом пути должны содержать следующую ин­
формацию.
1. Имеется ли путь до вершины графа?
2. Суммарный вес пути, начиная от заданной начальной вер­
шины (start)?
Но только этих сведений недостаточно, так как нет указаний,
откуда и как двигаться. Для задания недостающих сведений можно
организовать линейный список, который для графа, представленно­
го выше на рис. 81, будет иметь вид, показанный на рис. 84 а. Такой
линейный список можно организовать на базе массива структур
(рис. 84 б).

stmjct W // Путь до одной вершины


{
int exist; // (!= 0) - путь имеется
±nt ref; // Предыдущая вершина^ через
// которую проходит путь
float SumDlst; // Суммарная длина минимального пути
} ;

// Адрес первого элемента массива с информацией о минимальном


// пути между заданными вершинами
W *pMinWay/

270
start finish start finish
1 4 5 2 1 2 3 4 5
1 1 1 1 exist 1 1 1 1 1
0.0 10.0 20.0 30.0 SumDist 0.0 30.0 30.0 10.0 20 0
0 1 4 5 ref 0 5 4 1 4

J t i
t
(REFerence - ссылка): 0 - конец списка

a) 6)
Рис. 84. Представление кратчайшего пути между вершинами:
а) с помощью линейного списка;
б) на базе массива структур

16.5. Как найти минимальный путь

16.5.1. Требуется ли полный перебор путей

При поиске минимального пути часть путей можно отбросить


(рис. 85). Попытки, представленные на этом рис., неуместны, если,
допустим, существует
pMinWayf 18 ].SumDist = 50.0 и pMinWayf 18 ].exist != О

Текущий путь длиной 200.0 finish

•в;.-; •о
Рис. 85. Поиск минимального пути

16.5.2. Организация перебора путей

Как уже указывалось, для этой цели хорошо подходит рекур­


сивный алгоритм. Рассмотрим, как можно пройти отрезок пути от
достигнутой промежуточной вершины (intermediate) до финиша (fin­
ish) - рис. 86.

271
start
О • О
intermediate finish
а) intermediate = finish
Вершина Конец

б) intermediate != finish
intermediate
•>\ Ребро Путь

Рис. 86. Прохождение пути от достигнутой вершины до финиша

Попытку шага вперед из достигнутой вершины по заданному


ребру будем делать с помощью функции ForStep, а прохождение пу­
ти (если он есть) от достигнутой вершины до вершины finish - с по­
мощью функции Pass Way (взаимно рекурсивный вызов PassWay -
For Step).
Для решения транспортной задачи спроектируем программу и
в ней разработаем функции PassWay -ForStep. Спецификация функ­
ции, выполняющей шаг вперед, представлена на рис. 87. Обратите
внимание, что в список параметров этой функции включены только
три параметра - topl^ IndArc, top2. Это важно, так как для
рекурсивных функций, как это было показано выше, число
параметров следует минимизировать. Поэтому Gr, pMinWay опре­
делены как глобальные объекты. Исходный текст программы для
решения транспортной задачи, включающий определение функции
ForStep., приведен ниже. На данном этапе в этом тексте рекомендуем
рассмотреть только введенные типы, данные и определения всех
функций, кроме solution и PassWay. Указанные в конце функции бу­
дут рассмотрены позже.

Достигнутая вершина int top1 -


Индекс ребра, по которому W *pMin\/Vay
шагаем int IndArc- ForStep
Вершина на конце ребра int top2 - Массив с инфор­
Граф GRAPH Gr- мацией о наилуч­
шем пути
input process output
Рис. 87. Спецификация функции, выполняющей шаг вперед по ребру

Обратите также внимание на то, что функция ForStep является


служебной функцией и вызывается из функции PassWay, которая, в
свою очередь, вызывается из функции solution.

Ill
Файл TestGr.cpp. Тестирование решения транспортной задачи
с размещением данных в динамической памяти.
Определение функций^ используемых при решении транспортной
задачи, приведено в файле Graph.срр.
Определение Функций размеш,ения данных в динамической памя­
ти и освобождения занятой динамической памяти находится в
файле GrAlocFree.срр.
Включаемый файл программного проекта находится в файле
GrHead.h.
Давыдов В.Г. Консольное приложение, Visual C-f-f- 6
V
/ / Включаемый файл программного проекта для решения
// транспортной задачи
^include "GrHead.h"
±nt main ( // Возвраш,ает О при успехе
±пЬ АгдС, // Число аргументов в командной
// строке
cha.r *ArgV[ ] ) / / Массив указателей на аргументы
// командной строки
{
// Проверка числа аргументов командной строки
±f( ArgC /= 3 )
{
printf(
"\п Ошибка 5. В командной строке должно быть три аргумента:
"\п Имя__проекта. ехе имя_файла_ввода имя_файла_вывода \п" ) ,
exit ( 5 ) ;
}

// Чтение информации о графе


ReadGraph ( ArgV[ 1 ] ) ;

// Печать информации о графе


WriteGraph( ArgV[ 2 ], "w" ) ;

solution ( ) ; // Решение транспортной задачи

// Вывод результатов решения


OutRes ( ArgV[ 2 ], "а" ) ;

•returri Or
}

Файл GrHead.h. Подключение стандартных заголовочных фай­


лов, объявление используемых структурных типов, объявление
объектов с описателями класса хранения "внешний" и прототипов
функций. Используется как заголовочный файл в программном
проекте для решения транспортной задачи (задачи коммивояже­
ра) .
Давыдов В.Г. Консольное приложение. Visual C++ 6

273
_v
// Предотвращение возможности многократного подключения
iifndef GRHEAD_H
^define GRHEAD Н

^include <stdio.h> // Для ввода-вывода


^include <stdlib.h> // Для exit ( )
// Структурный тип для ребра графа
stjnzct А
{
±nt first/ // 1-я вершина ребра
±nt last; // 2-я вершина ребра
£1олt weight; // Вес ребра
} ;

// Структурный тип для графа


strvLcb GRAPH
{
±nt NumTop; // Число вершин
±nt NumArc; // Число ребер
A *pArc; // Указатель на начало массива ребер
// в динамической памяти
};

// Структурный тип пути до одной вершины


struct W
{
int exist; // (!=0) в графе имеется путь
// до вершины
xnt ref; // Предыдущая вершина, через которую
// проходит путь до данной вершины
£loa,t SumDlst; // Суммарная длина минимального пути
// до данной вершины
}.

// Объявления внешних объектов


extern GRAPH
Gr; // Граф
// Указатель на массив структур для хранения информации о
// минимальном пути от start до finish
extern W *pMlnWay;
extern int finish; // Вершина - финиш пути
extern int start; // Вершина - старт пути

// Прототипы функций
void GrAllocDM( void, ) ;
void GrFreeDM( void ) ;
void ForStep ( int, int, int ) ;
void PassWay ( int ) ;
void solution ( void ) ;
void OutRes ( char* *, char *pMode ) ;
void ReadGraph ( char *pFlleInp ) ;

274
void WriteGraph ( char ^pFileOut, char *pMode ) ;

^endif

Файл GrInpOut.срр. Функции файлового чтения данных о графе


и печати их.
Используется в программном проекте для решения транспорт­
ной задачи (задачи коммивояжера) .
Давыдов В.Г, Консольное приложение. Visual C++ 6
V
// Включаемый файл программного проекта для решения
// транспортной задачи (задачи коммивояжера)
#include "GrHead,h"

// Чтение данных о графе


void ReadGraph (
// Указатель на файл данных
char "^pFilelnp )
{
FILE *pStrInp; // Указатель на структуру со
// сведениями о файле ввода
±пЬ i , // Индекс ребра
RetCode, // Возвращаемые значения fscanf( )
// или их сумма
RetCodel; // Возвращаемое значение fclose( )

// Открытие файла ввода


pStrlnp = fopen( pFilelnpr "г" ) ;
±£( pStrlnp == NULL )
{
printf( "\n Ошибка 50, Файл %s для чтения не "
"открыт \п", pFilelnp ) ;
exit ( 50 ) ;
}

// Чтение количества вершин и количества ребер


RetCode = fscanf( pStrlnp, " %d", &Gr,NumTop ) ;
±f( RetCode != 1 )
(
printf ( "\n Ошибка 60, Ошибка при чтении числа "
"верш^^н графа \п" ) ;
exit ( 60 ) ;
}
RetCode = fscanf ( pStrlnp, " %d", &Gr,NumArc ) ;
±f( RetCode 1=1)
{
printf ( "\n Ошибка 70, Ошибка при чтении числа "
"ребер графа \п" ) ;
exit ( 70 ) ;
}

275
// Размещение структур данных графа в динамической памяти
GrAllocDM( ) ;
// Заполнение массива ребер графа
Ret Code = 0;
£ою ( i = 0; 1 < Gr.NumArc; i + -h )
{
RetCode += fscanf( pStrlnp, " %d %d %g",
&Gr.pArc[ i ].first, &Gr.pArc[ 1 J.last,
&Gr.pArc[ 1 ].weight ) ;
±f( ( Gr.pArcl i ].first < 0 ) \\
( Gr.pArcl i ].first >= Gr.NumTop ) | |
( Gr.pArc[ i ].last < 0 ) \\
( Gr.pArcf 1 ].last >= Gr.NumTop ) )
{
printf( "\n Ошибка 75. Индексы вершин д.б. в "
"диапазоне О. . %d \ л " , Gr.NumTop~l ) ;
exi t ( 75 ) /
}
}
±£( RetCode < 3*Gr.NumArc )
{
printf( "\n Ошибка 80. Ошибка чтения элементов "
"массива ребер \п" ) ;
exit ( 80 ) ;
}

// Чтение информации о вершинах - старте и финише пути


RetCode = fscanf ( pStrlnp, " %d", &start ) ;
±£( RetCode /= 1 )
{
printf( "\n Ошибка 90. Ошибка при чтении начальной"
" вершины пути \п" ) ;
exit ( 90 ) ;
}
RetCode = fscanf ( pStrlnp, " %d", & finish ) ;
±£( RetCode != 1 )
(
printf( "\n Ошибка 100. Ошмбка при чтении конечной"
" вершины пути \п" ) ;
exit ( 100 ) ;
}

// Закрытие файла ввода


RetCodel = fclose( pStrlnp ) ;
±f( RetCodel == EOF )
{
printf( "\n Ошибка 110. Ошибка закрытия файла %s \п",
pFilelnp ) ;
exit ( 110 ) ;
}

xretuxm/

276
// Печать данных о графе
void. WriteGraph (
char *pFileOut,// Указатель на файл результатов
char *pMode ) // Указатель на режим открытия файла
{
FILE *pStrOut/ // Указатель на структуру со
// сведениями о файле результатов
Int i, / / Индекс элемента массива ребер
RetCodel; // Возвращаемое значение fclose( )

// Открытие файла вывода


pStrOut = fopen( pFileOut, pMode );
±f( pStrOut == NULL )
{
printf( "\n Ошибка 120, Файл %s для вывода не "
"открыт \л", pFlleOut );
exit ( 120 ) ;
}

// Печать информации о графе


fprlntf ( pStrOutг "\п Число вершин графа: %d,"
" число ребер: %d \п"г Gr.NumTop, Gr.NumArc ) ;
fprlntf ( pStrOutr
" \j^* * * * * -^ * * * * * * * * * -^ * * * * * * * * * * * * * * * * * * "^ * * * "^^ * * * * * * * * * * * * *
"\n Индекс ребра 1-я вершина 2-я вершина Вес ребра"
"\п********************^**************************^*********"
"\п" ) ;
tori 1 = о; 1 < Gr.NumArc/ i + -i- )
{
fprlntf( pStrOut, "%10d %14d %14d %17f \л", i,
Gr.pArcf 1 ].first, Gr.pArc[ 1 ],lastr
Gr.pArcf i ].weight );
}

// Закрытие файла вывода


RetCodel = fclose( pStrOut );
±f( RetCodel -= EOF )
{
printf( "\n Ошибка 150. Файл %s не закрыт \n ",
pFileOut );
exit ( 150 );
}

return;
}

Файл GrAlocFree. cpp. Размеш,ение массива с информацией о


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

277
Давыдов В.Г. Консольное приложение^ Visual C++ 6
_V j
/ / Включаемый файл программного проекта для решения
// транспортной задачи (задачи коммивояжера)
#include "GrHead.h"

// Размещение массива с информацией о ребрах графа в


// динамичнеской памяти
void. GrAllocDM( void )
{
// Gr.NumTop - число вершин графа (определяется на
// внешнем уровне)
// Gr. NumArc - число ребер графа (определяется на внешнем
// уровне)
// Gr.pArc - указатель на начало массива с информацией о
// ребрах^ размещенного в динамической памяти
// (определяется на внешнем уровне)
// pMinWay - указатель на начало массива с информацией о
// наилучшем пути, размещенного в динамической
// памяти (определяется на внешнем уровне)
// Контроль корректности количества вершин графа
i£( Gr.NumTop < 2 )
{
printf(
"\п Предупреждение 10. Число вершин графа должно быть более"
" одной вершины"
"\п (задано число вершин, равное %d) . Принимается число"
" вершин, равное 2."
"\п Выполнение программы продолжается ", Gr.NumTop ) ;
Gr.NumTop = 2;
}

// Контроль корректности количества ребер графа


if( Gr. NumArc < 1 )
{
printf(
"\n Предупреждение 20. Число ребер графа должно быть не"
" менее одного ребра"
"\п (задано число ребер, равное %d) . Принимается число"
" ребер, равное 1. "
Gr.NumArc = 1;
}

// Размещение массива ребер в динамической памяти


Gr.pArc = new А[ Gr. NumArc ] ;
if( Gr.pArc == NULL )
{
printf( "\n Ошибка 30. Массив ребер в динамической"
" памяти не размещен " ) ;
exit ( 30 ) ;
}

11^
// Размещение массива структур с информацией о наилучшем
// пути в динамической памяти
pMlnWay = new W[ Gr. NumTop ] ;
±f( pMinWay == NULL )
{
printf(
"\n Ошибка 40. Массив структур с информацией о наилучшем"
" пути в \п"
"\п динамической памяти не размещен " ) ;
exit ( 4 0 ) ;
}

// Инициализация массивов, размещенных в динамической


// памяти, нулевыми значениями
for( Int 1 = О; 1 < Gr.NumArc; i++ )
{
Gr.pArcf i J.firSt = 0; Gr.pArcf i ],last = 0;
Gr.pArc[ 1 ].weight = O.Of;
}
£or( 1 = 0; 1 < Gr. NumTop/ 1 + + )
{
pMlnWayf 1 ]. exist = 0; pMinWayf i ].ref = 0/
pMinWay[ i ] . SumDist = 0.0;
}

return;
}

// Освобождение динамическом памяти, занятой массивами ребер


// и структур с информацией о наилучшем пути
void. GrFreeDM( void )
{
i£( Gr.pArc /= NULL )
{
delete [ ] Gr.pArc; Gr.pArc = NULL;
}

if( pMinWay != NULL )


{
delete [ ] pMinWay; pMinWay = NULL;
}

return;
}

Файл Graph. cpp.


Функции решения транспортной задачи:
* шаг вперед из достигнутой вершины по заданному ребру;
* прохождение пути от достигнутой вершины InterMediate, если
он есть, до вершины finish;
* поиск пути минимального веса в неориентированном взвешенном

279
графе;
* печать информации о наилучшем пути от start до finish.
Используется в программном проекте для решения транспорт­
ной задачи (задачи коммивояжера). |
Давыдов В.Г, Консольное приложение^ Visual C++ 6
'^/ I
// Включаемый файл программного проекта для решения транс­
портной задачи (задачи коммивояжера)
#include "GrHead.h"

// Определения объектов с описателем класса хранения внешний.


// Их объявление имеется в заголовочном файле проекта и
// доступно в других файлах проекта
Inb start; // Вершина - старт пути

// Эти объекты определяем в данном месте^ чтобы при


// взаимнорекурсивных вызовах функций PassWay( ) и
// ForStep ( ) они не создавались заново
GRAPH Or; // Граф
W *pMinWay; // Указатель на массив структур с
// информацией о наилучшем пути из
// вершины start в finish
Int finish, // Вершина - финиш пути
one, // 1-я вершина текущей дуги
two/ // 2-я вершина текущей дуги

// Шаг вперед по ребру с индексом IndArc из вершины topi в


// вершину top2
void. ForStep (
±пЬ topi, // Достигнутая вершина, из которой
// шагаем вперед
int IndArc, // Индекс ребра, по которому
// делается шаг вперед
int top2 ) // Вершина на конце ребра
{
£2.аа.Ь NewDist; // Расстояние до top2 по пути через
// topi

NewDist = pMinWayl topi ] . SumDist +


Or.pArc[ IndArc ].weight;

±f( !pMinWay[ top2 ]. exist )


{ // Пока пути до top2 нет
pMinWayl top2 ].exist = 1;
pMinWay[ top2 ]. SumDist = NewDist/
pMinWayf top2 J.ref = topi/ PassWay ( top2 ) /
}
else
{ // Путь до top2 уже существует
if( pMinWay[ top2 ]. SumDist > NewDist )
{ // Новый путь короче
pMinWayl top2 ].SumDist = NewDist/

280
pMinWayl top2 J.ref = topi; PassWay( top2 ) ;
}
}

return/
}

// Прохождение пути от достигнутой вершины InterMedlate, если


// он есть г до вершины finish
void. PassWay (
// Достигнутая вершина - отправная точка пути
Int InterMedlate )
{
±nt к; // Индекс текущей дуги графа
±f( InterMedlate == finish )
{ // ! ! ! Выход из рекурсии
return/
}
else
{
// Перебор ребер графа
£ог( к == О/ к < Gr.NumArc/ к++ )
{
one = Gr.pArc[ к ]. first/
two = Gr.pArc[ к J.last/
// Определения направления шага по ребру и
// выполнение шага в найденном направлении
±f( one == InterMedlate )
{
ForStep( one г к, two ) /
}
else ±f( two == InterMedlate )
(
ForStep ( two, k, one ) ;
}
}
return/ // !!! Альтернативный вариант выхода
// из рекурсии
} }

// Поиск пути минимального веса в неориентированном


// взвешенном графе
void, solution ( void )
{
int j / // Индекс вершины
// Начальная подготовка массива структур с информацией о
// наилучшем пути
fori j = О/ j < Gr.NumTop/ j++ )
{

281
pMinWayf j ], exist = 0;
}
pMinWay [ start ]. exist = 1;
pMinWay[ start ] . SumDist = O.Of;
pMinWay[ start J.ref = -1;

// Рекурсивное определение требуемого пути


PassWay( start );

return/
}

// Печать информации о наилучшем пути от start до finish


void. OutRes (
char *pFileOut, // Указатель на файл вывода
char *pMode ) // Указатель на режим вывода в файл
{
FILE *pStrOut; // Указатель на структуру со
// сведениями о файле результатов
±пЬ TempTopf // Текуш,ая вершина пути
RetCodel; // Возвраш;аемое значение fclose ( )

// Открытие файла вывода


pStrOut = fopen( pFileOut^ pMode );
±f( pStrOut == NULL )
{
printf( "\n Ошибка 140. Файл %s для вывода не "
"открыт \ л " , pFileOut );
exit ( 140 ) ;
)

// Печать информации о найденном пути


±f( !pMinWay[ fini'Sh ],exist )
{
printf (
"\n Искомого пути не существует \п" );
}
else
{
// Печать оптимального пути
ТетрТор = finish/
fprintf( pStrOut,
"\п Вершина-финиш: %d, вершина - старт: %d "
"\л Значение минимального пути: %д \л", finish,
start, pMinWay[ finish ].SumDist )/
fprintf ( pStrOut, "\n Список вершин, образуюш:их"
" этот путь (от finish до start): \п" )/
while ( ТетрТор != -1 )
{
fprintf( pStrOut, " %4d ", ТетрТор )/
Temp Top = pMi nWay [ Temp Top ] . ref/
}
}

282
// Закрытие файла результатов
RetCodel = fclose( pStrOut );
±f( RetCodel == EOF )
{
printf( "\n Ошибка 150. Файл %s не закрыт \n",
pFileOut );
exit ( 150 ) ;
}

// Освобождение динамической памяти


GrFreeDM( ) ;

retujcn/

При файле исходных данных


5 6
О 1 80
0 3 10
1 2 20
1 4 10
2 3 20
3 4 10
О 1

получаем решение транспортной задачи в файле результатов в


следующем виде:

Число вершин графа: 5 , число ребер: 6

Индекс ребра 1-я вершина 2-я вершина Вес ребра


0 О 1 80. ,000000
1 О 3 10. ,000000
2 1 2 20. ,000000
3 1 4 10. ,000000
4 2 3 20. ,000000
5 3 4 10. ,000000
В ерши на - финиш: 1, в ерши на - с тар т: О
Значение минимального пути: 30

Список вершин, образуюш;их этот путь (от finish до start) :


1 , 4 3 О

Спецификация функции прохождения пути от достигнутой


вершины до finish, если он есть, представлена на рис. 88.

283
Достигнутая вершина - отправная точка
пути int InterMediate

PassWay
Финиш пути int finish
Граф GRAPH Gr

input process output

Рис. 88. Спецификация функции прохождения пути


от достигнутой вершины j\o finish

Необходимо обратить внимание, что в список параметров этой


функции включен только один параметр - InterMediate- так как Gr,
finish определены на внешнем уровне (повторяем, что для рекурсив­
ных функций число параметров нуэюно минимизировать). Текст
функции PassWay приведен выше. На данном этапе рекомендуем
рассмотреть только функцию PassWay. Остальные функции будут
рассмотрены позже. Данная функция, как и функция ForStep, явля­
ется вспомогательной и вызывается из функции solution.
Из приведенной программы следует, что взаимно-рекурсивный
вызов функций выглядит следующим образом (рис. 89).

Pass Way ( start);

Выход (InterMediate == finish или обработаны все ребра графа)


Рис. 89. Взаимно-рекурсивный вызов функций Pass Way-ForStep

Спецификация функции solution для решения задачи в целом


представлена на рис. 90.

Вершина - старт
пути int start Массив с информацией о
solution - • наилучшем пути
Граф GRAPH Gr W *pMinWay

input process output


Рис. 90. Решение задачи в целом

Список параметров этой функции пуст. Объясняется это тем.

284
что start, Gr, pMinWay являются глобальными объектами
(определены на внешнем уровне). Текст программы, включающий
определение функции solution, приведен выше. Данная функция, в
отличие от предыдущих функций Pass Way и ForStep, является ин­
терфейсной функцией и вызывается для решения транспортной за­
дачи.

16.6. Пример поиска минимального пути в графе

Схема графа приведена на рис. 91.

first last weight

0 0 1 80.0
1 0 3 10.0
2 1 2 20.0
3 1 4 10.0
4 2 3 20.0
5 3 4 10.0

Внимание! Нумерация вершин и


ребер начинается с нуля, так как
минимальный индекс элемента
массива с языках Си/С++ равен нулю.
Рис. 9 1 . Пример схемы графа

Состояние массиваpMinWay после подготовки в функции solu­


tion перед вызовом функции PassWay{ start ) показано на рис. 92.

Индексы вершин
1 1 0 0 0 0 exist
pMinWay 0.0 0.0 0.0 0.0 0.0 SumDist
-1 0 0 0 0 ref (REFerence - ссылка):
-1 означает конец списка
start finish
Рис. 92. Состояние массива,pMinWay после начальной подготовки

В качестве задания для самостоятельной работы предлагается


проанализировать работу программы и убедиться, что в результате в
массиве pMin Way получится информация, показанная на рис. 93.
Важное замечание! В данном частном случае массивpMinWay
указывает наилучшие пути от start до всех остальных вершин. Но в
общем случае это не гарантируется. Гарантируется лишь оптималь-

285
ность пути из start ъ finish.

Индексы вершин
1 1 1 1 1 exist
pMJnWay 0.0 30.0 30.0 10.0 20.0 SumDist
-1 4 3 0 3 ref (REFerence - ссылка):
start finish -1 означает конец списка

i'

Рис. 93. Состояние массива/?МшЖду после решения


транспортной задачи

16.7. Печать информации о наилучшем пути

в рассмотренном примере получена информация о наилучшем


пути между вершинами start \\ finish в обратном порядке (рис. 94).

Рис. 94. Информация о наилучшем пути между


вершинами start и finish

Прототип и определение функции OutRes^ в которой произво­


дится печать информации о найденном оптимальном пути, приведе­
ны в подразд. 16.5.2. Там же приведен текст функции, выполняющей
тестирование спроектированного класса, и результаты тестирования.
Советуем внимательно изучить этот пример и поэксперимен­
тировать с ним.
При этом рекомендуем обратить внимание на следуюш^ие осо­
бенности рассмотренного программного проекта:
1. Взаимно-рекурсивный вызов функций Pass fVay-ForStep (ва­
рианты завершения рекурсии в методе Pass Way; минимизация коли­
чество параметров и внутренних данных в этих методах; алгоритми­
ческое решение, обеспечивающее получение решения транспортной
задачи при неполном переборе путей между заданными вершинами).
2. Структуру спроектированной программы.
3. Оформление исходных текстов

286
4. Терминологию при работе с графами.
5. Практическую значимость решения транспортной задачи
(получение оптимального пути между городами, связанными раз­
ветвленной системой дорог; определение оптимального маршрута
между заданными пунктами в крупном городе и т.п.).
17. п о и с к

Поиск, как и сортировка, может быть двух видов.


1. Внутренний поиск - поиск в оперативной памяти, в таблице
(т.е. в массиве).
2. Внегиний поиск- поиск на внешней памяти (на магнитном
диске или магнитной ленте).
Рассмотрим широко распространенные задачи внутреннего
поиска.

17.1. Постановка задачи внутреннего поиска

Таблица данных располагается в оперативной памяти и содер­


жит некоторое количество строк, вид которых представлен на рис.
95.

8 байт 62 байта (LDATA-1), данное - другие сведения


(LKEY-1,
LengthKEY).
ключ для
поиска
Рис. 95. Структура строки таблицы

Строка таблицы может занимать, например, 70 байт памяти


(см. рис. 95). Байт хранит код некоторого символа.
Приведем несколько примеров таблиц такого рода.
1. Русско-английский словарь. Ключ - русское слово, данное -
соответствующее английское слово и другие сведения.
2. Англо-русский словарь. Аналогично.
3. Таблица домашних адресов: ключ - фамилия, другие сведе­
ния - домашний адрес и телефон.
Для дальнейшего примем допущение, что ключ поиска может
содержать только строчные буквы латинского алфавита, цифры и
символ пробела, начинается с буквы, а символ пробела (символы
пробелов) может (могут) быть только завершающим (завершающи­
ми).
В кодовых таблицах буквы латинского алфавита упорядочены,
заглавные (прописные) буквы предшествуют строчным, цифры
предшествуют заглавным буквам, а пробел - цифрам (в смысле ал-

288
фавита). Русские буквы в кодовых таблицах - в общем случае неупо-
рядочены. Отсюда вывод - сравнение ключей поиска, содержащих
латинские буквы, можно проводить непосредственно (например, с
помощью строковой функции strcmp, как в нашем случае). И наобо­
рот, если ключ содержит русские буквы, то для сравнения ключей
следует использовать специально написанную для этой цели функ­
цию.
Данные для поиска в таблице могут иметь следующий вид:
const ±nt LKEY = 9; / / Длина ключа в строке таблицы
// LKEY~1
// Длина данного в строке таблицы LDATA-1
const xnt LDATA = 63;

// Тип для строки таблицы


struct STRTAB
(
// Ключ
char кеу[ LKEY ];
// Данные
сЬаг data[ LDATA ];
} ;

// приведенные ниже объекты будут использованы практически во


// веек функциях поиска в таблице и их целесообразно
// определить с описателем класса хранения внешний - это
// сделает указанные объекты доступными в других файлах
// проекта и всех функциях
STRTAB *рТаЫе; // Адрес первой строки таблицы в
// динамической памяти
±nt size; // Размер таблицы

Спецификация функции, выполняющей поиск в таблице, пред­


ставлена на рис. 96. Из нее следует, что хотя общее число исходных
данных {input) и результатов, получаемых из функции поиска {out­
put), равно пяти, все же в список параметров функции следует
включить только три из них, так как два исходных данных - table и
size — следует определить на внешнем уровне как глобальные объек­
ты.

STRTAB *рТаЬ1е И • Int &found


int size м search
char KeyWord[LKEY] И • int &line

input process output


Рис. 96. Спецификация функции поиска в таблице

Решение задачи поиска заключается в том, что в таблице


рТаЫе надо найти строку с полем ключа, совпадающим со словом

289
Key Word (если строка найдена, то ее индекс line, а флаг результата
поиска found=\) или получить ответ, что такой строки в таблице нет
(found=0).
Основными способами поиска в таблице являются.
/ . Последовательный поиск. Эффективность поиска (среднее
число обращений к таблице для нахождения искомой строки) равна
size/2.
2, Логарифмический поиск (бинарный, с помощью двоичного
дерева). Число обращений к таблице равно \Q>%^{size).
J. ПоисКу использующий прямой доступ к таблице. Число
обращений к таблице равно единице.
4. Поиск с использованием перемеиганной, слабо заполнен­
ной таблицы (хэт-таблицы). Число обращений к таблице близко к
единице.
Рассмотрим перечисленные способы поиска, кроме малоупот-
ребимого поиска с прямым доступом, рассмотренного в [6].

17.2. Последовательный поиск

Пример таблицы, заполненной для последовательного поиска,


показан на рис. 97.

П
lesson лекция Р
type тип о
с
word слово м
о
size-1 work работа
т
|р|
ключ данное ^-*^
Рис. 97. Пример таблицы для последовательного поиска

Поиск выполняется в полностью заполненной таблице. Про­


смотр таблицы выполняется последовательно, в соответствии с рос­
том индексов строк таблицы. Если в какой-то строке таблицы поле
ключа совпадает с KeyWord^ то поиск окончен с результатом "на­
шли". Если этого не произошло, а конец таблицы достигнут - поиск
окончен с результатом "не нашли".
Для таблицы, показанной на рис. 97, для ключевого слова word
результатом поиска 6yjXQT found = 1; line = 2. Аналогично, для клю­
чевого слова a«<i результатом поиска 6yjxQT found = 0.
Как указано выше, эффективность поиска определяется чис­
лом обращений к таблице. Для последовательного поиска эффек-

290
тивность поиска составляет в среднем size/2 обращений.
Программный проект, в котором содержатся определения
функций для поиска в таблице и пример их использования, приво­
дится ниже. В примере на данном этапе следует рассмотреть только
данные и те фрагменты проекта, которые относятся к функции Se-
quentialSearch для последовательного поиска в таблице. К числу та­
ких фрагментов относятся, в том числе, функции AllocTableDM
(размещение таблицы в динамической памяти), FreeTableDM (осво­
бождение занятой таблицей динамической памяти), SeqlnpTab (за­
полнение таблицы), PrintTab (печать содержимого таблицы) и Print-
Search (вывод результатов поиска).

/*
Файл TestSearch.срр.
Тестирование поиска в таблице.
Определение методов поиска в таблице приведено в файле
Sea rch Tab! е. срр.
Заголовочный файл проекта - файл SearchTable. h.
Для откытия и закрытия файлов используются универсальные
функции^ определенные в файлах OpenCloseFile.h и
OpenCloseFile.срр.
Давыдов В.Г. Консольное приложение. Visual C++ 6
*/

// Включаемый файл программного проекта для поиска в таблице


^include "SearchTable.h"

±nt main ( // Возвращает О при успехе


±nt ArgC, // Число аргументов в командной
// строке
dbai: *ArgV[ ] ) / / Массив указателей на аргументы
// командной строки (ArgV[ О ] -
// .ехе файл, в интегрированной
// среде программирования известен
// и не задается/ ArgV[ 1 ] - файл
// ввода/ ArgV[ 2 ] - файл вывода)
{
// Проверка числа аргументов командной строки
±f( ArgC != 3 )
{
printf(
"\n Ошибка 5. В командной строке должно быть три аргумента: "
"\п Имя_проекта. ехе имя_файла_ввода имя_файла_вывода \п" ) /
exi t ( 5 ) /
}

// Создаем и инициализируем таблицу из 4 строк


AllocTableDM( 4) /

±пЬ found, // 1 - нашли ключевое слово


line/ // Индекс найденной строки

291
// Заполняем таблицу для последовательного поиска и
// печатаем ее
SeqInpTab ( ArgV[ 1 ] ) ;
PrintTab( ArgV[ 2 7/ "^"г
" Состояние таблицы:" ) ;

// Тестирование последовательного поиска в таблице


FILE *pStructFlleOut = OpenFile( ArgV[ 2 ], "a",
170 ) ;
fprintf( pStructFileOutr
"\n\n Тестирование последовательного поиска \л" ) ;
CloseFile( pStructFileOut, ArgV[ 2 7, 180 ) ;
SequentialSearch ( "and", founds line ) ;
PrintSearchi ArgV[ 2 ], "a", found, line, "and" ) ;
SequentialSearch( "word", found, line ) ;
PrintSearch( ArgV[ 2 ], "a", found, line, "word" ) ;
// Заполняем таблицу для логарифмического поиска и
// печатаем ее
InpTabLog( ArgV[ 1 ] ) ;
PrintTab( ArgVf 2 ], "a",
" Состояние таблицы:" ) ;

// Тестирование логарифмического поиска в таблице


pStructFileOut = OpenFile( ArgV[ 2 ], "а", 190 ) ;
fprintf( pStructFileOut,
"\n\n Тестирование логарифмического поиска \п" ) ;
CloseFile( pStructFileOut, ArgV[ 2 ], 200 ) ;
LogariphmSearch ( "and", found, line ) ;
PrintSearch ( ArgVf 2 ], "a", found, line, "and" ) ;
LogariphmSearch ( "word", found, line ) ;
PrintSearch( ArgV[ 2 ], "a", found, line, "word" ) ;

// Заполняем таблицу для хэш-поиска и печатаем ее


BeginTable( ArgV[ 1 ], 2 ) ;
PrintTab( ArgV[ 2 ], "a",
" Состояние таблицы:" ) ;

// Тестирование кэш-поиска в таблице


pStructFileOut = OpenFile( ArgV[ 2 ], "а", 210 ) ;
fprintf( pStructFileOut,
"\n\n Тестирование хэш-поиска \n" ) ;
CloseFile( pStructFileOut, ArgVf 2 ], 220 ) ;
HashSearch ( "work", found, line ) ;
PrintSearchi ArgV[ 2 ], "a", found, line, "work" ) ;
HashSearch( "type", found, line ) ;
PrintSearch ( ArgVf 2 ], "a", found, line, "type" ) ;

// Освобождение динамической памяти, занятой таблицей


FreeTableDMi ) ;

292
retuxm 0;
}
-_
Файл SearchTable.h. Включаемый файл для поиска в таблице.
Давыдов В. Г. Консольное приложение^ Visual C-h+ 6
V
// Предотвращение возможности многократного подключения
// данного файла
Hfndef SEARCHTABLE_H
^define SEARCHTABLE_H

^include <string.h> // Для строковых функций

// Для открытия-закрытия файлов


#Include "OpenCloseFile.h"

const ±ntLKEY = 9; // Длина ключа в строке таблицы


// LKEY-1
// Длина данного в строке таблицы LDATA-1
const ±nt LDATA = 63;

// Тип для строки таблицы


stmict STRTAB
{
// Ключ
char кеу[ LKEY ];
// Данные
char data[ LDATA ];
} ;

// Объявление объектов с описателем класса хранения


// внешний. Они доступны в других файлах проекта^ в
// которых подключен данный файл
extern STRTAB
*рТаЫе; // Адрес первой строки таблицы в
// динамической памяти
extern ±nt size; // Размер таблицы
// Указатель на структуру со сведениями о файле ввода
extern FILE
*pStructFlleInp;

// Прототипы функций
void AllocTableDM( int );
void. FreeTableDM( void );
void SeqInpTab ( char * ) ;
void PrlntTab ( char *pFlleOut, chstr *, char *pHead );
void SequentlalSearch ( char [ ], int <5, int & );
void PrlntSearch(,char *, char *, int, int, char [ ] );
void Round( int );
void InpTabLog ( char * );
void LogarlphmSearch ( char [ ], int &, int & );
int Kod( char );

293
±nt Hash ( chstr [ ] ) ;
void. BeglnTable ( сЬлг *, ±nt ) ;
void. HashSearch ( сЪах: [ ], inb &, inb &),

§endif

Файл OpenCloseFile.h. Включаемый файл для функций открытия


и закрытия файлов.
Используются в любых программных проектах.
Давыдов В.Г. Консольное приложение,. Visual Сч-+ 6

// Предотвращение возможности многократного подключения


// данного файла
i^lfndef OPENCLOSEFILE_H
^define OPENCLOSEFILE_H
^Include <stdlo.h> // Для ввода-вывода
ilnclude <stdllb.h> // Для функции exit ( )

// Прототипы функций
FILE * OpenFlle ( char *, char *, inb ) ;
void CloseFlle( FILE *, char *, int WarnNum ) ;

#endlf

Файл OpenCloseFile.cpp. Универсальные функции открытия и


3акрытия файлов.
Используются в любых программных проектах.
Давыдов В.Г. Консольное приложение. Visual C++ 6
*/
// Включаемый файл
ilnclude "OpenCloseFile.h"

// Открытие файла
FILE * OpenFlle ( // Возвращает указатель на структуру
// со сведениями об открытом файле
// Указатель на имя .расширение открываемого файла
сЬаг *pFl 1 eNam е ,
cha.r *pMode, // Указатель на режим открытия файла
// Номер ошибки или предупреждения
int ErrWarnNum )
{
// Указатель на структуру со сведениями об открытом файле
FILE *pStructFlle/

// Открытие файла
pStructFlle = fopen ( pFlleName, pMode ) ;
if( IpStructFlle )
{

294
printf(
"\n Ошибка %d. Ошибка открытия файла %s в режиме \"%s\"\п",
ErrWarnNum, pFileName, pMode ) ;
exit ( ErrWarnNum )/
}

jc&tum pStructFile;
)

// Закрытие файла
void. CloseFile (
// Указатель на структуру со сведениями о закрываемом
FILE *pStructFile,
// Указатель на имя.расширение закрываемого файла
char *pFileNaine^
Int WarnNum ) // Номер предупреждения
{
// Закрытие файла
х£( fclose( pStructFile ) == EOF )
{
printf(
"\n Предупреждение %d. Файл %s не закрыт. \n"
"\n Выполнение программы продолжается \ n " ,
WarnNum^ pFileName );
}

геЬгит;
}

Файл SearchTable.cpp.
Функции поиска в таблице:
* размещение таблицы в динамической памяти и ее
инициа ЛИЗ а ция ;
* освобождение динамической памяти, занятой таблицей/
* заполнение массива значениями, читаемыми из файла на
магнитном диске (для последовательного поиска);
* заполнение таблицы значениями, читаемыми из файла на
магнитном диске (для логарифмического поиска);
* вывод содержимого таблицы в файл на магнитном диске/
* последовательный поиск в таблице;
* вывод результатов поиска в таблице в файл на магнитном
диске;
* обход вершин дерева с целью формирования словаря для
бинарного (логарифмического) поиска;
* бинарный (логарифмический) поиск в таблице, подготовленной
в форме алфавитно-упорядоченного двоичного дерева/
* преобразование символа ключа ~ строчная латинская буква,
цифра или пробел - в его порядковый номер (целое число) ;
* хэш-функция ключа "KeyWord" из "LKEY-1" символа (символ -
строчная латинская буква, цифра или пробел) для таблицы из
size строк;
* начальная подготовка хэш-таблицы;

295
* поиск в хэш-таблице.
Используется в программном проекте для поиска в таблице.
Давыдов В.Г. Консольное приложение^ Visual C-h+ 6 I
JV i
// Включаемый файл программного проекта для поиска в таблице
^include "SearchTable.h"

// Определения объектов с описателем класса хранения внешний.


// Их объявление имеется в заголовочном файле проекта и
// доступно в других файлах проекта
STRTAB *рТаЫе; // Адрес первой строки таблицы в
// динамической памяти
±nt size; // Размер таблицы
// Указатель на структуру со сведениями о файле ввода
FILE *pStructFileInp;

// Размеш.ение таблицы в динамической памяти и ее


// инициализация
void AllocTableDM(
±nt s ) // Число строк таблицы
{
// Проверяем г подходит ли размер таблицы?
±£( S < 1 )
{
printf(
"\п Предупреждение 10. Таблица должна содержать не менее"
" двух строк"
"\п (задано %d строк) . Принимается размер таблицы 2. "
"\п Выполнение программы продолжается. " ) ;
S = 2;
}

// Размещаем таблицу в динамической памяти


рТаЫе = new STRTAB[ s ] ;
±f( !рТаЫе )
{
printf( "\n Ошибка 20. Таблица не была размеш,ена "
"в динамической памяти " ) ;
exit( 20 ) ;
}

// Инициализация таблицы
fori ±nt i = О; i < s; 1ч-+ )
{
pTablef i ] . key[ 0 ] = '\0'/
pTablel i ].data[ 0 ] = '\0';
}
size = s;
jretujrn/

296
// Освобождение динамической памяти, занятой таблицей
void FreeTableDM( void )
{
xf( рТаЫе )
{
delete [ ] рТаЫе; рТаЫе = NULL;
}

// Заполнение массива значениями, читаемыми из файла на


// магнитном диске (для последовательного поиска)
void SeqInpTab (
// Указатель на файл ввода
сЬаг *pFlleInp )
{
// Открытие файла для чтения
FILE *pStructFileInp = OpenFile ( pFilelnp,
"г", 30 ) ;

// Заполнение массива
£ою ( ±nt 1=0; Ksize; i ++ )
{
±f( fscanfi pStructFilelnp, " %s %s",
pTablef i ].key, pTable[ i ].data ) != 2 )
{
printf( "\n Ошибка 40. Ошибка чтения строки^
" таблицы с индексом %d ", i ) ;
exit( 40 ) ;
}
}

// Закрытие файла ввода


CloseFile( pStructFilelnp, pFilelnp, 50 ) ;
return/
}

// Заполнение таблицы значениями, читаемыми из файла на


// магнитном диске (для логарифмического поиска)
void InpТаbLog (
// Указатель на файл ввода
сЬаг *pFileInp )
{
// Открытие файла для чтения
pStructFilelnp = OpenFile( pFilelnp, "г", 60 ) ;

Round( 1 ) ; // Рекурсивное заполнение таблицы

// Закрытие файла ввода


297
CloseFile( pStructFilelnp, pFllelnp, 70 )
retvLrn;
}

// Вывод содержимого таблицы в файл на магнитном диске


void PrintTab(
char *pFlleOut,// Указатель на файл вывода
char- *pMode^ // Указатель на режим открытия файла
char "^pHead ) // Указатель на заголовок для печати
{
// Открытие файла для записи
FILE *pStructFileOut = OpenFile ( pFileOut, pMode,
80 ) ;
// Печать таблицы с заголовком
fprintf( pStructFileOut, "\п %s \п", pHead ) ;
tor( ±nt 1=0; l<size; i++ )
{
fprintfC pStructFlleOutr "\n %-8s%-62s",
pTable [ i ].key, pTable [ i J.data ) ;
}

// Закрытие файла вывода


CloseFile( pStructFlleOut, pFileOut, 90 ) ;
return;
}

// Последовательный поиск в таблице


void SequentlalSearch (
// Ключ для поиска строки в таблице
char Keyword [ ],
±nt &foundr // 1 - нашли
±пЬ &line ) // Индекс найденной строки
{
±nt 1, // Индекс анализируемой строки
EndTab; // 1 - конец заполненной части
// та блицы
// Подготовка к поиску
found = О; EndTab = 0;

// Поиск
1 = 0;
while ( .'found && /EndTab )
{
±£( Istrcmpi Keyword^ pTable [ 1 ] . key ) )
Щ { // Нашли
found = 1; line = 1;
}
else

298
// Шаг вперед по таблице
if( i == size-1 )
{
EndTab = 1/
;
else
{
i ++/
}
}
}

retuim;
}

// Вывод результатов поиска в таблице в файл на магнитном


// диске
void PrintSearch(
char *pFileOutг// Указатель на файл вывода
char *pMode, // Указатель на режим открытия файла
±nt found, // 1 - нашли в таблице
±iib line г // Индекс строки в таблице
// Ключевое слово для поиска
char Keyword[ ] )
{
// Открытие файла для записи
FILE *pStructFlleOut = OpenFile ( pFileOut, pMode,
100 ) ;

fprintf( pStructFlleOutr "\n Результаты поиска для"


" ключевого слова: %s\n'\ KeyWord ) ;
±f( found )
(
fprintf( pStructFileOutr
"Индекс строки в таблице: %d. Найденная строка: \ л " , line ) ;
fprintf( pStructFileOut, "%-8s%-62s \л",
рТаЫе [ line J.key, рТаЫе [ line ] .data ) ;
}
else
{
fprintf( pStructFileOut, "Строка с ключом \"%s\" в"
" таблице не найдена, \п", Keyword ) ;
}

// Закрытие файла вывода


CloseFile( pStructFileOutг pFileOut, 110 ) ;

return/
}

// Обход вершин дерева с целью формирования словаря для


// бинарного (логарифмического) поиска, !!! Читаемые данные

299
// должны быть ал фа витно-упорядоченными по ключам
void Round(
±nt root ) // Корень дерева
{
±£( size < root )
{ // !!! Выход из рекурсии

Round( 2*root ); // Обойти вершины левого поддерева

// Приписать корню очередную по алфавиту строку таблицы


±f( fscanf( pStructFilelnp, "%8s%62s",
рТаЫе[ root-1 ].key, рТаЫе[ root-1 ],data ) ! =2 )
{
printf(
"\n Ошибка 120. Ошибка чтения строки таблицы \п" );
exit ( 120 );
}

Round( 2*root+l ); // Обойти вершины правого поддерева

return/

// Бинарный (логарифмический) поиск в таблице, подготовленной


// в форме алфавитно-упорядоченного двоичного дерева
void. LogariphmSearch (
// Ключ для поиска строки в таблице
сЪаг Keyword [ ] ,
±nt &found, // 1 - нашли
±Tib Scline ) // Индекс найденной строки
{
int i, // Индекс вершины дерева
EndTab; // 1 - достигнут конец таблицы

// Подготовка к поиску
found = 0; EndTab = 0;

// Поиск
i = 1;
while ( ! found && .'EndTab )
{
±f( !strcmp( Keyword, pTable[ i-1 ] . key ) )
{ //Нашли
found = 1; line = i-1;
}
else
{ //Шаг вперед по таблице
±f( strcmp( Keyword, pTable [ i-1 ] , key ) < 0 )
{
i = 2*i/
}

300
else
{
1 = 2*i-i-l/
}
EndTab = (i > size ) ,

return;

// Преобразование символа ключа - строчная латинская буква,


// цифра или пробел - в его порядковый номер (целое число)
±п\ t Kod( // Порядковый номер символа
char symbol ) // Преобразуемый символ
(
switch ( S3/mbol )
{
case ' a ': return 0;
case 'b' : return 1;
case 'c' : return 2;
case d' : return 3;
case 'e' : return 4;
case f : return 5;
case g' : return 6;
case h'; return 7;
case 'i ': return 8;
case J'.• return 9;
case ;c' : return 10
case 1 ' : return 11
case m ': return 12
case n ': return 13
case o' : return 14
case p': return 15
case q': return 16
case r' : return 17
case s' : return 18
case t': return 19
case u': return 20
case V* : return 21
case w' : return 22
case X' : return 23
case y ' : return 24
case z ': return 25
case 0' : return 26
case 1 ': return 27
case 2': return 28
case 3' : return 29
case 4 ' : return 30
case 5' : return 31
case 6' : return 32
case 7': return 33.

301
case '8*: return 34;
case '9': return 35;
case ' ': return 36;
dLefaul t:
printf(
"\n Ошибка 130. Ключ поиска содержит недопустимый символ. \п"
"\п Ключ может содержать толька строчные латинские буквы, "
" цифры и пробел \п" ) ;
exit ( 130 ) ;
}

return -1; // Этот оператор не будет


// выполняться
}

// Хэш-функция ключа "KeyWord" из "LKEY-1" символа (символ -


// строчная латинская буква, цифра или пробел) для таблицы
// из size строк
int Hash( // Возвращает индекс строки таблицы
// Ключ
char Keyword [ ] )
{
unsigned. ±nt
I Key; // Индекс символа в ключе
±nt ih = 0; // Значение хэш-функции
// Вычисление индекса строки таблицы
for( IKey = 0; I Key < strlen ( KeyWord ) ; IKey-h-h )
{
ih = ih * 37 -f Kod( KeyWordf IKey ] ) ;
ih = ih % size;
}
return ih;
}

// Начальная подготовка хэш-таблицы


void BeginTable (
char *pFileInp,// Указатель на файл ввода
int ТаЫеЬеп ) / / Число вводимых строк
{
int i, // Индекс строки таблицы
line, // Номер текуш,ей строки
found; // 1 - найдена позиция вставки
// Заносимое слово
char Keyword [ LKEY ];

// Инициализация таблицы нуль-символ а ми


£ог( i = О; i < size; i++ )
{
£or( int il = 0; il < LKEY; il++ )

302
рТаЫе[ i ] . key [ 11 ] = '\0';
fox:( int 12 = 0; 12 < LDATA; 12 + + )
pTablef 1 ] .data[ 12 ] = '\0';
}

// Отметка строк таблицы как свободных


for( 1 = О; 1 < size; 1++ )
(
pTablef 1 J.keyf О ] ^ ' '/
}

// Открытие файла для чтения


pStructFllelnp == OpenFlle( pFllelnpr "г", 140 ) ;
// Занесение в таблицу исходных строк
for( line = 0; line < TableLen; llne++ )
{ // Цикл чтения исходных строк
±f( fscanf( pStructFllelnp, " %s", Keyword ) != 1 )
{
printf( "\n Ошибка 150. Ошибка чтения ключа из"
" строки с индексом %d \п", line ) ;
}

// Поиск индекса '1' строки таблицы для ее заполнения


found = О; // Пока индекс не найден
while( /found )
{
±f( pTablef 1 ].key[ 0 ] == ' ' )
{ // Индекс найден
found - 1;
}
else
{ // Конфликт - шаг по таблице
1++; 1 = ( 1 > ( slze-1 ) ? 0: 1 ) ;
}
}

// Чтение данного
±f( fscanf( pStructFllelnp, " %s",
pTablef 1 ] .data ) != 1 )
{
printf( "\n Ошибка 160. Ошибка чтения ключа из"
" строки с индексом %d \л", line ) /
exlt ( 160 ) ;
}

// Занесение ключа в строку "1" таблицы


strcpyi pTablef 1 ].key, KeyWord ) /

// Закрытие файла ввода


CloseFlle( pStructFllelnp, pFllelnp, 170 ) ;
геЬмтсп;

303
}

// Поиск В хэш-таблице
void HashSearch(
// Ключевое слово для поиска
char Keyword [ ],
±nt бе founds // 1 - нашли
±nt &11пе ) // Индекс найденной строки в таблице
{
±nt 1, // Индекс строки таблицы
EndTah; // 1 - достигли свободной строки
// Подготовка к поиску
found = О; EndTab = О; i = Hash( KeyWord ) ;
// Поиск в таблице
while ( ( .'found ) && ( .'EndTab ) )
{
±f( pTablel 1 J.keyf 0 ] == ' ' )
{ // Достигли свободной строки
EndTab = 1;
}
else
{
±£( ! strcmp ( pTablef 1 ] . key, KeyWord ) )
{ // Нашли
found = 1; line = i;
}
else
{ // Шаг no таблице
i++; i = ( i > ( size-1 ) ? 0: i ) ;
}
}
}

retujni/

Для файла исходных данных, имеющего вид


call вызов
type тип
word слово
work работа

был получен следующий файл результатов:


Состояние та блицы:

call вызов
type тип
word слово
work работа

304
Тестирование последовательного поиска
Результаты поиска для ключевого слова: and
Строка с ключом "and" в таблице не найдена.
Результаты поиска для ключевого слова: word
Индекс строки в таблице: 2. Найденная строка:
word слово
Состояние та блицы:

word слово
type тип
work работа
call вызов
Тестирование логарифмического поиска

Результаты поиска для ключевого слова: and


Строка с ключом "and" в таблице не найдена.

.Результаты поиска для ключевого слова: word


Индекс строки в таблице: О. Найденная строка:
word слово

Со стояние та блицы:

call вызов

type тип

Тестирование хэш-поиска
Результаты поиска для ключевого слова: work
Строка с ключом "work" в таблице не найдена.

Результаты поиска для ключевого слова: type


Индекс строки в таблице: 2. Найденная строка:
type тип

17.3. Логарифмический (бинарный) поиск

Кардинальное повышение эффективности поиска в таблице


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

Пример. В реальных словарях, например в англо-русском сло­


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

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

Начальная подготовка таблицы в форме двоичного дерева.


Исходные данные при заполнении таблицы перед чтением должны
быть упорядочены по алфавиту для ключевых слов. Это легко обес­
печить, используя рассмотренные выше способы сортировки масси­
вов.
Пусть, например, читаемые данные содержат в каждой отдель­
ной строке информацию для заполнения одной строки таблицы,
причем они отсортированы по ключам по не убыванию (size=\0):
А Data for string 0
В Data for string 1
С Data for string 2
D Data for string 3
Е Data for string 4
F Data for string 5
G Data for string 6
Н Data for string 7
I Data for string 8
J Data for string 9 (size-1)

Двоичное дерево строится, как это было ранее рассмотрено, с


использованием рекуррентного подхода (см. рис. 57). Для нашего
примера после начальной подготовки двоичное дерево должно
иметь вид, показанный на рис. 98.
Соответственно этому алгоритму можно записать рекуррент­
ную функцию Round и использующую ее функцию InpTabLog для
начального заполнения и подготовки таблицы, прототипы и опреде­
ления которых содержатся в вышеприведенной программе. Обрати­
те внимание на то, что функция Round является вспомогательной
для функции InpTabLog.
Бинарный (логарифмический) поиск в таблице, подготов­
ленной в форме двоичного дерева. Идея поиска состоит в следую­
щем.
1. Исходный ключ сравнивается с ключом, соответствующим
корню дерева (номер соответствующей вершины дерева / = 1, а ин­
декс элемента массива - (/ - 1) ). Если при этом ключи совпадают, то
нужная строка найдена {found = 1), ее индекс line = i - 1 и поиск за-

306
вершен.

} D

2
]\
/
/
1

В F н J

А
/
Л- -V
с Е
Последние слова по
10
алфавиту (H-I-J)
присваиваются
Первые слова по правому поддереву
алфавиту (A-B-C-D-E-F)
присваиваются левому
поддереву
Среднее слово по алфавиту (G) присваивается
корню.

Аналогично заполняются левые и правые


поддеревья для частичных корней
Рис. 98. Двоичное дерево после начального заполнения

2. Если поиск не завершен, то определяется поддерево для


продолжения поиска путем сравнения KeyWord < рТаЫо[ /-1 ].кеу.
При положительном итоге необходимо вести поиск в левом подде­
реве и номер следующей вершины / = /*2. При выполнении же про­
тивоположного условия KeyWord > рТаЫе[ /-1 ].кеу, поиск следует
вести в правом поддереве и номер следующей вершины дерева / =
/*2+1.
3. При выполнении условия i > size (вершину / дерево не со­
держит) поиск следует прекратить, так как строка с ключевым сло­
вом KeyWord отсутствует {EndTab = 1 w found = 0). Иначе - выполня­
ется переход к п. 1 с новым значением /, соответствующим корню
левого или правого поддерева.
Легко заметить, что после каждого сравнения KeyWord с клю­
чом рТаЫе[ /-1 ].кеу область поиска сокращается примерно в два
раза и среднее число обращений к таблице (средняя длина поиска)
составляет 1^^.,, =\og^{size), что существенно эффективнее, чем при по­
следовательном поиске.

307
в соответствии со сказанным, прототип, определение функции
LogariphmSearch и пример ее вызова имеют вид, показанный в про­
грамме из подразд. 17.2.

17.4. Поиск с использованием перемешанной таблицы


(хэш-таблицы)

п р и поиске с использованием хэш-таблицы используется ор­


ганизация данных в виде массива. Основная идея поиска состоит в
преобразовании заданного ключа Key Word в индекс Hash( Key Word )
соответствующей строки в таблице. Поэтому такой способ поиска
иногда называют поиском с преобразованием ключей (рис. 99).

рТаЫе

Keyword Hash(KeyWord)
Индекс строки в
таблице с
Исходный key = Keyword
ключ

Size-1
Ключ (key) Данное (data)
Рис. 99. Хэш-поиск в таблице

Основная трудность преобразования ключей состоит в том,


что множество возможных значений ключей намного обширнее, чем
множество индексов строк в таблице. Так, например, если ключ со­
держит восемь символов, в качестве которых используются строч­
ные латинские буквы, цифры и пробел (всего 37 возможных значе­
ний каждого символа в ключе), то всего имеется 37^ возможных
значений ключей, что, естественно, во много раз превышает реаль­
ный размер таблицы size. Из сказанного следует, что функция Hash
является отображением "много в один".
Идея поиска с использованием хэил-таблицы состоит в сле­
дующем. Первый этап в операции поиска - вычисление соответст­
вующего индекса Hash{ KeyWord ) в таблице, а второй - очевидно
необходимый этап - проверка, действительно ли элемент с ключом
KeyWord находится в таблице в строке с индексом Hash( KeyWord ).
При этом сразу же возникают два вопроса.

308
1. Какую функцию Hash( KeyWord ) следует использовать?
2. Как поступать в ситуации, когда функция Hash{ KeyWord )
не дает местонахождения нужного элемента (! много ключей дают
одинаковый индекс)?
Ответ на второй вопрос заключается в том, что нужно исполь­
зовать какой-то метод для получения нового индекса в таблице, а
если и там нет нужного элемента, то следующего индекса и т.д. По­
добный случай, когда в строке Hash{ KeyWord ) находится другой
ключ, а не ключ KeyWord^ называется конфликтом, а задача получе­
ния альтернативных индексов li^зыв2iQTCЯ разрешением конфликтов.

Выбор функции преобразования. Основное требование к хо­


рошей функции преобразования Hash{ KeyWord ) состоит в том,
чтобы она распределяла ключи как можно более равномерно по
шкале значений индексов. Разумеется, она должна также эффектив­
но вычисляться, т.е. состоять из очень небольшого числа основных
арифметических действий.
Пусть ih определяет порядковый номер ключевого слова Key-
Word во множестве всех возможных значений ключей и вычисляется
следующим образом:
unsigned. ±nt
I Key; // Индекс символа в ключе
Int ih = О; // Значение хэш-функции

// Вычисление индекса строки таблицы


for( IKey = 0; I Key < strlen ( KeyWord ) ; IKey-h-h )
{
ih = ih * 37 + Kod( KeyWord[ IKey ] ) ;
}
ih = ih % size;

В результате вычислений ih получает значение из диапазона 0-


36. К сожалению, величина ih существенно превышает максимально
допустимое целое значение (2'^-1 или 2^'~1). По этой причине
функцию Hash{ KeyWord ) следует построить несколько иначе — вы­
числение
ih = ih % size;

перенести в блок цикла. Прототип полученной таким образом хэш-


функции и ее определение приведены в примере программы в под-
разд. 17.2. Функция Hash{ KeyWord ) также является вспомогатель­
ной и используется при хэш-поиске. Эта функция обладает тем
свойством, что значения ключей равномерно распределяются во
всем интервале индексов строк таблицы. Исследованиями показано,
что для большей равномерности распределения желательно, чтобы

309
size было простым числом (см. Вирт Н., Алгоритмы + структуры
данных = программы: Пер. с англ. М.: Мир, 1985. С. 305).

Разрешение конфликтов. Если строка в таблице рТаЫе, соот­


ветствующая заданному ключу Key Word, не содержит нужный эле­
мент, то имеет место конфликт. Это означает, что два или более
элементов таблицы имеют ключи, отображающиеся в один и тот же
хэш-индекс строки таблицы. Для разрешения конфликтов такого ро­
да существуют различные методы получения вторичных индексов.
Один из методов разрешения конфликтов состоит в просмотре
одного за другим различных элементов таблицы, начиная со строки
с индексом Hash( Key Word ), пока не будет найден нужный элемент
или не встретится свободное, не заполненное место таблицы. По­
следнее означает отсутствие в таблице строки с заданным ключом.
Этот метод называется открытой адресацией. Разумеется, что шаг
просмотра элементов таблицы при вторичных пробах должен быть
постоянным. Одним из таких методов является метод линейного ап­
робирования с открытой адресацией. Реализация этого метода со­
держится в определении функции HashSearch.

Отметка в таблице свободных мест. Для этой цели можно,


например, в первый символ ключа (байт) свободной строки таблицы
записать символ пробела:
// Отметка строк таблицы как свободных
fox:( 1 = О/ i < size/ i-h+ )
(
рТаЫе[ i ] . key [ О ] = ' ';
I
Начальная подготовка хэш-таблицы. При начальном запол­
нении хэш-таблицы также может иметь место конфликт. В связи с
этим сделаем валсное замечание. При хэш-поиске и при начальной
подготовке таблицы для разрешения конфликта следует использо­
вать один и тот эюе метод. В нашем примере таким методом явля­
ется метод линейного апробирования с открытой адресацией. Про­
тотип функции BeginTable, используемой для начального заполне­
ния хэш-таблицы, и ее определение имеются в примере, приведен­
ном в подразд. 17.2. Функция BeginTable является интерфейсной
функцией.

Функция для поиска в хэш-таблице. Прототип функции


HashSearch и ее определение имеются в примере, приведенном вы­
ше в подразд. 17.2. Функция HashSearch также является интерфейс­
ной функцией. Обратите внимание на то, что функции BeginTable и

310
HashSearch очень похожи друг на друга.
Пример тестирования хэш-поиска в таблице имеется в под-
разд. 17.2 (см. функцию main).

Эффективность хэш-поиска. Проведенный для линейного


апробирования анализ показал, что среднее значение числа проб при
поиске (длина поиска)
\-al2
WP ~
\-а '
где a = TabLen/size есть коэффициент заполненности таблицы (табл.
30).

Табл. 30. Эффективность хэш -поиска


а 0.1 0.2 0.3 0.5 0.9
L,,, 1.056 1.125 1.214 1.5 5.5

Из таблицы следует, что хэш-поиск имеет весьма высокую эф­


фективность. Но при этом важно понимать и недостатки данного
метода.
1. Существенное повышение эффективности поиска достига­
ется только при большой избыточности таблицы.
2. Сложность удаления элемента из таблицы.
В заключение отметим, что из перечисленных выше классиче­
ских задач прикладного программирования^ составляющих золотой
багаж любого программиста - сортировка массивов, транспортная
задача (задача коммивояжера), поиск в таблице, обработка списков,
работа с очередями; сортировка файлов ) — мы рассмотрели решение
первых трех классов задач прикладного программирования. Осталь­
ные задачи будут рассмотрены в следующем учебном пособии "Тех­
нология программирования" в связи с изучением и освоейием дру­
гих технологий программирования, таких как объектно-
ориентированное программирование, программирование с исполь­
зованием библиотеки стандартных классов языка C++ и др. Учебное
пособие "Технология программирования" предназначено для обес­
печения одноименной дисциплины, изучаемой в следующем, треть­
ем семестре в рамках подготовки бакалавров (направление 5502) и
специалистов (направление 6519).

ЗП
18- О Т В Е Т Ы И РЕШЕНИЯ К УПРАЖНЕНИЯМ
Д Л Я САМОПРОВЕРКИ

18.1. Для подраздела 2.4.4

Ответ к упражнению 1.
retcode^l 1=17 j=123 с1=4 с2=5 сЗ=6 а=2400,000000
Ъ=172,000000

Ответ к упражнению 2.

Файл 2_4_4_2.СРР

2. Имеется следующий фрагмент Си-программы:

float a;
±nt i^ jr
cbSLr cl, c2r c3;
±nt retcode;
ch^jc c4, c5r s[20]

Написать фрагмент программыг обеспечивающий чтение из


файлаf.dat на магнитном диске следующих значений:

а = 1,5 1 = 21 j = -12 с1 = 'в'


с2 = ' е ' сЗ = ' с ' с4 ^ 'а' с5 = ' н '
S = "Прочитанная-строка"

Как при этом будут выглядеть строки исходных данных в


файле f.dat?
Предусмотреть контроль корректности значений^ возвращае­
мых функциями библиотеки Си.
В. Давыдов^ консольное приложение (Microsoft Visual
Studio C++ 6.О)
*/
#include <stdio.h> // STanDart Input Output - для
// стандартных функций ввода-
// вывода

±nt main ( void ) // Возвращает О при успехе


{
float а;
Int iг j /
chctr cl, c2, c3;
char c4, c5^ s[20];

312
FILE *f_in; / / Указатель на файл для ввода
±пЬ ret code; // Возвращаемое значение для функции
// fscanf

// Открываем файл f.dat для чтения


f_±n = fopen( "f.dat", "г" );
±f( f__in == NULL )
{
printf( "\n Файл f.dat для чтения не открыт. " );
jzebvucn 1;
}

// Чтение данных из файла f.dat


retcode = fscanf( f_±n, " a = %f 1 = %d j = %d "
"cl = \'%c\' c2 = \'%c\' c3 = \'%c\' "
"c4 ^ \'%c\' c5 = \*%c\^ S = \"%s\" ",
&a, &i, &j, &cl, &c2, &c3, &c4, &c5, s );
if( retcode != 9 )
{
printf( "\n Данные прочитаны с ошибками." );
retvim 2;
}

// Закрываем файл ввода


fclose ( f_in ) ;

z-etiLm О;

Ответ к упражнению 3.

Файл 2_4_4_З.СРР

3. В программе имеются следующие переменные:

±nt d = 254/
float f = 1234.56;
cha.r *str = "Строка символов"/

Используя, по возможности, только эти данные написать


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

1+254^''^^^''^]^'^[^^''''^254]
(^^^^^1234.5600) ^^ (1234.5600^^'^^^)
/Стр/^^/м/

В. Давыдов, консольное приложение (Microsoft Visual


Studio C++ 6. 0)

^Include <stdlo.h> // STanDart Input Output - для


// стандартных функций ввода-

313
// вывода
±nt main ( void ) // Возвращает О при успехе
{
int d = 254;
float f = 1234.56f;
cha,r *str = "Строка символов"/
FILE *f_out; // Указатель на файл для взвода
// Открываем файл file,out для записи
f_out = fopen( "file.out"г "w" ) ;
±f( f_out == NULL )
{
printf ( "\n Файл file, out для записи не открыт. " ).
return 1;
}

// Вывод данных в файл file.out


fprintf( f_out, "[%+-lld],%2c[%8d] \n (%14. 4f) %2c("
"%-14.4f)\n/%.3s/%2c/%c/\n ", d, str[ 6 ], d, f,
str[ 6 ; , f, str, strf 6 ]r str[ 9 ] ) ;
// Закрываем файл взвода
fclose ( f_out ) ;

return 0;

18.2. Для подраздела 3.8

Ответ к упражнению 1.

Будет напечатано:
i=l j=3
next ( )=11
last ( )=0
nw(i+j) =9

Ответ к упражнению 2.

Будет напечатано:
i == 3 j = 1
next ( i ) = 3
last ( i ) =10

i = 3 j ^ 2
next ( i ) = 4
last ( i ) = 9

314
18.3. Для подраздела 3.9.3

Ответ купрамснению 1,

Фа ил 3_ 9_3_1. срр

Написать прототип^ определение функции и пример вызова


функции для вычисления суммы элементов одномерного массива
х[ N ] (N = 50) целого типа ^ имеющих нечетные индексы

В. Давыдов, Консольное приложение (Microsoft Visual


Studio C++ 6.О)
V
^include <stdio.h> // Для ввода-вывода

^define N50 // Размер массива

// Прототип
±nt SumUneven( ±nt ar[ ] ) ;
±nt main ( void ) // Возвращает 0 при успехе
{
Int a[ N ] ;
// Инициализация массива
toj: ( ±zib i=0/ i<N; i + + )
a[ i ] ^ 1;
// Вызов функции
±nt s = SumUneven( a ) ;
// Печать найденной суммы
printf( " Сумма значений элементов массива с нечетными "
"индексами = %d \л", s ) ;

x-etujm 0;
}

// Вычисление суммы значений элементов вектора с нечетными


// индексами
Int SumUneven ( // Возвращает сумму значений
// элементов с нечетными
// индексами
±пЬ аг[ ] ) // Обрабатываемый массив
{
int Sum = 0; // Искомая сумма
// Поиск суммы
£ог( Int i=l; i<N; i+=2 )

315
Sum += ar[ i ]/
return Sum/
}

Ответ к упражнению 2.

Файл 3_9_3_2. срр


Написать прототип, определение функции и пример вызова
функции для получения одномерного массива z[ N ] (N = 40) из
двух заданных массивов целого типа х[ N ], у[ N ] по правилу:
z[ i ] := тах{ х[ ± ], у[ ± ] }

В. Давыдов. Консольное приложение (Microsoft Visual


Studio C+-h 6, О)
*/
^include <stdio.h> // Для ввода-вывода

^define N40 // Размер массивов

// Прототип
void. CreateArr ( Int x[ ], int y[ 7, Int z[ ] ) ;
int main ( void ) // Возвращает 0 при успехе
{
±nt x[N],y[N],z[N];
// Инициализация исходных массивов
£or( int i=--0; i<N; i + + )
{
x[ i ] = 1; y[ i ] = 0;
}
// Вызов функции
CreateArr( x, y, z ) ;

retujm 0;
}

// Формирование массива
void CreateArr(
int X [ 7, // Исходные
int y[ ], // массивы
int z[ ] ) // Формируемый массив
{
// Формирование массива
for( int i=0; i<N; i+=2 )
{
if( x[ i ]>y[ i 7 ;
z[ i ] = x[ i ];

316
else
z[ i ] = y[ ± ];
}

18.4. Для подраздела 3.9.6

Ответ к упражнению 1,

/*
Файл 3_9_3_2, срр
1. В текстовом файле "ctrl4, dat" имеется 15 строк, каждая
из которых имеет следующий формат:
число_ 1 число_ 2
Здесь "число_1" определяет вид геометрической фигуры (1 -
квадрат, 2 - круг) , а "число_2" - параметр фигуры (при "чис-
ло_1" = 1 ~ длина стороны, а при "число_2" = 2 - радиус) .
1.1. Написать определение массива структур для хранения
указанных сведений о геометрических фигурах. Каждый элемент
массива должен иметь следующие поля:
* имя фигуры;
* длина стороны или радиус;
* площадь фигуры.
1.2. Написать фрагмент программы для чтения из файла на
магнитном диске "ctг14.dat" информации о геометрических фигу­
рах.
1.3. Написать фрагмент программы, вычисляющий площади
геометрических фигур.
1.4. Написать фрагмент программы, печатающий в файл
"ctrl4.out" параметры геометрических фигур. Сведения об от­
дельных фигурах располагаются в отдельной строке и имеют вид:
круг: радиус= . . . , площадь= . . .
или
квадрат: длина стороны= . . . , площадь= . . .
Предусмотреть контроль корректности значений, возвращае­
мых функциями библиотеки Си, указать какие включаемые файлы
требует представленный фрагмент.

В. Давыдов. Консольное приложение (Microsoft Visual


Studio C++ 6.0)
V
^include <stdlo.h> // Для ввода-вывода
^include <string.h> // Для строковых функций
#define N15 // Размер массива структур

int main ( void ) // Возвращает О при успехе


(

317
// Определение массива фигур
sbiract GeomFigure
{
cbai: name [ 8 ];// Название фигуры
double pa ram; // Параметр фигуры: длина стороны
// или радиус
double square/ // Площадь фигуры
) агг[ N ]; // Массив геометрических фигур
// Заполнение массива структур со сведениями о
// геометрических фигурах и вычисление их площадей
FILE *f__ln; // Указатель на файл для ввода
// Открываем файл ctrl4,dat для чтения
f__in = fopen( "ctrl4,dat", "г" ) ;
±£( f_±n == NULL )
{
print f ( "\n Файл ctrl4. dat для чтения не открыт. " ) ;
jretuxn 1;
}
±zib Tag; // 1 - квадрат^ 2 - круг
double pa ram; // Параметр фигуры
for( ±nt 1=0; KN; 1 ++)
{
±f( fscanf( f_ln, " %d %lf", &Tag, &param ) != 2 )
{
printf( "\n Ошибка чтения " ) ;
return 2;
}
switch ( Tag )
{
ca.se 1:
strcpy ( arr[ i J.name, "Квадрат" ) ;
arr[ 1 ] .pa ram = pa ram;
arr[ 1 ]. square = pa ram "&param;
break;
case 2:
strcpy( arr[ 1 J.name^ "Круг" ) ;
arr[ 1 ].pa ram = pa ram;
arr[ 1 ].square = 3.141592*param*param;
break;
default:
return 3;
}
}
// Закрываем файл чтения
fclose ( f_in ) ;
// Печать сведений о геометрических фигурах
FILE *f_out; // Указатель на файл для вывода
// Открываем файл ctrl4.out для записи
f_out = fopen( "ctrl4.out", "w" ) ;
lf( f out == NULL )

318
prlntf ( "\n Файл ctrl4. out для записи не открыт. " ) ;
jretujrn 4;
}
fo3z( 1=0/ KN/ 1++)
{
±£( !strcmp ( arrf 1 ] . name, "Квадрат" ) )
{
fprlntf( f_out, "\n Квадрат: длина стороны=%1д, "
"площадь = %1д " , arr[ 1 ] ,param,
arrf 1 ]. square ) ;
}
else
{
fprlntf( f_out, "\n круг: радиус=%1д, "
"площадь=%1д " , arr[ 1 ] .param,
arr[ 1 ]. square ) ;

// Закрываем файл вывода


fclose ( f_out ) ;
retuim 0;

18.5. Для подраздела 4.12

Ответ к упражнению 1 (рис. 100).

Нет

Нет

Рис. 100. Ответ к упражнению 1

Ответ к упралснению 2.
±f( a<=b )

к=п;

319
r=l;
}
else
r=3;

Ответ к упражнению 3.
swi tab( 1 )
{
case 4:
n-h+ ;
break;

case 1: case 7; case 9:


n=a+b;
break;

de£ault:
n=a-b;

Ответ к упражнению 4.

Будет напечатано:
•к

-- О -- -1 — -2 -- -3 -- -4

Ответ к упражнению 5.
/*
Файл 4__12_5. срр
5.Пусть определен массив
int а[ 25 ];
Напишите фрагмент Си-программы, который напечатает с но­
вой строки значения элементов "а" по 5 элементов в строке и
по 10 позиций на элемент. Решить задачу с помош^ю цикла
while.
В. Давыдов. Консольное приложение (Microsoft Visual Stu­
dio C++ 6. 0)
*/
^include <stdio.h> // Для ввода-вывода

Int main ( vo±dL ) // Возвращает 0 при успехе


{
int a[ 25 ];
// Инициализация массива
for( int i=0; i<25; i + + )

320
{
а[ i ] = 1;
}
// Печать массива
± = О;
while( i<25 )
{
±f( !( 1%5 ) )
printf ( "\п" ) ;
printf( "%10d", a[ ± ] ) ;
i ++ ;
}
printf ( "\n" ) ;

z-etux-n 0;

Ответ к упражнению 6.

При любых исходных значениях "А:" цикл будет выполняться


конечное число раз.

18.6. Для подраздела 6.9

Ответ к упражнению 7.

Будет напечатано:
10 13 16
15 13 11

Ответ купрамснению 2.

Будет напечатано:
рр-р = 4 *рр-а - 4 **рр = 14
рр-р = 3 *рр-а = 3 **рр = 13
рр-р - • = 4 *рр-а = 4 **рр = 14
рр-р = 4 *рр-а = 3 **рр = 13

18.7. Для подраздела 8.16

Ответ к упражнениям 1-3.

Файл LS.CPP

321
Определены следующие данные:

struct ELEM // Структура для элемента списка


{
Int dat; // Данное
ELEM *next; // Указатель на следующей элемент
} *start // Указатель на начало списка

Во входном файле Is,dat содержится некоторое количество


целых чисел г разделенных символами пробельной группы ( ' ',
'\t', '\л ' ; .
1. Написать прототип, определение и пример вызова функции
для ввода из входного файла имеющихся там чисел, представив
введенную информацию линейным списком, в котором каждый узел
(динамически размещенная структура) содержит две компоненты.
Первая компонента хранит данное (введенное число), а вторая -
указывает адрес следующей структуры. При этом первое прочи­
танное число должно находиться в начале линейного списка. Ис­
ходные данные и результаты работы функции следует передавать
через список параметров,
С целью обработки ошибок предусмотреть контроль значений,
возвращаемых функциями библиотеки языков Си/C++,
2. Дополнительно написать прототип, определение и пример
вызова функции для печати в файл ks.out на магнитном диске
содержимого линейного списка, Требования к оформлению функции
и обработке ошибок аналогичны указанным выше требованиям,
3. Дополнительно написать прототип, определение и пример
вызова функции, которая разрушает линейный список. Требования
к оформлению функции и обработке ошибок аналогичны указанным
в пункте 1 требованиям,

В. Давыдов, Консольное приложение (Microsoft Visual


Studio C++ 6,0)
*/
^Include <stdlo.h> // Для функций ввода-вывода
^Include <stdllb,h> // Для функции exit

stmict EL // Структура для элемента списка


{
int dat; // Данные (целое)
EL *next; // Указатель на следующий элемент
} ;

// Прототипы функций
void Create__beg ( EL *&, char * ) ;
void Add_end( EL *&, int ) ; \
void Prlnt_ls ( EL *, char *, char * ) /
void Dest_ls( EL *& ) ;
void Del_beg( EL *& ) ;

int main ( void ) // Возвращает 0 при успехе


{
// Указатель на начало списка

322
EL *start;

start = NULL/ // Инициализация списка


// Заполнение линейного списка символами из файла LS.DAT:
// первый прочитанный символ - в начале списка
Create_beg( starts "LS.DAT" );
// Вывод содержимого списка в файл
Print_ls( start, "LS.OUT"r "w");
Dest_ls ( start ); // Разрушение списка

jretixzm 0;
}

// Заполнение линейного списка символами из файла LS.DAT:


// первый прочитанный символ - в начале списка
void. Crea te_beg (
EL *&start, // Указатель на начало списка
// Указатель на файл ввода
cbstr *pFlleInp )
{
// Данное для элемента, добавляемого в конец списка
Int 1/
/ / Указатель на структуру со сведениями о файле для
// чтения
FILE *f_±n;

// Открываем файл для чтения


±£( ( f_in = fopen( pFllelnp, "г" ) ) == NULL )
{
printf
( "\n Файл %s для чтения не открыт \ л " ,
pFilelnp ) ;
exit(l);
}
±nt re;
// Указатель на файл ввода // Создаем список
wb±le( ( ГС = fscanfC f_in, " %d ", &i ) ) == 1 )
{
Add_end( start, i );
}
// Закрываем файл
±£( ( fclose( f_in ) ) =- EOF )
{
printf( "\n Файл %s не закрыт \ л " , pFilelnp );
exit(2);
}
jretixzm/
;

// Добавление элемента в конец списка


void Add_end(
EL *&start, // указатель на начало списка

323
int i) // Данные добавляемого элемента
// Указатель на новый (добавляемый) элемент списка
EL *temp,
*сиг; // Указатель на текущий элемент

temp = new EL; // 1: динамическое размещение


// элемента
±f( temp == NULL )
{
printf( "\n Элемент списка не размещен \п" ) /
exit ( 3 ) ;
}
temp->dat = 1; //2: занесение данного
temp->next = NULL; // 3: новый элемент является
// последним
±£( start == NULL ) // Новый список (пустой)
start = temp; // 4а: указатель на начало списка
else
{
// 46: проходим весь список от начала^ пока текущий
// элемент не станет последним
сиг = start;
wb±le( cur->next != NULL )
// Продвижение по списку
сиг = cur->next;
// 4в: ссылка последнего элемента на новый^
// добавляемый в конец списка
cur->next = temp;
}
геЬизпл;
}

// Печать содержимого списка на экран


void Prlnt_ls (
EL *start, // Указатель на начало списка
сЬа2Г *pFileOut,// Указатель на файл вывода
char *pMode ) // Указатель на режим открытия файла
(
EL *ргп; // Указатель на печатаемый элемент
±£( start == NULL )
{
printf ( "\п Список пуст. Распечатывать нечего \п" ) ;
retu2m;
}
// Открываем файл вывода
FILE *f_out = fopen( pFileOut, pMode ) ;
±f( !f__out )
{
printf ( "\n Ошибка открытия файла вывода &s \ л " ,
pFlleOut ) ;
324
exit( 4 );
}
prn = start; // Указатель на начало списка
fprintf( f_out, "\л Состояние линейного списка: \п" ) /
int i ^ О;
while ( prn ! = NULL ) // До конца списка
{
if( ! ( i%4 ) )
fprintfi f_out, "\n" ) ;
// Печать данных элемента
fprintfi f_out, "%15d", prn->dat ) ;
prn = prn->next; // Продвижение no списку

/ / Закрываем файл вывода


fclose( f_out ) /

rBturzi/
}

//***********************************************************
/ / Разрушение списка
void Dest_ls (
EL *&start ) // Указатель на начало списка
{
±f( start == NULL )
{
printf( "\n Список пуст. Удалять нечего" ) ;
return;
}
while( start /= NULL )
Del_beg( start ) ; / / Удаление первого элемента списка
return;
}

// * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * * *

325
// Удаление первого элемента списка
void Del_beg(
EL *&start ) // Указатель на начало списка
{
EL *del; // Указатель на удаляемый элемент
±f( start == NULL )
{
printf ( "\n Список пуст. Удалять нечего" ) ;
return/
}
// 1: подготовка первого элемента для удаления
del - start;
start = del->next; // 2: start сдвигается на второй
// элемент
delete del; // 1: удаление первого элемента
return;
ПРИЛОЖЕНИЯ

Приложение П.1. Тесты и программные проекты.


Варианты заданий

П.1.1. Тесты (контрольные работы)

На практических занятиях по основным разделам курса целе­


сообразно провести тестирование. Такими разделами являются:

• программирование на ПМ-ассемблере;
Q ввод;
о вывод;
• простейшие ветвления;
а циклы;
а структуры;
Q функции;
о области действия определений;
о массивы и указатели;
о работа с динамической памятью и операции с линейным списком;
а препроцессор, перечисления, функции с умалчиваемыми значениями ар-
гументов, перегрузка функций, шаблоны функций, перегрузка операций.

П.1.1.1. Программирование на ПМ-ассемблере. Варианты тестов

Изобразить схему программы и написать законченную


программу на языке ПМ для решения заданной задачи. Для ввода и
вывода использовать файлы MS DOS. Для обеспечения нагляд-
ности вывода использовать строковые данные.

Вариант 1. Ввести и напечатать значения элементов массива


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

Вариант 2. Ввести и напечатать значения элементов массива


целого типа с фиксированным размером 8 (для упрощения размер
массива вводить не нужно). Вычислить и напечатать значение наи-

327
большего элемента массива.

Вариант 3. Ввести и напечатать значения элементов массива


вещественного типа с размером 10. Вычислить и напечатать количе­
ство отрицательных элементов массива.

Вариант 4, Ввести и напечатать значения элементов массива


вещественного типа с размером 20. Вычислить и напечатать индекс
наименьшего элемента массива.

Вариант 5. Ввести и напечатать значения элементов массива


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

Вариант 6, Ввести и напечатать значение переменной х веще­


ственного типа. Вычислить для нее восьмую степень и напечатать
вычисленное значение. Решить задачу с использованием только
трех умножений.

Вариант 7. Ввести и напечатать значения переменных а, 6, с


вещественного типа. Определить наибольшее значение среди них,
присвоить его переменной d и напечатать. Решить задачу с исполь­
зованием только двух сравнений.

Вариант 8, Ввести и напечатать значения переменных а, Ь, с


вещественного типа. Определить и напечатать, сколько среди них
отличных от нуля.

Вариант 9, Ввести и напечатать значения переменных а, b це­


лого типа. Определить, равны ли они друг другу, и напечатать ответ.

Вариант 10. Ввести и напечатать значения переменных а, b


вещественного типа. Определить количество положительных значе­
ний среди заданных и напечатать ответ.

Вариант 11, Ввести и напечатать значения переменной х ве­


щественного типа. Вычислить и напечатать значение функции у :=
\х\.

Вариант 12. Ввести и напечатать значения переменных а, Ь, с,


d вещественного типа. Определить и напечатать z := max( min( а, b ),
max( с, d ) ) .

328
Вариант 13, Ввести и напечатать значения переменных х, у и
Z вещественного типа. Вычислить и напечатать значения
переменных и := т а х ( х, у, z ) , / : = min( х, у, z ).

Вариант 14. Ввести и напечатать значения переменных а, Ь, с,


d вещественного типа. Сделать такую перестановку значений этих
переменных, чтобы а приняло значение 6, b приняло значение с, с
приняло значение а. Значения этих переменных после перестановки
также напечатать.

Вариант 15. Ввести и напечатать значения переменных хну


вещественного типа. Вычислить и напечатать значения перемен­
ных и := т а х ( д:, у ) , / : = min( д:, у ).

Вариант 16. Ввести и напечатать значения переменных х, у^ z


вещественного типа. Вычислить и напечатать целое/7 по правилу:

1 при к = min{ X^ у^ Z ) ,
Р := 2 при у = min{ X^ у г Z ) ,
3 при Z = m i n ( X, у г z )

Вариант 17. Ввести и напечатать значения переменных а, 6, с


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

Вариант 18. Ввести и напечатать значение х вещественного


типа. Вычислить и напечатать значение у:

+1 при X > О ^
У := О п р и X = О,
-1 при X < О

Вариант 19. Ввести и напечатать значения переменных а, Ь, с,


d вещественного типа. Определить и напечатать количество нулевых
значений среди заданных.

Вариант 20. Ввести и напечатать значения переменных а, 6, с,


d вещественного типа, причем два из них одинаковы. Найти и напе­
чатать значение, отличное от этих двух.

329
п. 1.1.2. Ввод в языках Си/С++. Варианты тестов

Ниже приведены варианты фрагмента программного кода, содер­


жащего определения некоторых переменных. В комментариях к опреде­
лениям переменных указано, какие значения переменных нужно ввести.
Написать фрагмент программы, обеспечивающий:
• открытие файла (потока Си) '4npuf^ для работы с файлом операцион­
ной системы "Test2.m'^;
• ввод из этого файла (потока Си) значений переменных, указанных в
комментариях к программному фрагменту соответствующего вари­
анта;
• закрытие файла (потока Си).
Указать, как при этом будут выглядеть строки исходных дан­
ных в файле операционной системы ^^Test2Jn^^ (сделайте это обяза­
тельно, иначе Ваш ответ нельзя будет проверить).
Предусмотреть контроль корректности значений, возвращае­
мых функциями библиотеки Си ^^foperC\ ^^fscanf\ Подключить не-
обходимые стандартные заголовочные файлы.

Вариант 7.
double d; // 4. 7
char s[ 3 ]; // "Ой
unsigned long uli; // 31
short si; // 12
char c; // 'r'
xnt 1; // -21

Вариант^ 2.
long double b; //4.7
char s[ 3 ]; // "Я"
long i; // -1
short j ; //12

Вариант 3.
long double b; // 4.7e2
char s[ 20 ]; // "4"
int i; // 12
unsigned j ; // 0x21

Вариант 4.
double b; //4.7
char s[ 20 ]; // "Отлично'
long int i; // -21

330
unsxgned. long- J/ // 0x12

Вариант 5.
float <a. // 1.5
b ; // 14. 7
±nt if // -21
J/ // 12
char Clr // 'y'
c2. // 'P'
c3 , // 'a '
c4. // '/ '
s[20]; // "Прочитa иная-строка

Вариант 6.

float b; // 14.0
±nt J/ // 12
unstgnedL u; // 21
cbeir c4. // 'P'
s[20]; // "Зимний-вeчер"

Вариант 7.
double d; // 2.0
float a. // 1.5
b; // 12.21
Int i/ // -21
unsigned J/ // 0x12
char Clr // '1 '
c2. // '2'
c3 , // '3'
c4. // '4'
n n It
s[20] ; //

Вариант 8.

long double d; // 1.5


float a; // 1.5
long ±nt ±; // -1
unsigned j r // 13
char Clr // '4'
s[20] ; ngn
//

Вариант 9.

float a; // 1.5
int ir // 21
J/ // -12
char Clr // 'в'
c2r // 'e'

^1
s[20]; // "Прочмтанная-строка'

Варит чт 10.
float br // 5.0
k; // 15,123
long^ ±nt a; // 27
char Clr // 'B'
s[20] ; // "Строка

Вариант 11, Имеется следующий фрагмент Си-программы:


float а, Ь;
±nt
char clr s[20]

Строки исходных данных в файле (потоке Си)- ''stdin'' имеют


следующий вид (каждая клетка содержит один символ):
+ + + + + + + + + + + + + + +
I I I - I 2 I I I 1 1 1 . 1 5 1 1 . 1 1 1 \п1
+ + + + + + + + + + + + + + +
| Э | т | о | | с ! г | р | о | к | а | I | \ л |
+ + + + + + + + + + + + + +
1 1 1 2 1 I I I \л|
+ + + + + + +
Написать фрагмент программы, обеспечивающий чтение из
файла с указателем ''stdin^\ следующих значений:
а : 1.5 (должно быть прочитано значение 1.5)
b : 14.7 i : -21 j : 12 cl : ' . '
S : "строка"

Предусмотреть контроль корректности значений, возвращае­


мых функцией библиотеки Си ^^scanf\ Подключить необходимые
стандартные заголовочные файлы.

Вариант 12, Имеется следующий фрагмент Си-программы:


float a г b;
±nt ir j ;
char clr c2r c3;
±nt RetCode;
RetCode = fscanf( stdirir " %i %3d %c %c %c %f %f " ,
&ir &jr &clr &c2r Scc3r &br &a ) ;

Строки исходных данных в файле (потоке Си) "stdin" имеют


следующий вид (каждая клетка содержит один символ):

332
+ + + + + + + 4-
+ + + + + + +
I I 1 - I 7 I 7 I | - 1 2 | 4 | 3 1 5 | 5 | 7 | \ л |
+ + + + + + + + + -|. + + + -I- +
I \ 2 \ . \ 4 \ e \ 3 \ I I i I 4 I . I 7 I \л|
+ + + + + + + + + + + + + +
I I I 7 I 2 I I I \л|
+ + + + + + +
К а к и е з н а ч е н и я п о л у ч а т п е р е м е н н ы е RetCode, а, b, /, j , cl, c2,
c3?

Вариант 13, И м е е т с я с л е д у ю щ и й ф р а г м е н т С и - п р о г р а м м ы :

float а, Ь/
±nt i, j ;
chsLx: cl, c2, c3;
±nt RetCode;
RetCode = fscanf( stdin, " %o 2%ld %c 5%c %c %f %f " ,
&i, &jr &cl, &c2, &c3, &b, &a ) ;

С т р о к и и с х о д н ы х д а н н ы х в ф а й л е ( п о т о к е С и ) ''stdin" имеют
следующий вид (каждая клетка содержит один символ):

I I I 17 17 1 I I 2 I 4 I 3 I 5 I 5 I 7 I \л|
+ + + + + + + + + + + + + -I- +
I \ 2 \ . \ 4 \ е \ 3 \ I \ 1 \ 4 \ . \ 7 \ \п\
+ + + + + + + + + + + + + +
I I I 7 I 2 I I I \л|
+ + + + + + +
Какие значения получат переменные RetCode, а, Ь, i, j , с 1, с2,
сЗ?

Вариант 14, И м е е т с я с л е д у ю щ и й ф р а г м е н т С и - п р о г р а м м ы :

float а г Ь;
±nt i/ J/
chstr clr s[ 20 ];

Написать фрагмент программы, обеспечивающий:


открытие файла (потока Си) ''Input" для работы с файлом операцион­
ной системы "Test2.in";
ввод из этого файла (потока Си) следующих значений указанных ни­
же переменных:
а • 1.5 Ь . 4. 7
1 • -21 J • 12
t г
с1 S . "String

• закрытие файла (потока Си).


При этом строки исходных данных в файле операционной сис­
т е м ы "Test2Jn" имеют следующий вид (каждая клетка содержит

333
один символ):
+ + + + + + + + + + + + + + +
I I \ S \ t \ r \ i \ n \ g \ \ 1 \ 2 \ 1 1\л|

I J I . I 5 I I \ - \ 2 \ 1 \ I 4 I . I 7 I \л|
+ + + + + + + + + + + + + +
I I - I . I - I I \л|
+ + + + + + +
Предусмотреть контроль корректности значений, возвра­
щаемых функциями библиотеки Си ^^fopen^\ ^^fscanf\ Подклю­
чить необходимые стандартные заголовочные файлы.

Вариант 15. Имеется следующий фрагмент Си-программы:


flo&t a;
double Ь;
±nt i;
unsigned Jr

Написать фрагмент программы, обеспечивающий:


• открытие файла (потока Си) '4npuf для работы с файлом операцион­
ной системы ''Test2.w''\
• ввод из этого файла (потока Си) следующих значений указанных ни­
же переменных:
а : 1.5 b : 4.7
i : -21 j : 0x12

• закрытие файла (потока Си).


При этом строки исходных данных в файле операционной сис­
темы ''Test2An'' имеют следующий вид (каждая клетка содержит
один символ):
+ + + + + + + + + + + + + + +
I I I I I I I \ о \ к \ 1 \ 2 \ I \ \п\
+ + + + + + + + + + + + + + +
\ 1 \ . \ 5 \ I \ - \ 2 \ 1 \ I 4 I . I 7 I \л|
-I- + 4- + + + + + + + +• + + +
Предусмотреть контроль корректности значений, возвра­
щаемых функциями библиотеки Си ^^fopen^\ ^^fscanf\ Подклю­
чить необходимые стандартные заголовочные файлы.

Вариант 16. Имеется следующий фрагмент Си-программы:


flostt а;
double Ь;
long int i;
unsigned long j ;

Написать фрагмент программы, обеспечивающий:

334
• открытие файла (потока Си) "Input'' для работы с файлом операцион­
ной системы "Test2Jn'';
• ввод из этого файла (потока Си) следующих значений указанных ни­
же переменных:
а: 1,5 Ь : 4.7
±: -21 j : 0x12

• закрытие файла (потока Си).


При этом строки исходных данных в файле операционной систе­
мы "Test2Jn'' имеют следующий вид (каждая клетка содержит один сим­
вол):
-j. + + + + -j- + + + -I- + + + -I- +
I I I I I I I 10 1 x 1 1 1 2 1 I I \л|

I i 1 . I 3 I I 1 - I 2 I I I I 4 I . I 7 I \л|
+ + + + + + + + + + + + + +
Предусмотреть контроль корректности значений, возвра­
щаемых функциями библиотеки Си ^^fopen^\ ^^fscanf\ Подклю­
чить необходимые стандартные заголовочные файлы.

Вариант 17, Имеется следующий фрагмент Си-программы:


float а г Ь;
±nt i, j , ret code/
chcir cl, c2 r c3;
RetCode = fscanf( stdin, " %i %4d %c %c %c %f %f ",
&i, &j, &cl, &c2, &c3, &ar &b ) ;

При этом строки исходных данных в потоке stdin имеют сле­


дующий вид (каждая клетка содержит один символ):
+ + + + + + + + + + + + + + +
I I 1 1 1 7 1 \ 1 \ 2 \ 3 \ 4 \ 5 \ 6 \ 1 \ \ \п\
+ + + + + + + + + + + 4- + + +
\ 2 \ . \ 4 \ е \ 2 \ 1 1 I 7 I 2 I | 1 | . | 5 | \ л |
+ + + + + + + + + + + + + + +
Какие значения получат переменные RetCode, а, Ь, i, j , cl, с2,
сЗ?

Вариант 18. Имеется следующий фрагмент Си-программы:


float а, b;
±nt ^r J/
cha.r cl, s[ 20 ];

Написать фрагмент программы, обеспечивающий:


открытие файла (потока Си) '4npuf' для работы с файлом операцион­
ной системы ''Test2dn''\

335
• ввод из этого файла (потока Си) следующих значений указанных ни­
же переменных:
а : 1.5 b : 14.7
1 : 1 j : 12
cl: ' .' s : "Это хорошо"

• закрытие файла (потока Си).


При этом строки исходных данных в файле операционной систе­
мы "Test2.iii" имеют следующий вид (каждая клетка содержит один
символ):
+ + + + + + + -1- + + -I- + + + +
\ Э \ т \ о \ \ к \ о \ р \ о \ ш \ о \ I | 1 1 \ п |

1 1 1 . 1 5 1 I 1 I 2 I I I 1 . 1 I I \л|
+ + + + + + + + + + + + + +
\ 1 \ 4 \ . \ 7 \ I I I I I ! I I \л|
+ + + + + + + + + + + + + +
Предусмотреть контроль корректности значений, возвра­
щаемых функциями библиотеки Си ^^fopen^\ ^^fscanf\ Подклю­
чить необходимые стандартные заголовочные файлы.

Вариант 19, Имеется следующий фрагмент Си-программы:


float b;
±nt J, retcode;
unsigned. u;
char c4, s[ 20 ]

Написать фрагмент программы, обеспечивающий:


• открытие файла (потока Си) "Input'' для работы с файлом операцион­
ной системы "Test2»in";
• ввод из этого файла (потока Си) следующих значений указанных ни­
же переменных:
и : 21 b : 14.7
j : 12
с4: 'р' S : "Зима-вечер"

• закрытие файла (потока Си).


При этом строки исходных данных в файле операционной систе­
мы "Test2.in" имеют следующий вид (каждая клетка содержит один сим­
вол):

336
+ + + + + + + + + + + + + + +
I S l M l M l a l - l B l e l ^ J l e l p I \ p \ \ \n\
+ +- - - + + + + + + + + + + + + +
I I 1 I 1 1 1 2 1 I I \ 2 \ 1 \ \ \n\
+ + + -f + + + -I- -f + 4- -I- 4- +
I i I 4 I . I 7 I I I I 1 I I I I \ л |
H + + + + + + H + + + + + +
Предусмотреть контроль корректности значений, возвра­
щаемых функциями библиотеки Си ^^fopen^\ ^^fscanf\ Подклю­
чить необходимые стандартные заголовочные файлы.

Вариант 20. Имеется следующий фрагмент Си-программы:


float Ь;
ant jr retcode;
unsigned. и;
ciiajT c4, s[ 20 ];

Написать фрагмент программы, обеспечивающий:


• открытие файла (потока Си) ''Input" для работы с файлом операцион­
ной системы "Test2Jn";
• ввод из этого файла (потока Си) следующих значений указанных ни­
же переменных:
и : 21 Ь . 14. 7
1 • 12
с4: 'Р' S . "Ура-вечер"

• закрытие файла (потока Си).


При этом строки исходных данных в файле операционной систе­
мы "Test2Jn'' имеют следующий вид (каждая клетка содержит один сим­
вол):
+ + 4- + + + + + + + + + + + +
1 " | 3 ^ 1 р | а | - | в ! е | ч | е | р | " | | | \ л |
+ + + + + + + + + + + + + + +
I ' I р I ' I I I I 2 I I I I 2 I I I I \л|
+ + -|. 4- + + + + + + Ч- + + +
I 1 1 4 I . I 7 I I I I I I I I I \ л |
-I- + ^ + 4- + + + + + + + + +
Предусмотреть контроль корректности значений, возвра­
щаемых функциями библиотеки Си ^^fopen^\ ^^fscanf\ Подклю­
чить необходимые стандартные заголовочные файлы.

П. 1.1.3. Вывод в языках Си/С++. Варианты тестов

Ниже приведены варианты фрагмента программного кода, со­


держащего вывод в файл (поток Си) ''stdouf\ Укажите, как будут
выглядеть строки вывода в файл (поток Си) ''stdouf после выполне­
ния заданного фрагмента. Для удобства в приведенных вариантах
337
фрагментов программного кода символ "^" обозначает пробельный
символ.

Вариант 1.
floett г;
±пЬ 1 = 17;
г = l,5f * 2.0elf;
fprlntf ( stdout, "*r=%5.2e^%s'^*l = %—i'd\n*%-3s\n".

Вариант 2.
float r;
±nb 1 = 17/
r = 1.5 * 2,Of­
fprint f( stdout, "*r^%5.2f^%5s^*l=%-+10d\n*%-30s\n",
r, " _ " , 1, "*", );

Вариант 3.
double r;
int 1 = 17;
r = 1.543 * 2. 0;
fprlntf( stdoutr "*r=%5.21f'^%-4s^-^l = %- + 10d\n*%-8s\n",

Вариант^ 4.
float r = 3.0;
int 1 = 17;
fprlntf ( stdoutr "*r=%5.2f''%5s^*l = %- + 10d\n*%-30s\n",
T^ II n ,• II Tic " ) .

Вариант 5.
float r = 1.5e2;
±nt 1 = 7 ;
fprlntf( stdout, "^%30s\n^r=%f^%5s^l=%10d\n", "*", r ,
" " " , 1 );

Вариант 6.
float r = 1.5e2;
int 1 = 5 ;
fprlntf( stdout, "^r=%f'^%-5s^l = %+10d\n'^%2.3s",
Гг "*", 1, "строка" );

Ниже приведены варианты фрагментов программного кода,


содержащие определения данных и их инициализацию.

338
Написать фрагмент программы, обеспечивающий:
• Опфытие файла (потока Си) ^^Outpuf^ для работы с файлом операци­
онной системы ^^ Tests. ouf\
• Вывод в открытый поток ''Outpuf строк заданного вида.
Указание. При выводе максимально использовать указанные в вариан­
тах данные и возможности форматированного вывода.
• Закрытие файла (потока Си).
Предусмотреть контроль корректности значений, возвращае­
мых функциями библиотеки Qnfopen п/close. Подключить необхо-
димые стандартные заголовочные файлы.

Вариант 7. Фрагмент Си-программы:


long d = 254;
double f = 1234.56;
cha.r str[ ] = "Строка 1";

Вид выводимых строк (ниже знак ^ обозначает пробел):


[+254^^]-^^ [-^^254]
(+1234.6^) (1.234560E-h03^^)

Вариант 8. Фрагмент Си-программы:


±nt d = 254;
float f = 1234.56;
chstr str[ ] = "Строка";

Вид выводимых строк (ниже знак ^ обозначает пробел):


[^'^•f-254]'^[254]
(+1234.6'^) (1.234560Е+03)

Вариант 9. Фрагмент Си-программы:


±nt d = 254;
float t f = 1234.56;
cha.r str[ ] = "Строка

Вид выводимых строк (ниже знак '^ обозначает пробел):


[^'^+254]^[254]
(+1234.6^) (1.234560Е+03)
/^^^-^^^-^^^^Стр/

Вариант 10. Фрагмент Си-программы:


±nt d = 254;
flosLt f = 1234.56;
сЬлг *str = "Строка символов";

339
Вид выводимых строк (ниже знак ^ обозначает пробел):
[+254] '^^[''^^^^254]
(^^1234, б) ^-^ (1.234560Е+03)
/^^'-^^^'^^^^Стр/^^ /м/

Вариант 77. Фрагмент Си-программы:


int d = 254;
float f = 1234.56;
cha.xr "^str = "Строка символов";

Вид ВЫВОДИМЫХ строк (ниже знак ^ обозначает пробел):


(-^'-^^^1234,5600) ^^ (1234.5600^^^^'')
/Стр/^^/м/

Вариант 12. Фрагмент Си-программы:


±пЬ d = 113;
float f = 12.34;
char *str = "Строка символов";

Вид ВЫВОДИМЫХ строк (ниже знак ^ обозначает пробел):


[+ 113^^^^^]^^[^^^'-^254]
(--^+12.3)
/Строка^^^^^/^^/ил/

Вариант, 13, Фрагмент Си-программы:


±nt d = 254;
float f = 1234.56;
сЪат *str = "Строка символов";

Вид выводимых строк (ниже знак ^ обозначает пробел):


^^^^^254]'^^[^^254^^''^^^^'^^^]
(+1234. 6) ^'^ (^^^^^1.23Е+03)
/ ^ ^ ^ ^ ^Строка/ "^ ^ / ^ "^лов/

Вариант 14. Фрагмент Си-программы:


±nt d - 254;
float f = 1234.56;
char *str = "Строка символов";

Вид выводимых строк (ниже знак ^ обозначает пробел):


Y ^ ^ ^ ^ ^+254]^"[254 -^---;
(1234. 5-----;^^(^1.234Е+03)
/Стр - - ' - - - / ' - ^/мв/

340
Вариант 15. Фрагмент Си-программы:
±пЬ d = 254;
float, f = 1234.5 6;
char *str = "Строка символов";

Вид выводимых строк (ниже знак ^ обозначает пробел):


[-254^^^^^]^^[^^^^^+254]
(^^^+1234.5600)
/Строка симв^^^^/^^/т/

Вариант 16. Фрагмент Си-программы:


xnt d = 123;
float f = 1234.56;
char *str = "Прочитанная строка";

Вид ВЫВОДИМЫХ строк (ниже знак ^ обозначает пробел):


[^-^^^^^123]^^[1234. 6-^^^^^]
(123-----)
/^^^^^^^^^^Просто/

Вариант 17. Фрагмент Си-программы:


±Tit d = 123;
float f = 1234.56;
char *str = "Прочитанная строка";

Вид выводимых строк (ниже знак ^ обозначает пробел):


[----- + 123]--[ + 1234. 6-----J
(-----123.)

Вариант 18. Фрагмент Си-программы:


±nt d = 254;
float f = 1234.56;
char *str = "Строка символов";

Вид ВЫВОДИМЫХ строк (ниже знак ^ обозначает пробел):


[+254]--[-----254]
(--1234. 6) -- (1.234560Е+03)
/^^^^^-^^^^^Стр/-- /м/

Вариант 19. Фрагмент Си-программы:


int d = 123;
float f = 1234.56;
char *str = "Прочитанная строка";

341
Вид выводимых строк (ниже знак ^ обозначает пробел):
(123 ;

Вариант 20. Фрагмент Си-программы:


±пЬ d = 123;
float f = 1234.56;
сЪ.а.х: *str = "Прочитанная строка";

Вид ВЫВОДИМЫХ строк (ниже знак ^ обозначает пробел):


[^^^^-^ + 123] ^^[ + 1234. 6^^^^^]
(^^^^^123,)

* П.1.1.4. Простейшие ветвления. Варианты тестов

Вариант 1. С помощью операторов ветвлений и присваивания


записать фрагмент программы, вычисляющий значение переменной
п по следующему правилу:
[ л+1 при 1=1 или 1=5^
п := [ а+Ь при 1=7 или 1=12,
[ а-Ь в остальных случаях

Вариант 2. С помощью операторов ветвлений и присваивания


записать фрагмент программы, вычисляющий значение переменной
п по следующему правилу:
[ п+1 при а>0 и Ь = 0 ,
п : = [ а-^Ь п р и а<=0 и Ь = 0 ,
[ а-Ь в остальных случаях

Вариант 3. Изобразить фрагмент схемы алгоритма, соответст­


вующий следующему фрагменту программы:
±f ( с < 3 ) ±f ( с == 2 ) а++; else b-h-h; а += 1;

Вариант 4. Изобразить фрагмент схемы алгоритма, соответст­


вующий следующему фрагменту программы:
±f ( с < 3 ) ±£ ( с -== 2 ) а + + ; Ь++; а += 1;

Вариант 5. С помощью операторов ветвлений и присваивания


записать фрагмент программы, вычисляющий значение переменной
п по следующему правилу:

342
[ 1 при i=l или 2 или 1,
л := [ 2 при 1=10,
[ О в остальных случаях

Вариант 6. Изобразить фрагмент схемы алгоритма, соответст­


вующий следующему фрагменту программы:
±f( с == 1 ) а + + ; else ±f( с == 2 ) а-~; else ±f( с === 3 )
а -h= 1/

Вариант 7. С помощью операторов ветвлений и присваивания


записать фрагмент программы, вычисляющий значение переменной
п по следующему правилу:
[ л+1 при 1=4,
л := [ а+Ь при 1=1 или 7 или 9,
[ а-Ь в остальных случаях

Вариант 8. С помощью операторов ветвлений и присваивания


записать фрагмент программы, вычисляющий значение переменной
Z по следующему правилу:
[ х+5 при а>2 и Ь=0,
Z := [ а+Ь при а<0,
[ X в остальных случаях

Вариант 9. Изобразить фрагмент схемы алгоритма, соответст­


вующий следующему фрагменту программы:
±f( с < 3 ) ±£( с == 2 ) а + + / else b++/ ±f( с < 2 ) C+ +;
а += 1;

Вариант 10. Записать фрагмент программы, соответствующий


следующему фрагменту схемы программы (рис. 101):

: > ^ Да
уНет
R:=X; P:=Y;

i к
Q:=1;

R:=Y; Р:=Х:

Рис. 101. Фрагмент схемы программы

Вариант 11. Записать фрагмент программы, соответствующий


следующему фрагменту схемы программы (рис. 102):

343
Да
X>Y Q:=1;

Нет
R:=Y; P:=X;

Рис. 102. Фрагмент схемы программы

Вариант 12. Изобразить фрагмент схемы алгоритма, соответ­


ствующий следующему фрагменту программы:
±£( с < 3 ) ±f( с == 2 ) a-h + ; else b+-h; ±f( с < 2 ) c+-h/
else a +=^ 1; { C+ + / b+ +; }

Вариант 13. Изобразить фрагмент схемы алгоритма, соответ­


ствующий следующему фрагменту программы:
[ х+5 при 1 = 1 , 3 , 5 ;
Z := [ а-\-Ь при 1 = 2 , 4 , 6 ;
[ к в остальных случаях

Вариант 14. Записать фрагмент программы, соответствующий


следующему фрагменту схемы программы (рис. 103):

Да
^а<=Ь^ a:=d;

Нет
R:=X; P:=Y;

Рис. 103. Фрагмент схемы программы

Вариант 15. Изобразить фрагмент схемы алгоритма, соответ­


ствующий следующему фрагменту программы:
±f( с <= 1 ) а + + ; else ±f( с == 5 ) а--; else а *= 2;

Вариант 16. Записать фрагмент программы, соответствующий


следующему фрагменту схемы программы (рис. 104):

V
у"^"" a:=d;
Да i L

R:=X; P:=Y;

Рис. 104. Фрагмент схемы программы

344
Вариант 17, С помощью операторов ветвлений и присваива­
ния записать фрагмент программы, вычисляющий величину i ( i >= О
) по следующему правилу:
[ л+1 при 1=0,
л := [ а+Ь при 1=2,4,6,8,10 и т.д.
[ а-Ь в остальных случаях

Вариант 18. Изобразить фрагмент схемы алгоритма, соответ­


ствующий следующему фрагменту программы:
±£( с < 5 ) ±f( с == 1 ) а + + ; Ь- -= 1;

Вариант 19. С помощью операторов ветвлений и присваива­


ния записать фрагмент программы, вычисляющий значение пере­
менной Z по следующему правилу:
[ х-ь5 при а>2 и Ь = 0 ,
Z := [ а+Ь при а < = 2 ,
[ X в остальных случаях

Вариант 20. Записать фрагмент программы, соответствующий


следующему фрагменту схемы программы (рис. 105):

V^^ R:=X; P:=Y;


уНет i i


Рис. 105. Фрагмент схемы программы

П.1.1.5. Циклы. Варианты тестов

Вариант 1. Задан массив:


float а[ 34 ];

Написать фрагмент программы, который напечатает с новой


строки значения элементов массива по пять элементов в строке и по
пятнадцать позиций на элемент. Печатаемые значения прижимать к
левой границе поля вывода, а положительные значения печатать со
знаком плюс. Решить задачу с помощью цикла/Ьг.

345
Вариант 2. Задан массив:
double а[ 104 ];

Написать фрагмент программы, который напечатает с новой


строки значения элементов массива по пять элементов в строке и по
пятнадцать позиций на элемент. Печатаемые значения прижимать к
правой границе поля вывода, а положительные значения печатать со
знаком плюс. Решить задачу с помощью цикла do-while.

Вариант J. Задан массив:


dLovible а[ 43 ] ;

Написать фрагмент программы, который напечатает с новой


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

Вариант 4, Задан массив:


воллЫе а[ 50 ];

Написать фрагмент программы, который напечатает с новой


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

Вариант 5. Задан массив:


double а[ 50 ];

Написать фрагмент программы, который напечатает в уже от­


крытый поток ''Output с новой строки значения элементов массива
по четыре элемента в строке и по двадцать позиций на элемент.
Печатаемые значения прижимать к правой границе поля вывода, а в
дробной части печатать шесть цифр. Решить задачу с помощью цик­
ла do..while. Использовать только один цикл.
Подключить необходимые стандартные заголовочные файлы.

Вариант 6, Дано следующее определение:


±пЬ к;

346
При каких исходных значениях к приведенный ниже цикл бу­
дет выполняться бесконечно?
wb±le ( к < 5 ) k+'h;

Возможные варианты ответов: при к <= ..., или при к >= ..., или
таких к не существует.

Вариант 7. Пусть определены переменные


±пЬ к, п;

Укажите, что напечатает следующий фрагмент программы (ни­


же знак ^ обозначает пробел):
prlntf( "\n\n^%-2s-\n"r " " ) ;
for( к = 7; к >= 5; к— ;
I
printf( "\п\п" ) ;
п = 6 - к; printf( "%i--%-h3d-%5s--'\ к, л, "-" ) ;
}

Вариант 8. Дано следующее определение:


int к;

При каких исходных значениях к приведенный ниже цикл бу­


дет выполняться бесконечно?
Willie ( к >^ 15 ) к+ + ;

Возможные варианты ответов: при А: <= ..., или при к >— ..., или
таких к не существует.

Вариант 9. Пусть определены переменные:


int к, п;

Укажите, что напечатает следующий фрагмент программы (ни­


же знак ^ обозначает пробел):
printf( "\n\n\t^%-2s-\n"r "12345" ) ;
for( к = 5; к > 5; к++ )
{
printf( "\л\л" ; /
п = б - к/ pr±ntf( "%±-^%4d^%2s^^", кг п, "-" ) ;
}

Вариант 10. Дано следующее определение:

347
±nt к;
При каких исходных значениях к приведенный ниже цикл бу­
дет выполняться бесконечно?
do
{
к+ + ;
} while ( к > 10 ) ;

Возможные варианты ответов: при к <= ..., или при к >= ..., или
таких к не существует.

Вариант 11, Пусть определены переменные:


±пЬ к, п;

Укажите, что напечатает следующий фрагмент программы (ни­


же знак ^ обозначает пробел):
printf( "\n%-3.2s\n", "*****" ) ;
foi:( к = 5; к > 5; к-- )
{
п ^ 6 - к; printf( "%i--%04d-%2s--", к, л, "-" ) ;
}

Вариант 12. Дано следующее определение:


int к;

При каких исходных значениях к приведенный ниже цикл бу­


дет выполняться бесконечно?
do
{
к++ ;
} while ( к > -5 ) ;

Возможные варианты ответов: при к <= ..., или при к >= ..., или
таких к не существует.

Вариант 13. Пусть определены переменные:


int к, п;

Укажите, что напечатает следующий фрагмент программы (ни­


же знак ^ обозначает пробел):
printf( "\n\n-%-5s-\n", " " ;/

348
fojc( к ^ 5; к >= 5; к— ;
(
printf( "\п\п" ) ;
п = 6 - к; printf( "%l--%4d-%2s--", к, л, "-" ) ;
}

Вариант 14. Дано следующее определение:


±nt к;
При каких исходных значениях к приведенный ниже цикл бу­
дет выполняться бесконечно?
while( к < 12 ) к++;

Возможные варианты ответов: при к <= ..., или при к >= ..., или
таких к не существует.

Вариант 15. Пусть определены переменные:


±пЬ кг п;

Укажите, что напечатает следующий фрагмент программы (ни­


же знак ^ обозначает пробел):
printf( "\n%3s\n", "-" ) ;
for( к = 5; к > 5; к-- )

{
п = б - к; printf( "%i--%4d-%2s--", к, л, "-" ) ;
}

Вариант 16. Сколько раз будет выполнено тело приведенного


ниже цикла?
for( ±nt к=4; к<17; к+=3 ) /

Возможные варианты ответов: тело цикла будет выполнено ...


раз или цикл будет выполняться бесконечно.

Вариант 17. Пусть определены переменные:


±пЬ к, п;

Укажите, что напечатает следующий фрагмент программы (ни­


же знак ^ обозначает пробел):
printf( "\n%3s\n", "-12345" ) ;
for( к = 5; к >= 1; к— ;
(

349
п = 8 - к/ pr±ntf( "%i--%4d-%2s--", к, л, "-12" ) ;
}

Вариант 18. Сколько раз будет выполнено тело приведенного


ниже цикла?
±пЬ с = 3;
foxi ±nt к=4/ к<17; к+=3, с+=2 ) ;

Какое значение будет иметь с после выхода из цикла?

Вариант 19. Пусть определены переменные:


±пЬ к;

Укажите, что напечатает следующий фрагмент программы (ни­


же знак ^ обозначает пробел):
prlntf( "\n-%-5s%s-\n", "*", "+" ) ;
for( к = 1; к >= -3; к— ;
pr±ntf( "-%5d-%3s-"r к, "--" ) ;

Вариант 20. Пусть определены переменные:


±nt к^ п;

Укажите, что напечатает следующий фрагмент программы (ни­


же знак ^ обозначает пробел):
printf( "\n%6s\n", "-" ) /
toxi к = 5/ к >= 1; к-- )
{
п = 6 - к; pr±ntf( "%i--%4d-%2s--", к, л, "***" ) ;
}

П.1.1.6. Структуры. Варианты тестов

В ответах на приведенные ниже варианты тестов необходимо


выполнить следующее.
Закрыть открытые файлы, как только они станут не нужны.
Предусмотреть контроль корректности значений, возвращае­
мых функциями библиотеки Си ^^fopeti'^ ^^fscanf\ Указать, какие
включаемые файлы требует представленный фрагмент.

Вариант 1. В файле операционной системы "Task4Jn'' хранит­


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

350
порядковый номер студента в группе; позиция 3 - пробельная лите­
ра; позиции 4...22 - фамилия студента длиной не более 18 символов,
в произвольном месте поля; позиция 23 - пробельная литера; пози­
ция 24.- четыре оценки по четырем предметам, разделенные не ме­
нее чем одной пробельной литерой. Количество студентов в группе
равно 16. Пример строк указанного файла:
01 Андреев 5 4 5 5
02 Быков 5 5 5 5
16 Яковлев 4 4 5 4

Написать: 1) определение массива структур для хранения ука­


занной ведомости; 2) фрагмент программы, который заполнит экза­
менационную ведомость данными, вводимыми из файла операцион­
ной системы "Task4Jn" (ввод данных должен осуществляться в тек­
стовом режиме; 3) фрагмент программы, который вычисляет сред­
нюю экзаменационную оценку по всем предметам и студентам (т.е.
среднюю оценку из 64 оценок), а затем выводит значение этого по­
казателя в файл операционной системы "Task4,out",

Вариант 2. В файле операционной системы "f.in" имеется 10


строк, каждая из которых содержит длины сторон прямоугольников
(значения длин задаются в формате с плавающей точкой и разделе­
ны пробелами).
Написать: 1) определение массива структур для хранения ука­
занных длин сторон прямоугольников, их площадей и периметров;
2) фрагмент программы для чтения длин сторон прямоугольников из
файла операционной системы "/.ш"; 3) фрагмент программы, вычис­
ляющий и печатающий площади и периметры прямоугольников в
файл операционной системы ''f.ouf\

Вариант 3. Имеется следующий фрагмент программы:


stiract ExamReport // Строка экз. ведомости
{
// Фамилия студента
char Name [ 15 ] ;
unsigned Mark; // Экзаменационная оценка
} ;
/ / MA ТНета tics : в едомость по ма тема тике
ExamReport Math [ 16 ];

Написать фрагмент программы, который заполнит экзамена­


ционную ведомость ^^Math^^ данными, вводимыми из файла опера­
ционной системы "Task4.in". Ввод данных должен осуществляться в
текстовом режиме. В каждой строке файла "Task4.in" содержатся

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

Вариант 4, В файле операционной системы ''Task4.m" хранит­


ся в текстовой форме ведомость сдачи экзаменов студентами неко­
торой группы. Каждая строка этого файла содержит сведения об од­
ном студенте, представленные в следующем формате: позиции 1...2 -
порядковый номер студента в группе; позиция 3 - пробельная лите­
ра; позиции 4... 15 - фамилия студента длиной не более 11 символов,
в произвольном месте поля; позиция 16 - пробельная литера; пози­
ция 17 - три оценки по трем предметам, разделенные не менее чем
одной пробельной литерой. Количество студентов в группе равно
16. Пример строк указанного файла:
01 Андреев 5 4 5
02 Быков 5 5 5

16 Яковлев 4 5 4

Написать: 1) определение массива структур для хранения ука­


занной ведомости, причем в связи с каждым студентом необходимо
хранить только фамилию и три оценки, а порядковый номер студен­
та должен быть представлен неявно, индексом элемента массива
структур; 2) фрагмент программы, который заполнит экзаменацион­
ную ведомость данными, вводимыми из файла операционной систе­
мы ''Task4Jn" (ввод данных должен осуществляться в текстовом ре­
жиме); 3) фрагмент программы, который вычисляет среднюю экза­
менационную оценку по всем предметам и студентам (т.е. среднюю
оценку из 48 оценок), а затем выводит значение этого показателя в
файл операционной системы "Task4.out'\
Замечание, Очевидно, каждая строка исходных данных со­
держит лишние сведения: порядковый номер студента в группе
(в начале строки). При вводе эти номера следует игнорировать
(каким-либо способом).

Вариант 5. Имеется следующий фрагмент программы:


struct ExamReport // Строка экз. ведомости
{
// Фамилия студента
cJiar- Name [ 15 ];
unsigned. Mark; // Экзаменационная оценка
} ;
// MATHematlcs: экзаменационная ведомость ,по математике
ExamReport Math[ 16 ];

352
в каждой строке файла "Task4.in" содержатся следующие поля
данных: фамилия студента длиной не более 13 символов, начинаю­
щаяся с позиции 1; экзаменационная оценка (в позиции 16). Между
последней литерой фамилии и оценкой расположены.пробельные
литеры.
lianncaTb фрагмент программы, который заполнит экзамена­
ционную ведомость ^'Math*^ данными, вводимыми из файла опера­
ционной системы "Task4.in" (ввод данных должен осуществляться в
текстовом режиме).

Вариант 6» В файле операционной системы ^^Test6.in" имеется


пять строк, каждая из которых содержит длины сторон прямоуголь­
ников (значения длин разделены двумя пробелами).
Написать: 1) определение массива структур для хранения ука­
занных длин сторон прямоугольников, их площадей и периметров;
2) фрагмент программы для чтения длин сторон прямоугольников из
файла операционной системы "Test6.in"; 3) фрагмент программы,
вычисляющий и печатающий площади и периметры прямоугольни­
ков в файл операционной системы "Test6.ouf\

Вариант 7. Имеется следующий фрагмент программы:


sbract ExamReport // Строка экз. ведомости
{
// Фамилия студента
char Name [ 15 ];
unsigned Markl; // Экзаменационная оценка 1
unsigned. Mark2; // Экзаменационная оценка 2
} Ехат[ 16 ] ;

В каждой строке файла "Task4.in" содержатся следующие поля


данных: фамилия студента длиной не более 13 символов, начинаю­
щаяся с позиции 1; экзаменационная оценка (в позиции 16); пробел
(в позиции 17); экзаменационная оценка (в позиции 18). Между по­
следней литерой фамилии и первой оценкой расположены пробель­
ные литеры.
Написать фрагмент программы, который заполнит экзамена­
ционную ведомость "£xa/?z" данными, вводимыми из файла опера­
ционной системы "Task4,in" (ввод данных должен осуществляться в
текстовом режиме).

Вариант 8. Имеется следующий фрагмент программы:


struct EXAM_REPORT // Строка экзаменационной ведомости
{

353
chajT fam[ 21 ];// Фамилия экзаменуемого
int mark; // Экзаменационная оценка
} math[ 16 ];
// Абсолютная успеваемость (процент студентов с
// положительными оценками)
£1оа.Ь аи;
// Качественная успеваемость ( процент студентов, получивших
// "4" и "5" )
float ки;

В каждой строке этого файла содержится фамилия студента


длиной не более 19 символов, начинающаяся с позиции 1, и экзаме­
национная оценка (поз. 22). Между фамилией и оценкой расположе­
ны "пробелы".
Написать: 1) фрагмент программы для чтения экзаменацион­
ной ведомости из текстового файла "/./«"; 2) фрагмент программы,
вычисляющей и печатающей в файл "/.ow/" абсолютную и качест­
венную успеваемость группы по математике.

Вариант 9, В файле операционной системы ''Task4,in'' хранит­


ся в текстовой форме ведомость со сведениями о продуктах. Каждая
строка этого файла содержит сведения об одном виде продукта,
представленные в следующем формате: позиции 1...2 - порядковый
номер продукта; позиция 3 - пробельная литера; позиции 4... 15 — на­
звание продукта длиной не более 11 символов, в произвольном мес­
те поля; позиция 16 - пробельная литера; позиции 17... 19 - содержа­
ние белка в 100 граммах продукта (целое).
Количество продуктов в ведомости равно 16. Пример строк
указанного файла:
01 минтай 20
02 щука 21

16 сметана 15

Написать: 1) определение массива структур для хранения ука­


занной ведомости и фрагмент программы, который заполнит ведо­
мость данными, вводимыми из файла операционной системы
"Task4.in'' (ввод данных должен осуществляться в текстовом режи­
ме); 2) фрагмент программы для нахождения и печати (в файл
"Task4.out") информацию о продукте с наибольшем содержанием
белка.

Вариант 10, В текстовом файле ''Task4.in'' содержится список


книг библиотеки, имеющий следующий вид:
01 Иванов Программирование 20,500

354
15 Петров Архиваторы 7.200

Каждая строка списка содержит сведения об одной книге: пер­


вые две позиции - порядковый номер книги, третья позиция - "про­
бел", с поз. 4 начинается фамилия автора длиной не более 13 симво­
лов, поз. 18...34 - название книги (из одного слова), поз. 35 - "про­
бел", с поз. 36 - стоимость книги.
Написать: 1) определение массива структур для хранения ука­
занного списка и фрагмент программы для чтения списка из файла
"Task4Jn''; 2) фрагмент программы, вычисляющей и печатающей
среднюю стоимость книг в библиотеке в файл ''Task4.ouf\

Вариант 11, В текстовом файле "Task4.in" содержится ин­


формация о квартире, имеющая следующий вид:
01 Комната 15

05 Кухня 5

Каждая строка содержит сведения об одной комнате: первые


две позиции - порядковый номер комнаты, третья позиция - "про­
бел", с поз. 4 начинается название комнаты длиной не более 15 сим­
волов, с поз. 21 - метраж комнаты.
Написать: 1) определение массива структур для хранения ука­
занных данных и фрагмент программы для чтения данных о квар­
тире из файла "Task4.m"; 2) фрагмент программы для нахождения и
печати общего метража данной квартиры в файл "Task4.out".

Вариант 12. В текстовом файле "Task4.i?7" содержится ведо­


мость сдачи экзаменов студентами некоторой группы, имеющая сле­
дующий вид:
01 Андреев 5 4 5

16 Петров 4 5 4

Каждая строка ведомости содержит сведения об одном студен­


те: первые две позиции - порядковый номер студента, третья пози­
ция - "пробел", с поз. 4 начинается фамилия студента длиной не бо­
лее 11 символов, поз. 16...20 — оценки по трем предметам. Каждой
оценке предшествует пробел, а первой оценке может предшество­
вать и большее число "пробелов".
Написать: 1) определение массива структур для хранения ука­
занной ведомости и фрагмент программы для чтения ведомости из
файла ''Task4.in''\ 2) фрагмент программы для нахождения и печати

355
списка должников (студентов, имеющих хотя бы одну двойку ) в
файл "Task4.ouf\

Вариант 13. В текстовом файле "Task4.in" содержится ведо­


мость сдачи экзаменов студентами некоторой группы, имеющая сле­
дующий вид:
01 Андреев 5 4 5
1 б Петров 4 5 4

Каждая строка ведомости содержит сведения об одном студен­


те: первые две позиции - порядковый номер студента, третья пози­
ция - "пробел", с поз. 4 начинается фамилия студента длиной не бо­
лее 11 символов, поз. 16...20 — оценки по трем предметам (матема­
тике, программированию и физике). Каждой оценке предшествует
пробел, а первой оценке может предшествовать и большее число
"пробелов".
Написать: 1) определение массива структур для хранения ука­
занной ведомости и фрагмент программы для чтения ведомости из
файла "Task4Jn"; 2) фрагмент программы для нахождения и печати
списка должников по программированию в файл "Task4.out".

Вариант 14. В текстовом файле "Task4.in" имеется ведомость


сдачи экзаменов студентами некоторой группы:
01 Андреев 5 4 5
1 б Петров 4 5 4

Каждая строка ведомости содержит сведения об одном студен­


те: первые две позиции - порядковый номер студента, третья пози­
ция - "пробел", с поз. 4 начинается фамилия студента длиной не бо­
лее 10 символов, поз. 16...20 — оценки по трем предметам (матема­
тике, программированию и физике). Каждой оценке предшествует
пробел, а первой оценке может предшествовать и большее число
"пробелов".
Написать: 1) определение массива структур для хранения ука­
занной ведомости и фрагмент программы для чтения ведомости из
файла "Task4Jn"; 2) фрагмент программы для нахождения и печати
списка студентов, сдавших физику на "отлично" в файл "Task4.out".

Вариант 15. В текстовом файле "Task4.in" содержатся сведе­


ния о предприятиях сферы обслуживания районов города, имеющие
следующий вид: ^

356
1. Калининский 10 20 7
10. Выборгский 15 10 9

Каждая строка содержит сведения об одном районе: первые


две или три позиции - номер района (с точкой), далее следует один
или два "пробела", поз. 5.. 19 — название района длиной не более 14
символов, далее следуют три целых числа, каждому из которых
предшествуют один или более пробелов. Первое число задает коли­
чество аптек, второе — универсамов, а третье - химчисток.
Написать: 1) определение массива структур для хранения ука­
занной информации и фрагмент программы для чтения данных из
файла "Task4.in''; 2) фрагмент программы для нахождения и печати
в файл "Task4.out" названия района (или районов ), в котором (в
которых) находится больше всего аптек.

Вариант 16, В текстовом файле "Task4.m" содержится ин­


формация о квартире, имеющая следующий вид:
01 Комната 15

05 Кухня 5

Каждая строка содержит сведения об одной комнате: первые


две позиции - порядковый номер комнаты, третья позиция - "про­
бел", с поз. 4 начинается название комнаты длиной не более 15 сим­
волов, с поз. 21 - метраж комнаты.
Написать: 1) определение массива структур для хранения ука­
занных данных и фрагмент программы для чтения данных о квар­
тире из файла "Та^-Ы.ш"; 2) фрагмент программы для нахождения и
печати в файл "Task4.ouf' метража самой большой по площади ком­
наты в квартире.

Вариант 17» Ъ текстовом файле ''Task4.in'' содержится список


книг библиотеки, имеющий следующий вид:
01 Иванов Программирование 20.500

15 Петров Архив а торы 7.200

Каждая строка списка содержит сведения об одной книге: пер­


вые две позиции - порядковый номер книги, третья позиция - "про­
бел", с поз. 4 начинается фамилия автора длиной не более 12 симво­
лов, поз. 18.,.34 - название книги (из одного слова), поз. 35 - "про­
бел", с поз. 36 - стоимость книги.
Написать: 1) определение массива структур для хранения ука­
занного списка и фрагмент программы для чтения списка из файла

357
''Task4An''\ 2) фрагмент программы, вычисляющей и печатающей
полные данные (номер, автор, название и цена) самой дорогой книги
в библиотеке в файл "Task4.ouf\

Вариант 18. В текстовом файле "Task4.m" содержится ведо­


мость сдачи экзаменов студентами некоторой группы, имеющая сле­
дующий вид:
01 Андреев 5 4 5

16 Петров 4 5 4

Каждая строка ведомости содержит сведения об одном студен­


те: первые две позиции - порядковый номер студента, третья пози­
ция - "пробел", с поз. 4 начинается фамилия студента длиной не бо­
лее 11 символов, поз. 16..20 — оценки по трем предметам. Каждой
оценке предшествует пробел, а первой оценке может предшество­
вать и большее число "пробелов".
Написать: 1) определение массива структур для хранения ука­
занной ведомости и фрагмент программы для чтения ведомости из
файла "Task4.in"; 2) фрагмент программы для нахождения и печати
списка студентов-тоечников (сдавших экзамены на одни тройки) в
файл ''Task4.ouf\

Вариант 19. В файле операционной системы " / ш " имеется 10


строк, каждая из которых содержит длины сторон прямоугольников
(значения длин задаются в формате с плавающей точкой и разделе­
ны пробелами).
Написать: 1) определение массива структур для хранения ука­
занных длин сторон прямоугольников, их площадей и периметров;
2) фрагмент программы для чтения длин сторон прямоугольников из
файла операционной системы " / ш " ; 3) фрагмент программы, вычис­
ляющий и печатающий длины сторон прямоугольников, имеющих
максимальные периметр и площадь в файл операционной системы
у.оиГ,

Вариант 20. В файле операционной системы "Task4.m" хра­


нится в текстовой форме ведомость со сведениями о продуктах. Ка­
ждая строка этого файла содержит сведения об одном виде продук­
та, представленные в следующем формате: позиции 1...2 - порядко­
вый номер продукта; позиция 3 - пробельная литера; позиции 4... 15
- название продукта длиной не более 11 символов, в произвольном
месте поля; позиция 16 - пробельная литера; позиции 17... 19 — со­
держание белка в 100 граммах продукта (целое); позиция 20 - про­
бельная литера; позиции 21...23 — калорийность 100 грамм продукта

358
(целое). Количество продуктов в ведомости равно 16. Пример строк
указанного файла:
01 минтай 20 100
02 щука 21 120

16 сметана 15 150

Написать: 1) определение массива структур для хранения ука­


занной ведомости и фрагмент программы, который заполнит экза­
менационную ведомость данными, вводимыми из файла операцион­
ной системы ''Task4.in^' (ввод данных должен осуществляться в
текстовом режиме); 2) фрагмент программы для нахождения и
печати (в файл ''Task4.ouf') названия продукта (продуктов) с
наибольшей калорийностью.

П.1.1.7. Функции. Варианты тестов

В ответах на приведенные ниже варианты тестов выполнить


следующее.
Для решения указанных в вариантах тестов задач написать
прототип, определение функции и пример ее вызова.
Для передачи в функцию исходных данных и получения из нее
ответов использовать список параметров. Бдинственный ответ луч>
ше получать из функции как возвращаемое значение.

Вариант 1, Вычислить тах:=наиб{а,^,с}. Исходные данные


имеют тип с плавающей точкой.

Вариант 2, В массиве целого типа определить количество по­


ложительных, отрицательных и нулевых элементов.

Вариант 3. Вычислить тах:=наиб{л,/)} и тш:=наим{а,6}.

Вариант 4. Подсчитать в одномерном массиве целого типа


размером 100 элементов наименьшее значение среди положитель­
ных элементов.

Вариант 5. Подсчитать в одномерном массиве целого типа


размером 100 элементов среднее арифметическое значение. Поста­
райтесь не потерять в ответе дробную часть.

Вариант 6. Подсчитать в одномерном массиве целого типа


размером 100 элементов индекс и значение последнего из положи­
тельных элементов.

359
Вариант 7. Подсчитать в одномерном массиве целого типа
размером 100 элементов количество нулевых значений.

Вариант 8, Сформировать одномерный массив с элементами


z[ i ] ( О <= i < N ) , N==20

ИЗ двух заданных массивов целого типа х[ i ], у[ i ] по правилу:


z[ 1 ] := mini^ к[ 1 ], у[ i ] } , i = О, 1, .,., N-1

Вариант 9. Вычислить сумму квадратов элементов двух од­


номерных массивов вещественного типа размером по 40 элементов
и получить ее из функции как возвращаемое значение
39
Сумма ( х[±] * x[i] -h у[1] ^ у[1] )
1=0

Вариант 10, Найти индекс максимального элемента в массиве


целого типа из 30 элеметов. Результат получить из функции как воз­
вращаемое значение.

Вариант 11, Написать функцию с двумя параметрами логиче­


ского типа, возвращающую значение в соответствии со следующей
таблицей истинности:

Параметры Возвраща емый


Первый Второй результат
false false false
false true false
true false true
true true false

Параметры и результат - целого типа: false соответствует ну­


левому и true - ненулевому значениям.

Вариант 12. Получить одномерный массив z из двух заданнкх


массивов вещественного типа х, у по правилу:
zfi] := ( x[i] -h y[i] ) / 2r ± = О, 1, ,.., 29

Вариант 13. Найти величину и номер первого отрицательного


и последнего положительного элементов в массиве вещественного
типа заданного размера.

360
Вариант 14. Поменять местами первый и последний элемент,
второй и предпоследний и т.д. в одномерном массиве вещественного
типа заданного размера.

Вариант 15. Вычислть среднее арифметическое для положи­


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

Вариант 16. Написать функцию нахождения минимального


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

Вариант 17. Написать функцию нахождения максимального


положительного числа кратного пяти в одномерном массиве целого
типа заданного размера.

Вариант 18. Найти количество нулевых элементов в одномер­


ном массиве целого типа заданного размера и сформировать новый
массив из ненулевых элементов исходного массива.

Вариант 19. В одномерном массиве вещественного типа за­


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

Вариант 20. Сжать одномерный массив вещественного типа


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

П.1.1.8. Области действия определений. Варианты тестов

В ответах на приведенные ниже варианты тестов укажите, как


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

Вариант 1. Что напечатает следующая программа?


^include <stdio,h>
±nt i == Or j = 2;
int main ( void )
{
auto int i = 0/

361
printf( "± = %d j^%d \ л " , i , j ) ;
{
±nt i =2, j ^ 0;
pr±ntf( "± = %d j = %d \n", 1, j ) ;
{
Izit j = 10; i += 1; j += 2;
printf( "i = %d j=%d \ л " , i , j ) ;
}
printf( "i = %d j=%d \ л " , i , j ) ;
}
printf( "i = %d j = %d \n", i, j ) ;
z-etuzrn 0;
} // end function "main"

Вариант 2, Что напечатает следующая программа?

^include <stdio.h>
±nt i = 10, j = 2;
±nt main ( void )
{
a u t o ±nt i == 8;
{
±nt j = 0; printfi "i = %d j = %d \ л " , i , j ) .
{
int j = 10; i += 1; j += 2;
printf( "i=%d j=%d \n", 1, j ) ;
}
j + + ; printfi "i = %d j=^%d \n", i, j ) ;
}
printfi "i = %d j = %d \ л " , i , j ) ;
return. 0;
}

Вариант 3. Что напечатает следующая программа?

^include <stdio.h>
Int i , j = 1;
±nt maini void )
{
±nt i = 5;
{
{
±nt j = 2; j += 3;
}
j += 5; printfi "i+l=%d j=%d \n", i+1, j ) ;
}
printfi "i = %d j = %d \n", i, j ) ;
return 0;
}

Вариант 4. Что напечатает следующая программа?

362
^include <std±o.h>
±пЬ 1 =^ 1, j = 10;
Inb main ( void. )
(
±nt i = 3;
{
printf( "i + l=%d j=%d \ л " , i+1, j ) ;
{
±nt j = 1; j += 3;
}
j -f- 5; printf( "l=%d j^%d \л", i, j ) ;
}
printfi "i = %d j-i-l=%d \ л " , i , j-hl ) ;
iretuxn 0;
}
Вариант 5. Что напечатает следующая программа?

^include <stdio.h>
int i = 1; j = 10;
±nb main ( void. )
{
int i = 3;
I
printf( "i+l^%d j = %d \n"r i+1, j ) ;
(
int j = 1; j +=^ 3;
}
j += 5; printf( "i=%d j=%d \n", i, j );
}
printf( "i = %d j + l==%d \n'\ i , j+1 ) ;
геЬ\12ПЛ 0;
}

Вариант 6. Что напечатает следующая программа?

^include <stdio.h>
int i, j ;
int main ( void )
{
auto int i = 3;
{
printf( "i + l=%d j=%d \ л " , i + 1, j ) ;
{
auto int j = 1; j += 3;
printf( "i = %d j = %d \n", i/j ) ;
}
j += 5; printf( "i = %d j = %d \n", i, j ) ;
}
printf( "i = %d j + l = %d \n", i , j+1 ) ;
return 0;
}

363
Вариант 7. Что напечатает следующая программа?
^include <stdlo.h>
±nt 1 = 10^ j /
int main ( void, )
{
static int i = 3/
{
printf( '4 = %d j = %d \n", i, j ) ;
{
a u t o int j = 10; i += 1; j += 2;
prlntf( '4 = %d j = %d \n", 1, j ) ;
}
j += 5; prlntf( "l = %d j = %d \n", 1, j ) ;
}
prlntfi "l = %d j = %d \n", i , j+1 ) ;
return 0;
}
Вариант 8. Что напечатает следующая программа?

^Include <stdlo,h>
int 1=10, j =2;
int main ( void )
{
auto int 1=8;
{
int j = 0; prlntf( "l=%d j=%d \n'\ i, j )
{
int j = 10; 1 += 1; j += 2;
prlntf( "l = %d j = %d \n", 1, j ) ;
}
j++; prlntfi "l = %d j = %d \л", i, j ) ;
}
prlntf( '4 = %d j = %d \л", i, j ) ;
return 0;

Вариант 9. Что напечатает следующая программа?

^Include <stdlo.h>
// Прототипы функций
int next ( int ) ; int reset ( void ) ;
int last ( int ) ; int naw ( int ) ;
int 1=1;
int main ( void )
{
auto int 1, j ; 1 = reset( ) ;
fori j = 1; 3 <= 2; j++ )
{
prlntf( "\nl = %d j = %d\n", 1, j ) ;
prlntf( "next( 1 ) = %d\n", next( 1 ) ) ;

364
printf( "last( i ) - %d\n", last ( i ) ) ;
prlntf ( "naw( 1+j ) = %d\n", naw ( 1-hj ) ) ;
}
retuxrn 0;

±nt reset ( void. )

return 1/

±nt next ( xnt j )

return ( j = i ++ ) ;

int last ( int j )

static int 1 =^ 10; return ( j = l-~ ) ;

int naw ( int i ;

auto int j = 10; return( i = j += 1 ) ;

Вариант 10, Что напечатает следующая программа?


^include <stdio.h>
// Прототипы функций
int next ( int ) ; int reset ( void ) ;
int last ( int ) ; int naw( int ) ;
int 1=1;
int main ( void )
{
auto int Ir j ; 1 = reset ( ) ;
fori j = 1; j <= 2; j++ )
{
prlntf( "\nl = %d j = %d\n", 1, j ) ;
prlntf ( " n e x t Г i ; = %d\n", next ( 1 ) ) ;
prlntf ( "last( 1 ) = %d\n'\ last( 1 ) ) ;
prlntf ( "naw( 1+j ) - %d\n", naw( 1+j ) ) ;
}
retujzn 0;

int reset ( void )

return 1;

int next ( int j )

return ( j = 1-- ) ;

int last ( int j )

static int 1 = 10; return( j = 1++ ) ,

365
±nt naw ( ±пЬ i )
{
auto ±nb j - 10; return( 1 - j ~= i ) ;
}

Вариант 11. Что напечатает следующая программа?


^include <stdio.h>
// Прототипы функций
±nt next ( ±zit ) ; int reset ( -void ) ;
±nt last ( ±nt ) / i^t naw( ±nt ) ;
Int i = 1;
±nt main ( void )
{
auto ±nt i, j ; i = reset( ) ;
fori J, = I; J <= 2; j+-h )
{
prlntf( "\ni = %d j = %d\n", i , j );
print f( "next( i ; = %d\n"^ next ( 1 ) ) ;
printf( "last( 1 ) = %d\n", last ( 1 ) ) ;
printf( "naw( i+j ) --= %d\n", naw ( i+j ) ) ;
)
return 0;
int reset( void )

return 1;

±nt next ( int j )

return( j = --1 ) ;

int last ( int j )

static int i =10; return ( j = +4-1 ) ;

int naw( int i )

auto int j =10; return ( i = j -= i + + ) ;

Вариант 12. Что напечатает следующая программа?

^include <stdlo.h>
// Прототипы функций
int next ( int ) ; int reset ( void ) ;
int last ( int ) ; int naw( int ) ;
int 1=3;
int main ( void )
{
auto int i, j ; 1 = reset( ) ;
for( j = 4; j <= 5; j++ )

366
{
print f ( "\nl = %d j = %d\n", i , J ) /
print f( "next ( 1 ) = %d\n"^ next ( 1 ) ) ;
prlntf( "last( i ) = %d\n"r last ( 1 ) ) ;
print f ( "naw( 1+j ) = %d\n" r naw ( 1+j ) ) /
}
jcetum 0;

int reset( void )

jretujrn i /

i n t next ( ±nt j )

jcGtuim ( j = - - i ) ;

i n t last ( int j )

static int 1; jret-ami j = i-f-/- ) ;

i n t naw ( int 1 )

a u t o int j = 5; retuim ( 1 = j += 1++ ) /

Вариант 13, Что напечатает следующая программа?


^Include <stdlo.h>
// Up ототипы функций
int next ( int ) ; int reset ( void ) ;
int last ( int ) ; int naw( int ) ;
int 1 = 2;
int main ( void )
{
auto int i, j ; 1 = reset ( ) ;
for( j = 0; j <= 1; j-h-h )
{
prlntf( "\nl = %d j = %d\n"r ir j ) ;
print f( "next ( 1 ) = %d\n"r next ( 1 ) ) ;
prlntf( "last( 1 ) = %d\n", last( 1 ) ) ;
prlntf ( "naw( 1+j ) = %d\n", naw ( 1+j ) ) ;
}
return 0;
}
int reset( void )
{
return( 1 + 1 ) ;
}
int next ( int j )
{
return ( j = 1++ ) ;
}
int last ( int j )
367
{
static int i = 4; return ( j = i ++ ) ;
}
±Tit naw ( ±nt 1 )
{
auto int j = 3; return ( 1 = j += i ) ;
}

Вариант 14. Что напечатает следующая программа?


^include <stdio.h>
// Прототипы функций
int next ( int ) ; void reset( void );
int last ( int ) ; int naw( int ) ;
int i;
int main ( void )
{
auto int j ; reset( ) ;
for( j = 2; j <- 3; j-h+ )
{
printfi "\ni = %d j = %d\n", i , j ) ;
print f( "next( i ) = %d\n", next ( i ) ) ;
printf( "\ast( i ) = %d\n", last ( i ) ) ;
printfi "naw( i-hj ) - %d\n", naw( i-hj ) ) ;
}
return 0;
}
void reset ( void )
{
i = 5/ return;
}
int next ( int j )
{
return ( j = i -h j ) ;
}
int last ( int j )
{
sta-tic int i = 2; return ( j += i + + ) ;
}
int naw( int i )
{
auto int j = 1; return ( i = j+i- ) ;
}

Вариант 15. Что напечатает следующая программа?

^include <stdio.h>
// Upототипы функций
int next ( int ) ; int reset ( void ) ;
int last ( int ) ; int naw( int ) ;
int i = 6;
int main ( void )
{

368
a u t o ±nt j , i; i = reset ( ) ;
£or( j = 2; j <= 3; j++ )
{
printfC "\ni = %d j = %d\n", i , j ) ;
print f( "next( i ) = %d\n", next ( ± ) ) ;
printf( "last( i ; = %d\n"r last ( i ) ) ;
printf( "naw( i+j ) = %d\n", naw( i+j ) ) ;
}
x-etux-ii 0;
}
±nt reset( void )
I
Int 1=2; зо&Ьихпл i-h-h;
}
int next ( int j )
{
return ( j = ~-i ) ;
}
int last ( int j )
{
static int i = 2; return ( j =- i + + ) ;
}
int naw( int i )
{
auto int j = 7; return( i = j -= i ) ;
}
Вариант 16, Что напечатает следующая программа?
^include <stdio,h>
// Прототипы функций
int next ( int ) ; int reset ( int ) ;
int last ( int ) / int naw( int ) ;
int 1=4;
int main ( void )
{
auto int jr 1 = 1; 1 = reset ( 1%4 ) ;
£or( j = 1; j < 3 ; j++ )
{
printfi "\ni = %d j = %d\n", i , j ) ;
print f( "next( i ) = %d\n", next ( i ) ) ;
printfC "last( 1 ) = %d\n"r last ( i ) ) ;
printf( "naw( i-hj ) = %d\n", naw( i+j ) ) ;
}
return 0;
}
int reset ( int i )
{
return i;
)
int next ( int j )
{
return( j = ++i ) ;

369
;
±nt last ( int j )
{
st&tic int i = 6; jretixm {" j = - - i ) ;
}
int naw( int 1 )
{
auto int j = 3; JoetvLTni i = j -= 1 ) ;
}

Вариант 17. Что напечатает следующая программа?

^include <stdlo.h>
// Прототипы функций
int next ( int ) ; int reset ( void. ) ;
int last ( int ) ; int naw( int ) ;
int 1 = 4/
int main ( void )
{
auto int J, 1/ 1 = reset( ) ;
£or( j = 2; j < 4 ; j++ )
{
prlntf( "\nl = %d j = %d\n", reset ( ) , j ) ;
print f( "next( 1 ) = %d\n", next ( 1 ) ) ;
prlntf( "last( 1 ) = %d\n", last( 1 ) ) ;
prlntfi "naw( 1+j ) = %d\n", naw( 1+j ) ) ;
}
return 0;

int reset ( void )

return 1++;

int next ( int j )

return ( j = i-- ) ;

int last ( int j )

static int 1 = 5; return( j = 1++ ) ;

int naw( int 1 )

auto int j = 4; return( 1 = j += 1 ) ;

Вариант 18. Что напечатает следующая программа?

^Include <stdlo.h>
// Прототипы функций
int next ( int ) ; int reset ( void ) ;
int last ( int ) ; int naw( int ) ;
int 1 = 10;

370
int main ( void )
{
auto int jr i/ i = reset( ) ;
fori j = 2; j < 4; j+ч- )
{
printf( "\ni = %d j = %d\n"r reset ( ), j ) ;
{
static int i = 7/ int j = 10;
prlntf( "\ni = %d j = %d\n", i-h+, j ) ;
}
print f ( "next ( i ) = %d\n", next ( i ) ) ;
printf( "last( i ) = %d\n", last ( i ) ) ;
printfC "naw( i-f-j ) = %d\n", naw( i+j ) ) ;
}
return 0;
int reset ( void )

return( i + 5 ) ;

int next ( int j )

return( j = i~- ) ;

int last ( int j )

static int 1=^1; return ( j = i-h+ ) /

int naw( int i )

auto int j = 3; return( i = j -= i ) ;

Вариант 19. Что напечатает следующая программа?


^include <stdio.h>
// Пр ото типы функций
int next ( int ) ; int reset ( void ) ;
int last ( int ) ; int naw( int ) ;
int i = 10;
int main ( void )
{
auto int j , i; i = reset ( ) ;
for( 1 = 2; j < 4 ; j-h+ )
{
print f ( "\ni = %d j = % d \ n " , r e s e t f ^, j ) ;
printf( "next ( i ) = %d\n", next ( i ) ) ;
printf( "last( i ) = %d\n", last ( i ) ) ;
printf( "naw( i+j ) = %d\n"r naw( i+j ) ) ;
}
return 0;
}
int reset ( void )

371
return( i + 5 ) ;

±nt next ( ±nt j )

return ( j = i-- ) /

±nt last ( ±nt j )

static ±nt i = 1; return ( j = i-h+ ) ;

int naw ( int 1 )

auto int j = 3; return ( 1 = j -= 1 ) ;

Вариант 20. Что напечатает следующая программа?


^Include <stdio.h>
// Прото типы функций
int next ( int ) ; int reset ( int ) ;
int last ( int ) ; int navj ( int ) ;
int 1=3;
int main ( void )
{
auto int jr 1 = 5; 1 = reset ( 1/2 ) ;
£or( j = 6; j < 8 ; j++ )
{
prlntf( "\nl = %d j = %d\n'\ 1, j ) ;
prlntf ( "next ( 1 ) = %d\n" r next ( 1 ) ) ;
prlntf( "last( 1 ) = %d\n", last( 1 ) ) ;
prlntf ( "naw( 1+j ) = %d\n'\ naw( 1+j ) ) /
I
return 0;

int reset( int 1 )

retuim. 1;

int next ( int j )

return( j = -~1 ) ;

int last ( int j )

static int 1 = 4; retum( j = l + -h ) ;

int naw ( int 1 )

auto int j = 4; return ( 1 = j ~= 1 ) ;

372
п.1.1.9. Массивы и указатели. Варианты тестов

В ответах на приведенные ниже варианты тестов укажите, как


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

Вариант 7. Что напечатает следующая программа?


^include <stdlo.h>
±nt Array[ ] = { 0 , 4 , 5 , 2 , 3 } ;
±nt main ( void )
{
int Index^ ^Pointer;
£or( Index = 0/ Index <= 4; Index+=2 )
pr±ntf( " %3d"r "^ (Аггауч-Index—; ) /
printf ( "\n" ) ;
Pointer = Array + 1;
£or( Index = 0; Index <= 2; )
printf ( " %3d" r Pointer [ -h + Index ] ) ;
printf ( "\n" ) ;
x-etuxn 0;
}

Вариант 2. Что напечатает следующая программа?


^include <stdio.h>
int Array[ ] = { 1, 2, -7, 4, 3 };
int main ( void )
(
int Index, ^Pointer;
fori Index = 0; Index <= 4; Index+=2 )
{
printf( " %3d". Array[ Index] ) ;
printf ( "\n" ) ;
}
Pointer = Array+1;
fojci Index = 0; Index <= 2; ++Index )
{
printf( " %3d". Pointer[ ++Index ] ) ;
printf ( "\л" ; /
}
jretuzm 0;
}

Вариант 3. Что напечатает следующая программа?


^include <stdio.h>
int Array[ ] = { 1 , 4 , 7 , 2 , 3 } ;
int main ( void )
{

373
Int Index ^ "^Pointer;
£or( Index = 1; Index <= 4; Index+=1 )
{
pr±ntf( " %3d"r Array[ +4-IndexJ );
printf( "\n" ) ;
}
Pointer = Array;
£ox: ( Index = 0; Index <= 2; + + Index )
{
printf( " %3d'\ Pointer[ Index+-h ] );
printf( "\n" ) ;
)
return 0;

Вариант 4. Что напечатает следующая программа?


^include <stdio,h>
int Array[ ] = { Ir 4r 5, 12, 3 } ;
±nt main ( void )
{
int Index, ^Pointer;
fox:( Index = 1; Index <= 4; Index-h=l )
{
printfi " %3d"r * (Array-hlndex-h-h) );
printfC "\n" ) ;
}
Pointer = Array + 1;
for ( Index = 0; Index <= 2; + + Index )
printfi " %3d"r Pointer[ ++Index ] );
printf( "\n" ) ;
retxirn 0;
}

Вариант 5. Что напечатает следующая программа?


^include <stdio.h>
int Array[ ] = { 0 , 4 , 5 , 2 , 3 } ;
int main ( void )
{
int Index, "^Pointer;
for( Index = 0; Index <= 4; Index+=2 )
printf( " %3d", * (Array-hlndex++) );
printf( "\n" ) ;
Pointer = Array + 1;

for( Index = 0; Index <= 3; Index++ )


printfi " %3d". Pointer[ ++Index ] );
printfi "\n" ) ;
return 0;

374
Вариант 6. Что напечатает следующая программа?
^Include <std±o.h>
±nt Array[ ] = { 0 г 4 г 5 г 2 , 3 } ;
±пЬ main ( void )
{
±nt Index, ^Pointer;
£ог( Index = 0; Index <= 4; Index-i-=2 )
printf( " %3d"r * (Array+Index-h+) );
print f ( "\n" ) ;
Pointer = Array + 1;
£ою( Index = 0; Index <= 3; Index + + )
printf( " %3d"r Pointer[ ч-ч-Index ] ) ;
printf ( "\n" ) ;
ire turn 0;
}

Вариант 7. Что напечатает следующая программа?


^include <stdio.h>
±nt Array[ ] = { 0 , 4 r 5 r 2 , 3 ) ;
int main ( void )
{
Int Index, * Pointer; ,
for( Index = 0; Index <= 2; Index-h=l )
printf( " %3d", *(Array+Index++) );
printf ( "\л" ) ;
Pointer = Array;
£OJ: ( Index = 0; Index <= 3; Index++ )
printf( " %3d". Pointer[ -h+Index ] );
printf ( "\л" ; ;
return 0;
}

Вариант 8, Что напечатает следующая программа?


^include <stdio.h>
Int Array[ ] = { 0 , 4 , 5 , 2 r 3 } ;
±nt main ( void )
{
Int Index, ^Pointer;
for( Index = 0; Index <= 2; Index-h=2 )
printf ( " %3d", * (Array+Index-h+) );
printf ( "\n" ) ;
Pointer - Array;
£or( Index = 1; Index <= 2; Index+ч- )
printf( " %3d". Pointer[ ++Index ] );
printf ( "\n" ); jreturn 0;
}

Вариант 9, Что напечатает следующая программа?

375
^include <stdio.h>
Int Array[ J = { 0 , 4 , 5 , 2 , 3 } /
±nt main ( void, )
{
±nt Index, '^'Pointer;
£or( Index = 0; Index <= 2; Index+=2 )
prlntf( " %3d"r * (Array-fIndex) );
printf( "\n" ) ;
Pointer = Array;
for( Index = 1; Index <= 2; Index++ )
printf( " %3d"r Pointer[ Index ] );
printf( "\n" ) ;
iretuim 0/
}

Вариант 10. Что напечатает следующая программа?


^include <stdio,h>
±nt main( void )
{
±nt a[ ] = { 10, 11, 12, 13, 14, 15, 16 }, i, *p,
for( p = a, i = 0;p + 2*i <= a -h 6; p++, i + + )
printfi " %3d", *( p + 2*i ) );
printf( "\n" ) ;
fori p = a + 5; p >== a + 1; p -= 2 )
printf ( " %3d", *p ) ;
printf( "\n" ) /
retuzm 0;
}

Вариант 11. Что напечатает следующая программа?


^include <stdio.h>
±nt main ( void. )
{
±nt a[ ] = { 10, 11, 12, 13, 14, 15, 16 }, i, *p;
for( p = a, i = 0; ++p + i <= a + 5; p++, i-h+ )
printf ( " %3d", *( -h+p + i ) );
printf ( "\n" ) ;
fori p = a + 5/ p >= a + 1; p -= 2 )
printfi " %3d", *p++ );
printfi "\n" ) ;
return 0;
}

Вариант 12. Что напечатает следуьрщая программа?


^include <stdio.h>
±nt main i void )
{
int a[ ] = { 10, 11, 12, 13, 14, 15 }, i, *p;

376
£ою ( р = а, ± = О; р + i <= а + 5; р+-/-, ±++ )
pr±ntf( " %3d", *( ++Р + i ) ) ;
printf( "\n" ) ;
£o:c ( p ^ a + 5; p >= a + 1; p-- )
printf( " %3d", *p— ) ;
printf( "\n" ) ;
retvLirn 0;

Вариант 13. Что напечатает следующая программа?

^include <stdio.h>
x n t main ( void. )
{
±nt a[ ] = { 10, 11, 12, 13, 14, 15 } , i , *p;
for( p = a+2, i = 0; p + i <= a + 5; p+ + , i + + )
print f( " %3d", * ( p -h 1 ) ) /
printf ( "\n" ) ;
fox:( p==a + 5;p>=a + l; p— ;
printf ( " %3d", *—p ) ;
printf ( "\n" ) ;
return 0;
I

Вариант 14. Что напечатает следующая программа?

^include <stdio,h>
±пЬ main ( void. )
{
Int a[ ] = { 10, 11, 12, 13, 14, 15 } , i , *p;
fori p == a, i = 0 ; p - h i < = a - h 5 - i ; p+ + , i+-h )
printf ( " %3d", *( p + i + + ) ) ;
printf ( "\n" ) ;
£or ( p = a + 5; p >= a ; p~- )
printf( " %3d", *p— ; /
printf ( "\n" ) ;
jretujm 0;
}

Вариант 15. Что напечатает следующая программа?

^include <stdio.h>
Int main ( void )
{
int a[ ] ^ { 15, 11, 10, 13, 14, 10 } , i , *p;
£or ( p = a, i = 0;p-hi<=a + 5 ~ i ; p+ +, i + + )
printf ( " %3d", p[ i ] ) ;
printf ( "\n" ) ;
tor( p = a + 5; p >= a ; p -= 2 )
printf( " %3d", *p ) ;
printf ( "\n" ) ;
return 0;

Ъ11
Вариант 16, Что напечатает следующая программа?
^include <stdio.h>
±nt main ( void )
{
±nt a[ 3 J[ 3 J ^ { { 1, 2, 3 } ,
{ 4, 5, 6 Ь
( 7, 8, 9 } };
±nt *pa[ 3 ] = { a[ 1 J, a[ 2 ], a[ 1 ] } /
for( int 1 = 0; 1 < 3 ; i++ )
printf( "%d %d %d\n", a[ 1 ][ 2-i J,
*(*(ач-±) + ! ) , * ( pa [ 1 ] ) ) ;
jretujrn 0;
}

Вариант 17. Что напечатает следующая программа?

^Include <stdio.h>
±пЬ main( void )
{
±nt a[ 3 ][ 3 ] = { { 1, 2, 3 } ,
{ 4, 5, 6 Ь
/ 7 Й Я ) }'
int *pa[ 3 ] = { a[ 2 ]\ a] 0 ], a[ 2 ] } ;
for( int i = 0; i < 2 ; i++ )
printf( "%d %d %d\n", a[ i ][ 2-i ],
*(*(a + i ) + i ) , * ( p a [ i ] ) ) ;
return 0;

Вариант 18, Что напечатает следующая программа?

^include <stdio.h>
int main ( void )
{
int a[ 3 ][ 3 ] = { { 1, 2, 3 } ,
{ 4 , 5 , 6 Ь
{ 7, 8, 9 } } ;
int *pa[ 3 ] = { a[ 2 ], a[ 0 ], a[ 1 ] } ;
for( int i = 0; i < 2 ; i++ )
printf( "%d %d %d\n", a[ i ][ 2-i 7 ,
*(*(a + i ) + i ) , ^ ( p a [ i ] ) ) ;
return 0;
}

Вариант 19, Что напечатает следующая программа?

^include <stdio.h>
int main ( void )
{

378
±nt a[ 3 ] [ 3 ] == { { Ir 2, 3 } г
{ 4, 5, 6 Ь
{ 7, 8, 9 } } ;
±nt *pa[ 3 ] - { a[ 2 ], a[ 0 ], a[ 1 ] } ;
fo2:( int i = 0/ i < 2 ; i++ )
printf( "%d %d %d\n"r a[ 1 ][ 2-i 7 ,
*(*(a + i ) + l ) , ^ ( p a [ l ] ) ) /
return 0;

Вариант 20. Что напечатает следующая программа?


^Include <std±o.h>
int main ( void, )
{
±nt a[ 3 ] [ 3 ] -' { { 1, 2, 3 } ,
( 4, 5, 6 ; ,
{ 7, 8, 9 } } ;
int *pa[3] = { a [ 0 ] , a [ l ] , a [ 2 ] } .
£or( int i = 2; i > 0 ; i— ;
printf( "%d %d %d\n'\ a[ i ][ 2~i ],
*(*(a + i ) + i ) , * ( p a [ i ] ) ) ;
z-etuxn 0;

П. 1.1.10. Операции над линейным списком.


Работа с динамической памятью. Варианты тестов

Вариант / . Определен следующий структурный тип:


s t r u c t Node // NODE: узел линейного списка
{
Node *рЫпк/ // Pointer LINK:
// указатель на очередной узел
floatt Info; // INFOrmation: информация
} ;

В текстовом файле операционной системы ''TestSAn'' содер­


жится некоторое количество вещественных чисел, разделенных
символами пробельной группы ( ' ', '\/', '\«' ).
Написать прототип, определение и пример вызова функции, кото­
рая должна ввести из файла ^'TestS.in" содержащиеся в нем веществен­
ные числа и запомнить их в узлах линейного списка, в котором каждый
узел (динамически размещенная в памяти структура) имеет тип Node.
При этом первое прочитанное число должно находиться в последнем от
начала узле линейного списка, второе число - в предпоследнем узле и
т.д.
Все исходные данные (указатель на **имя. расширение**
файла ввода) и все результаты работы функции (указатель на
начало линейного списка) должны передаваться через список

379
параметров. С целью обработки ошибок предусмотреть кон­
троль значений, возвращаемых функциями библиотеки Си
^^fopen^\ ^^fscanf^ и операцией new. Подключить необходимые
стандартные заголовочные файлы.

Вариант 2, Определен следующий указатель на начало ли­


нейного списка:

stJTuct Node // NODE: узел линейного списка


{
Node *рЫпк; // Pointer LINK:
// указатель на очередной узел
dovible In fo ; // INFOrmat ion: инф ормация
} *start;

Написать прототип, определение и пример вызова функции,


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

Вариант 3. Определен следующий указатель на начало ли­


нейного списка:
stxnict Node // NODE: узел линейного списка
{
Node *pL±nk; // Pointer LINK:
// указатель на очередной узел
float In fo; // INFOrma tion: информа ция
} *start/

Написать прототип, определение и пример вызова функции,


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

Вариант 4. Определен следующий /-^азатель на начало ли­


нейного списка:

380
struct Node // NODE: узел линейного списка
{
Node *pLlnk; // Pointer LINK:
// указатель на очередной узел
±nt Info; // INFOrmation: информация
} *start;

Написать прототип, определение и пример вызова функции,


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

Вариант 5. Определен следующий указатель на начало ли­


нейного списка:
struct Node // NODE: узел линейного списка
{
Node *pLink; // Pointer LINK:
// указатель на очередной узел
float Info; // INFOrmation: информация
} *start;

Написать прототип, определение и пример вызова функции,


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

Вариант 6. Определен следующий структурный тип:


struct Node // NODE:
{ // узел линейного списка
Node ^pLink; // Pointer LINK: указатель на
// очередной узел списка
in t Info; // INFOrm at ion:
// содержательная информация
} ;

В текстовом файле операционной системы "TestS.in" содер-

381
жится некоторое количество целых чисел, разделенных символами
пробельной группы ( ' ', V , '\«' ).
Написать прототип, определение и пример вызова функции,
которая должна BBCCTJI ИЗ файла ''TestS.in'' содержащиеся в нём це­
лые числа и запомнить их в узлах линейного списка, в котором каж­
дый узел (динамически размещенная в памяти структура) имеет тип
Node. При этом первое прочитанное число должно находиться в
первом от начала узле линейного списка, второе число - во втором
узле и т.д.
Все исходные данные (указатель на "имя. расширение**
файла ввода) и все результаты работы функции (указатель на
начало линейного списка) должны передаваться через список
параметров. С целью обработки ошибок предусмотреть кон­
троль значений, возвращаемых функциями библиотеки Си
^^fopen^\ ^^fscanf^ и операцией new. Подключить необходимые
стандартные заголовочные файлы.

Вариант 7. Определен следующий указатель на начало ли­


нейного списка:
зЬгасЬ Node // NODE: узел линейного списка
{
Node *рЫпк; // Pointer LINK:
// указатель на очередной узел
float Info; // INFOrmation: информация
} *start;

Написать прототип, определение и пример вызова функции,


которая в процессе просмотра списка выводит данные (числа) в
файл на магнитном диске ^'f.ouV\ не разрушая информацию в линей­
ном списке. В частном случае, перед вызовом этой функции линей­
ный список может быть пуст.
Все исходные данные (указатель на начало линейного спи­
ска, указатель на **имя. расширение** файла вывода) должны пе­
редаваться через список параметров. С целью обработки ошибок
предусмотреть контроль значения, возвращаемого функцией
библиотеки Си ^^fopen^\ Подключить необходимые стандартные
заголовочные файлы.

Вариант 8. Определен следующий указатель на начало ли­


нейного списка:
struct Node // NODE: узел линейного списка
{
Node *рЫпк; // Pointer LINK:
// указатель на очередной узел

382
float Info; // INFOrm at ion: информа ция
I *start;

Написать прототип, определение и пример вызова функции,


которая в процессе просмотра списка выводит данные (числа) в
файл на магнитном диске "f.ouf\ одновременно освобождая память,
занятую линейным списком. В частном случае, перед вызовом этой
функции линейный список может быть пуст.
Все исходные данные (указатель на начало линейного спи­
ска, указатель на **имя. расширение** файла вывода) и результа­
ты работы функции (указатель на начало линейного списка)
должны передаваться через список параметров. С целью обра­
ботки ошибок предусмотреть контроль значения, возвращаемо­
го функцией библиотеки Си ^^fopen^\ Подключить необходимые
стандартные заголовочные файлы.

Вариант 9. Определен следующий указатель на начало ли­


нейного списка:

stxnjct Node // NODE: узел линейного списка


{
Node *рЫпк; // Pointer LINK:
// указатель на очередной узел
float Info; // INFOrm ation : информа ция
} *start;

Написать прототип, определение и пример вызова функции


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

Вариант 10. Определен следующий указатель на начало ли­


нейного списка:
stjract Node // NODE: узел линейного списка
(
Node *рЫпк; // Pointer LINK:
// указатель на очередной узел
float Info; // INFOrm ation: информа ция
*start;

Написать прототип, определение и пример вызова функции

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

Вариант 11, Определен следующий указатель на начало ли­


нейного списка:
stmict Node // NODE: узел линейного списка
{
Node *pLink/ // Pointer LINK:
// указатель на очередной узел
±nt In fo ; // INFOrm ati on : ин форма ция
} *start;

Написать прототип, определение и пример вызова функции


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

Вариант 12. Определен следующий указатель на начало ли­


нейного списка:

stmjct Node // NODE: узел линейного списка


{
Node *рЫпк; // Pointer LINK:
// указатель на очередной узел
±nt In fo ; // INFOrm at Ion: информа ция
} *start;

Написать прототип, определение и пример вызова функции


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

384
Вариант 13. Определен следующий указатель на начало ли­
нейного списка:
stxract Node // NODE: узел линейного списка
{
Node *pLink; // Pointer LINK:
// указатель на очередной узел
±nt Info; // INFOrmation: информация
} *start;

Написать прототип, определение и пример вызова функции


для удаления из линейного списка элемента, предшествующего каж­
дому элементу, в котором хранится значение y?«(i. В частном случае,
перед вызовом этой функции линейный список может быть пуст или
может содержать любое количество элементов.
Все исходные данные {find^ указатель на начало линейного
списка) и результаты выполнения функции (указатель на нача­
ло линейного списка) должны передаваться через список пара­
метров.

Вариант 14. Определен следующий указатель на начало ли­


нейного списка:
зЬгасЬ Node // NODE: узел линейного списка
{
Node *pLink; // Pointer LINK:
// указатель на очередной узел
Info; // INFOrm at ion: информа ция
} *start;

Написать прототип, определение и пример вызова функции


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

Вариант 15. Определен следующий указатель на начало ли­


нейного списка:
stzTict Node // NODE: узел линейного списка

385
Node *pL±nk; // Pointer LINK:
// указатель на очередной узел
±nt Info; / / INFOrm at ion: мн ф орма ция
} *start;

Написать прототип, определение и пример вызова функции


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

Вариант 16. Определен следующий указатель на начало ли­


нейного списка:

struct Node // NODE: узел линейного списка


{
Node *рЫпк; // Pointer LINK:
// указатель на очередной узел
±nt Info/ / / INFOrm at ion: ин форма ция
} *start;

Написать прототип, определение и пример вызова функции


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

Вариант 17. Определен следующий указатель на начало ли­


нейного списка:

stxract Node // NODE: узел линейного списка


{
Node *pLink; // Pointer LINK:
// указатель на очередной узел
±nt Info; // INFOrm at ion: информа ция
} *start;

Написать прототип, определение и пример вызова функции


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

386
ного списка) должны передаваться через список параметров.

Вариант 18. Определен следующий указатель на начало ли­


нейного списка:
stjTuct Node // NODE: узел линейного списка
{
Node *pLink; // Pointer LINK:
// указатель на очередной узел
Infor­ // INFOrmation: информация
} ms tart;

Написать прототип, определение и пример вызова функции


для вставки новых элементов и в начало (в него помещается значе­
ние addjbeg), и в конец (в него помещается значение add_end) ли­
нейного списка. В частном случае, перед вызовом этой функции ли­
нейный список может быть пуст или может содержать любое коли­
чество элементов.
Все исходные данные {addbeg^ add_end указатель на нача­
ло линейного списка) и результаты работы функции (указатель
на начало линейного списка) должны передаваться через список
параметров. С целью обработки ошибок предусмотреть кон­
троль значения, возвращаемого операцией new.

Вариант 19. Определен следующий указатель на начало ли­


нейного списка:
stxTict Node // NODE: узел линейного списка
{
Node *pLlnk; // Pointer LINK:
// указатель на очередной узел
±nt Info; // INFOrmation: информация
} *start;

Написать прототип, определение и пример вызова функции


для вставки в линейный список после каждого элемента, в котором
хранится значение/?/7(i, двух элементов, в которых будут храниться
значения addl и add2, В частном случае, перед вызовом этой функ­
ции линейный список может быть пуст или может содержать любое
количество элементов.
Все исходные данные {find^ addl^ add2^ указатель на начало
линейного списка) должны передаваться через список парамет­
ров. С целью обработки ошибок предусмотреть контроль значе­
ния, возвращаемого операцией new.

Вариант 20. Определен следующий указатель на начало ли­


нейного списка:

387
stmict Node // NODE: узел линейного списка
{
Node *pLink; // Pointer LINK:
// указатель на очередной узел
xnt Info; // INFOrmation: информация
} *start;

Написать прототип, определение и пример вызова функции


для вставки в линейный список перед каждым элементом, в котором
хранится значение yz«<i, двух элементов, в которых будут храниться
значения addl и add2. В частном случае, перед вызовом этой функ­
ции линейный список может быть пуст или может содержать любое
количество элементов.
Все исходные данные (Jlnd^ addl^ add2^ указатель на начало
линейного списка) и результаты выполнения функции (указа­
тель на начало линейного списка) должны передаваться через
список параметров. С целью обработки ошибок предусмотреть
контроль значения, возвращаемого операцией new.

П.1.1.11. Препроцессор, перечисления, функции с умалчиваемыми


значениями аргументов, перегрузка функций, шаблоны функций,
перегрузка операций. Варианты тестов

Вариант 1, Директивы препроцессора. Укажите, как следует


оформить заголовочный файл, чтобы приведенная ниже запись не
приводила к возникновению ошибки:
# include "flle.h''
# include "file.h''

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


ный файл.

Вариант 2. Перечисления. Будет ли корректной приведенная


ниже программа:
^include <stdio.h>
±nt main ( jroid )
{
enum t{ c=-l, pasc=4, ada, modula2, forth=4 };
t m; m = a da;
printfi "\n m - %d", m ) ;
return 0;
}

Что при этом будет выведено на экран?

388
Вариант 3. Функции с умалчиваемыми значениями пара­
метров. Имеется следующий фрагмент программного кода:
voxd. DrawCircle ( izit к=100, Int у=100, ±nt radius=100 ) ;

Является ли запись прототипа функции правильной (обоснуйте


ответ)? Являются ли правильными приведенные ниже вызовы функ­
ции? В случае положительного ответа укажите, с какими значения­
ми параметров функция будет выполняться?
DrawCircle( ) ;
DrawCircle( 200 ) ;
DrawCircle( 200, 300 ) ;
DrawCircle( 200, 300, 400 ) ;
DrawCircle( , , 400 ) ;

Являются ли правильными приводимые ниже записи прототи­


пов функций (обоснуйте ответ)?
void. DrawCircle ( int х, int у=100, Int rad ) ;
void. DrawCircle ( int x, int y=100, int radlus=100 ) ;
void DrawCircle ( int x, int y, int radlus=100 ) ;

Вариант 4. Шаблоны функций, В одномерном массиве, со­


стоящем из п элементов, вычислить сумму отрицательных элемен­
тов. Исходные данные и полученные результаты обязательно
передавать через список параметров. Написать прототип, опреде­
ление шаблона функций и пример ее вызова для типов int, float и
double.

Вариант 5. Перегрузка операций для пользовательских ти­


пов. Определен следующий пользовательский тип для работы с ком­
плексными данными:
struct CMP // CoMPlex: комплексный тип
(
dovible г; // Вещественная часть
double i; // Мнимая часть
} ;

Написать определение функции, перегружающей операцию


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

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


пов. Определен следующий пользовательский тип для работы с ком­
плексными данными:

389
struct CMP // CoMPlex: комплексный тип
{
double r; // Вещественная часть
double ±; // Мнимая часть
} ;

Написать определение функции, перегружающей операцию


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

Вариант 7. Шаблоны функций. В одномерном массиве, со­


стоящем из п вещественных элементов, вычислить сумму элементов
массива с нечетными номерами. Исходные данные и полученные
результаты обязательно передавать через список параметров.
Написать прототип, определение шаблона функций и пример ее вы­
зова для типов intafloat и double.

Вариант 8. Перегрузка операций для пользовательских ти­


пов. Определен следующий пользовательский тип:
struct V
{
int arr[ 4 ]; // Вектор
} ;

Написать определение функции, перегружающей операцию


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

Вариант 9. Перегрузка операций для пользовательских ти­


пов. Определен следующий пользовательский тип:
struct V
{
double arr[ 4 ]; // Вектор
} ;

Написать определение функции, перегружающей операцию


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

Вариант 10. Функции с умалчиваемыми значениями пара­


метров. Имеется следующий фрагмент программного кода:
void Rect ( float w, tloat 1=1,5 ) ;

390
Является ли запись прототипа функции правильной (обоснуйте
Ваш ответ)? Являются ли правильными приведенные ниже вызовы
функции? В случае положительного ответа укажите, с какими зна­
чениями параметров функция будет выполняться?
Rect ( ) ;
Rect ( 2.0 ) ;
Rect ( 2.00, 3.00 ) ;

Вариант 11. Шаблоны функций. В одномерном массиве, со­


стоящем из п элементов, вычислить наибольшее значение элемента
массива. Исходные данные и полученные результаты обязатель­
но передавать через список параметров. Написать прототип, оп­
ределение шаблона функций и пример ее вызова для типов long,
float и double.

Вариант 12. Директивы препроцессора. Опишите:


• действия препроцессора по директиве include;
• различие форматов ^include <file.h> и include ''file.h'\

Вариант 13. Функции с умалчиваемыми значениями пара­


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

Вариант 14. Функции с умалчиваемыми значениями пара­


метров. Имеется следующий фрагмент программного кода:
void Point ( double х , double у=-1.5 ) ;

Является ли запись прототипа функции правильной (обоснуйте


Ваш ответ)? Являются ли правильными приведенные ниже вызовы
функции? В случае положительного ответа укажите, с какими зна­
чениями параметров функция будет выполняться?
Point ( , ) ;
Point ( 2.0, -1.5 ) ;
Point ( 2.00, 3.00, 4.7 ) ;
Point ( 4.7 ) ;

Вариант 15. Шаблоны функций. В одномерном массиве, со­


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

391
зова для типов int и double.

Вариант 16. Шаблоны функций. В одномерном массиве, со­


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

Вариант 17. Шаблоны функций. В матрице, состоящей из п


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

Вариант 18. Шаблоны функций. В матрице, состоящей из п


строк и т столбцов, определить максимальное из отрицательных
значений элементов матрицы. Исходные данные и полученные ре­
зультаты обязательно передавать через список параметров. На­
писать прототип, определение шаблона функций и пример ее вызова
для типов int и double.

Вариант 19. Директивы препроцессора. Что напечатает дан­


ная программа?
^include <stdio.h>
^define AREA (г) 3.14*г*г
±пЬ main ( void )
{
printf( "%f\n'\ AREA( 2.0-1.0 ) ) ;
jretujm 0;
}

Вариант 20. Шаблоны функций. В одномерном массиве, со­


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

П.1.2. Программные проекты

На практических занятиях студенты выполняют три про-

392
граммных проекта:
• решение простой задачи с использованием ПМ-ассемблера (выпол­
няется по усмотрению преподавателя и требует наличия компакт-
диска, прилагаемого к данному учебному пособию);
• структурное программирование средствами языков Си/С++;
• средства модульного программирования в языке C++.

П. 1.2.1. Программирование на ПМ-ассемблере. Варианты


программных проектов

Среда программирования. Интегрированная среда програм­


мирования ПМ-ассемблера описана в [1] и имеется на компакт-
диске.

Формулировка решаемой задачи. Задача, предложенная для


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

Содермсание отчета
1. ТЕХНИЧЕСКОЕ ЗАДАНИЕ - формулировка решаемой зада­
чи, требования к программному проекту, язык программирования.
2. ТЕКСТ ПРОГРАММЫ - назначение программы, листинг с
исходным текстом программы в самодокументируемой форме.
Многочисленные примеры оформления исходных текстов ПМ-
программ имеются в [1] и на компакт-диске.
3. ОПИСАНИЕ ПРОГРАММЫ - назначение программы; метод
решения задачи и основные расчетные соотношения; схема про­
граммы с необходимыми пояснениями, выполненная в соответствии
с действующими стандартами.
3. ПРОГРАММА И МЕТОДИКА ИСПЫТАНИЙ - разработка
контрольного примера (примеров) с их обоснованием и анализом,
результаты вычислений по отлаженной программе, выводы.

Варианты 1-5, В качестве первых пяти вариантов можно ис­


пользовать приведенные выше варианты 1-5 из подразд. П. 1.1.1.

Вариант 6. Ввести и напечатать значения элементов массива


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

393
Вариант 7. Ввести и напечатать значения элементов массива
вещественного типа с заданной размерностью. Вычислить и напеча­
тать произведение положительных элементов массива. Если массив
не содержит элементов с положительными значениями, то в качест­
ве ответа напечатать "В массиве нет положительных элементов".

Вариант 8. Ввести и напечатать значения элементов массива


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

Вариант 9. Ввести и напечатать значения элементов массива


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

Вариант 10, Ввести и напечатать значения элементов массива


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

Вариант 1L Ввести и напечатать значения элементов массива


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

Вариант 12. Ввести и напечатать значения элементов массива


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

Вариант 13. Ввести и напечатать значения элементов массива


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

Вариант 14. Ввести и напечатать значения элементов массива


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

394
Вариант 15, Ввести и напечатать значения элементов массива
вещественного типа с заданной размерностью. Упорядочить массив
по возрастанию значений элементов. Отсортированный массив на­
печатать.

Вариант 16, Ввести и напечатать значения элементов массива


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

Вариант. 17, Ввести и напечатать значения элементов массива


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

Вариант 18, Ввести и напечатать значения элементов массива


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

Вариант 19, Ввести и напечатать значения элементов массива


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

Вариант 20, Ввести и напечатать значения элементов массива


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

П.1.2.2. Структурное программирование средствами языков Си/С++.


Варианты программных проектов

Среда программирования. Любая интегрированная среда про­


граммирования языка С+-ь. На начальном этапе обучения можно ре­
комендовать использование простой интегрированной среды про­
граммирования Borland С+-ь 3.1 с переходом в будущем на более со-

395
временную и широко распространенную среду программирования
Microsoft Visual Studio C++ 6.0.
Формулировка решаемой задачи, С использованием средств
структурного программирования языков Си/С++ спроектировать три
элементарных программы для решения.
1. Задачи с линейным следованием операторов. Например, вы­
числить значение функции
у = arctg( 1 1 4 • 1п(«))

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


2. Задачи с ветвлением (использовать структурированные
операторы if, switch). Например, вычислить значение функции
[ а+Ь при х<1,
у := [ а*Ь при 1<=х<=2,
[ а-Ь в остальных случаях
3. Задачи с циклом (использовать структурированные операто­
ры while, do-while, for). Например, вычислить сумму ряда

Набор вариантов программных проектов приводится ниже.

Содерлсание отчета.
1. ТЕХНИЧЕСКОЕ ЗАДАНИЕ - формулировка решаемой задачи,
требования к программам, язык программирования.
2. ТЕКСТ ПРОГРАММЫ - для каждой программы в заголовке-
комментарии указать ее назначение, привести листинг с исход­
ным текстом в самодокументируемом виде. Создание программ­
ного проекта рассмотрено в приложении П.2. Рекомендации по
структуре программы и пример оформления исходного текста
программы приведены в приложении П.З.
3. ПРОГРАММА И МЕТОДИКА ИСПЫТАНИЙ - разработка кон­
трольных примеров с их обоснованием и анализом, результаты
вычислений по отлаженной программе, выводы. Рекомендации по
методике отладки разработанной программы приведены в
приложении П.4.

Вариант 1. Вычислить значения функций и сумму ряда


у = arctg('';5Lii^-.ln(a)) +10"^. 2,5 19(111(^7))

396
fa+b при х<\.
у = } ab при 1<=:X<=2,
Уа-b при х>2

Вариант 2. Вычислить значения функций и значение


факториала
V5^tg^(arcsin(x))^^:
ШЫ
при jc>3.
у =
{" при 1<х<=3,
YU при д:<=1
п\

Вариант 3. Вычислить значения функций и сумму ряда


х^ к
е . , ,ч2,33 sin'^Cx+TT/Z)
Vtg(in(A:)) j2.ig(x)

11 6 , 7 J C + 9 , 2 X ^ - 1 , 0 2 O C ^ «рм jc<=0,

•^ » — ^ при x>0
ax +b-x •sin(A:)

Вариант 4. Вычислить значения функций и сумму ряда


s2(el^4.3
у = дЯ^В^-*^»^ У ) .(2.3-^^1)
In* (л:)
a-\-bz-\-C'Z при х=\.
J^ = < d-\-n-z-¥f-z при jc-2.
g-bh-z+mz в остальных случаях

х-\-\ х+Ъ х+п


у = + + ... +
1 3 «

Вариант 5. Вычислить значения функций и сумму ряда

_ s'm(x^+x~^+x^^^) \0~^'k

397
f а-х+4 при x>4,
у = Ja(l-e-^) при 0<=х<=4,
I 0 в остальных случаях

У = Е{(-1Г/(2-а+1)}
а=0

Вариант 6, В ы ч и с л и т ь з н а ч е н и я ф у н к ц и й и с у м м у р я д а
sin'^(^2,8-e^+x) V-2 ^^ ^ л
У = •9,110^
х^-а"
Са+Ь при а>Ь,
Д' = при а<-Ь и а>0,
" в остальных случаях

1 V^2,3

а=1 а!

Вариант 7. В ы ч и с л и т ь з н а ч е н и я ф у н к ц и й и с у м м у р я д а

>^ 75,73-я •^^г^ И)-11.7

г к1 при \х\>\ и а>0.


>^ = < \+х при |x|<=l и а>0.
1 sin(x) в остальных случаях

у = Z(2.a-l)-0,5 а-\
а=1

Вариант 8. В ы ч и с л и т ь з н а ч е н и я ф у н к ц и й и с у м м у р я д а

_ (е^^^+Ь/а)'Л: (л-/) У24-23,6


5/ п
к1
с 9
z^+\g(a+b'C)/x при х>0.
У = < ^/z'X'Sm(a'X) при x<=0 W (x*z)>0.
1 в остальных случаях

у = 2+ I {Ы)'''^^<-;г^+~^)}
а=0 Ъа-\-\ 2'а+2

Вариант 9. В ы ч и с л и т ь з н а ч е н и я ф у н к ц и й и с у м м у р я д а

a+b
У =
7г/4-\-х' -1/J
398
(a+b+c при \b\<=\a\ и \c\<=\b\.
у = J a+b при \b\<=\a\ и
к1>1*|.
в остальных случаях
[ -

Вариант 10. Вычислить значения функций и произведение


сомножителей
у = e«(^^>(^psin(x)-x(^"'-x'/'"))10-3

Г(ж/2)-е<-^^'=*д<^)) при х>0.


У = } Я-/2 при х=0.
[ (^/2)е(*-*> в остальных случаях

у = \\+ П(2-а+1)

Вариант 11. Вычислить значения функций и сумму ряда


-4
sin(x)'10
^ ч |^c|-arcsin(x) •(7r/2 + e^^^' (а^-х^)))

х+2 при jc<3,

у =< .2-5 при 3<=jc<6,

зГ в остальных случаях
х+ух

у = Z(-l)"+'-l/a

Вариант 12. Вычислить значения функций и сумму ряда


.5
т-л/аЬ

(l/Q+a)^ при х>=1,


У = \ а^-,х при х<0.
ОМх-а в остальных случаях

У = Z(-l)''-l/(2-f3-a)
а=0

399
Вариант 13. Вычислить значения функций и сумму ряда

с b а
(\g{a/{a+b) при (а'Ь+с)>5,
У = < sin(a) при 0<=(а'Ь+с)<=5,
в остальных случаях
I 0

Вариант 14. Вычислить значения функций и сумму ряда

2 2
X-Z при х>0 и X >Z ,
2 2 2
У = ^ X 'Z при х<0 и X >Z ,
0 в остальных случаях

у = иК2-а+\Г
а=0

Вариант 15. Вычислить значения функций и сумму ряда

при л:>0.
у =

1"
\' при
в

У = Z^^
остальных
-l<=jc<=0.
случаях

Вариант 16. Вычислить значения функций и сумму ряда

?'^'^*^\-Ъ'Б\Г?{Х) , , In(a^)

а""}!^ 24,61-10"
при jc>0.

400
У=<

1"
\' при
в
-1<=л:<=0,
остальных случаях
п

Вариант 17. В ы ч и с л и т ь з н а ч е н и я ф у н к ц и й и с у м м у р я д а
^.(^-)2-a.(,_5)3.sm(^.10-5

{sin(x)

1
при

при х=0,
х>0,

п
ij—x в остальных случаях
а=\

Вариант 18. Вычислить значения функций и сумму ряда

е~^'^+] х^ . ^ - 4
\%{х 1{а-\-Ь)) при (сг+^))>0,
^ 1 \а+6|-lg(jc) при (а+Ь)<=0

у = Е8-(2.а-1)
а=1

Вариант 19. Вычислить значения функций и сумму ряда

^а-х
при х>3.
У =
гf''U при
при
1<х<=3,
л:<=1

й=1 2-0+1

Вариант 20. Вычислить значения функций и сумму ряда

1п*(х)
г a-x+4 при х>4.
у = \ а(1-е-^) при 0<=д:<=4,
в остальных случаях
1 0

401
o^l ^'

Указания no выполнению программных проектов


• При вычислении значения функции следует проверить область
допустимых значений аргументов функции (например, при вычис­
лении х^, где а - вещественное, должно быть д:>0; подкоренное
выражение, аргументы логарифмических функций должны быть
также положительными; делитель должен быть отличен от нуля; ар­
гумент тангенса не должен быть кратен ж/2 и т.п.).
• Для получения возможности использования математических
функций необходимо подключить соответствующий заголовочный
файл:
^include <math.h>

При этом следует иметь ввиду, что большинство математических


функций используют аргументы и имеют возвращаемое значение с
типом double. Поэтому аргументы функций, вычисляемых в про­
граммных проектах 1 и 2 также должны иметь тип double. Исчерпы­
вающий перечень и описание стандартных математических и других
стандартных функций приведен в [5].

П. 1.2,3. Средства модульного программирования в языке C++.


Варианты программных проектов

Среда программирования. Любая интегрированная среда про­


граммирования языка C++. Повторяем, что начальном этапе обуче­
ния можно рекомендовать использование простой интегрированной
среды программирования Borland C++ 3.1 с переходом в будущем на
более современную и широко распространенную среду программи­
рования Microsoft Visual Studio C++ 6.0 или 7.0 (.NET).

Задание (формулировка решаемой задачи). Задача, предло­


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

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

Содержание отчета.
1. ТЕХНИЧЕСКОЕ ЗАДАНИЕ - формулировка решаемой зада­
чи, требования к программе (в том числе та часть спецификации,
которая относится к обработке ошибок и предупреждений), язык
программирования.
2. ТЕКСТ ПРОГРАММЫ - для программы в заголовке-
комментарии указать ее назначение, привести листинг с исходным
текстом в самодокументируемом виде. Пример оформления исход­
ного текста программы приведен в приложении П.5.
3. ОПИСАНИЕ ПРОГРАММЫ - описание файловой и функ­
циональной структур программного проекта (вторая часть
спецификации), краткое описание работы программы и схемы 2-3
функций, выполненные в соответствии с действующими стандарта­
ми. 4. ПРОГРАММА И МЕТОДИКА ИСПЫТАНИЙ - описание
методики отладки, требования к контрольным примерам, разработка
контрольных примеров с их обоснованием и анализом, результаты
вычислений по отлаженной программе, выводы.

Указания по выполнению программных проектов,


1. Предусмотреть запуск программного проекта с
использованием командной строки.
2. Использовать файловый ввод-вывод.
3. Массив размещать в динамической памяти (особенности
размещения матрицы в динамической памяти рассмотрены выше в
разд. 8).

Вариант 1. Найти максимальное число, встречающееся в за­


данном векторе более одного раза.

Вариант 2. Определить норму заданной матрицы, т.е. значе­


ние
тах(Х|Ф][У]|)
у

Вариант 3. По заданной квадратной матрице размером N-N

403
построить вектор длиной (2Л^-1), элементы которого - максимумы
элементов диагоналей, параллельных главной, включая главную
диагональ.

Вариант 4, Характеристикой строки матрицы назовем сумму


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

Вариант 5. Для заданной квадратной матрицы найти минимум


среди сумм модулей элементов диагоналей, параллельных побочной
диагонали.

Вариант 6. Говорят, что матрица имеет седловой элемент


Ф][уЪ если элемент a[i][j] является минимальным в / - о й строке и
максимальным в у-ом столбце. Найти номера строки и столбца ка­
кого-либо седлового элемента и его значение.

Вариант 7. Найти значение наибольшего элемента матрицы


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

Вариант 8, Характеристикой столбца матрицы назовем сумму


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

Вариант 9, Элемент матрицы называется локальным миниму­


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

Вариант 10, Составить программу нахождения элемента


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

Вариант 11, Составить программу нахождения максимально­


го значения элемента вектора среди отрицательных и минимального
значения — среди положительных элементов.

Вариант 12, Написать программу, которая упорядочивала бы


элементы вектора по знаку, сначала положительные, а затем — отри-

404
дательные, в таком же порядке, как в исходном векторе.

Вариант 13. Составить программу, позволяющую найти мак­


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

Вариант 14. Составить программу поиска элементов, встре­


чающихся в векторе более одного раза. Из найденных элементов
сформировать новый вектор.

Вариант 15. Составить программу упорядочения по возраста­


нию элементов каждой строки матрицы. Сортировка строк должна
выполняться на месте, что означает, что вспомогательный вектор не
должен использоваться.

Вариант 16. Составить программу вычисления количества


положительных элементов в левом нижнем треугольнике квадрат­
ной матрицы. Треугольник включает диагональ матрицы.

Вариант 17. Составить программу обмена местами макси­


мального элемента главной диагонали квадратной матрицы и мини­
мального элемента побочной диагонали.

Вариант 18. Составить программу печати значений элементов


той строки матрицы, сумма элементов которой минимальна.

Вариант 19. Составить программу нахождения суммы значе­


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

Вариант 20. Составить программу перестановки строк матри­


цы по убыванию значения их первого элемента.

П.1.3. Экзаменационное тестирование

Наряду с традиционной формой, экзаменационное тестирова­


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

405
• простейшие ветвления;
а циклы;
• структуры;
а функции;
• области действия определений;
• массивы и указатели;
• работа с динамической памятью и операции с линейным
списком;
• препроцессор, перечисления, функции с умалчиваемыми
значениями аргументов, перегрузка функций, шаблоны функций,
перегрузка операций.
Комплексная проверочная работа рассчитана на 1 ч. 15 мин.
Ответ на каждый тестовый вопрос, в зависимости от правильности и
полноты, оценивается О, 0,25, 0,5, 0,75 или 1 баллом. Таким обра­
зом, максимальная сумма баллов может достигнуть 5.
В соответствии с набранными баллами выставляются следую­
щие экзаменационные оценки:
• "отлично" (4,25-5 баллов);
• "хорошо" (3,5-4 балла);
• "удовлетворительно" (2,5-3,25 балла);
• "неудовлетворительно" (менее 2,5 баллов).
Примеры формулировок тестовых экзаменационных вопросов
содержатся в подразд. П.1Л.

КОМПЛЕКСНАЯ ЭКЗАМЕНАЦИОННАЯ РАБОТА


Пример варианта

!• Структуры. В файле операционной системы "Task4.in'' хра­


нится в текстовой форме ведомость сдачи экзаменов студентами не­
которой группы. Каждая строка этого файла содержит сведения об
одном студенте, представленные в следующем формате:
позиции 1...2 - порядковый номер студента в группе;
позиция 3 - пробельная литера;
позиции 4...22 - фамилия студента длиной не более 18 сим­
волов в произвольном месте поля;
позиция 23 - пробельная литера;
позиция 24 - четыре оценки по четырем предметам, раз­
деленные не менее чем одной пробельной литерой.
Количество студентов в группе равно 16. Пример строк ука­
занного файла:
01 Андреев 5 4 5 5
02 Быков 5 5 5 5
16 Яковлев 4 4 5 4

406
1.1. Написать объявление массива структур для хранения ука­
занной ведомости.
1.2. Написать фрагмент программы, который заполнит экзаме­
национную ведомость данными, вводимыми из файла операционной
системы "Task4.in". Ввод данных должен осуществляться в тексто­
вом режиме.
1.3. Написать фрагмент программы, который вычисляет сред­
нюю экзаменационную оценку по всем предметам и студентам (т.е.
среднюю оценку из 64 оценок), а затем выводит значение этого по­
казателя в файл операционной системы ''Task4.ouf\
Примечание.
Закрыть открытые файлы, как только они станут не нуж­
ны.
Предусмотреть контроль корректности значений, возвра­
щаемых функциями библиотеки Си ^^fopen^\ ^^fscanf\ Указать,
какие включаемые файлы требует представленный фрагмент.

2. Функции. Написать прототип, определение функции и при­


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

3. Массивы и указатели. Что напечатает следующая про­


грамма?
^include <stdlo.h>

xnt Array[ ] = { 0 , 4 , 5 , 2 , 3 } ;

±nt main ( void )


{
±nt Index;
±nt ^Pointer;

for( Index = 0; Index <= 4; Index+=2 )


printf ( " %3d"r * (Array+Index--) );
prlntf ( "\n" ) ;

Pointer = Array + 1;
fox:( Index = 0; Index <= 2; )
printf( " %3d". Pointer[ ++Index ] );
printf ( "\n" ) ;

return 0;

407
4. Операции с линейным списком. Работа с динамической
памятью. Определен следующий указатель на начало линейного
списка:
stJTuct Node // NODE: узел линейного списка
{
Node *pLink; // Pointer LINK:
// указатель на очередной узел
double Info; // INFOrmation: информация
} * start;

Написать прототип, определение и пример вызова функции,


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

5. Шаблоны функций. В одномерном массиве, состоящем из п


элементов, вычислить сумму отрицательных элементов. Написать
прототип, определение шаблона функций и пример ее вызова для
типов int, float и double.

Приложение П.2. Создание программного проекта

Ниже рассматривается создание программного проекта в двух


средах программирования:
• в интегрированной среде проектирования программ (IDE - Inte­
grated Development Environment) MS Visual Studio C++ 6.0;
• в IDE Borland C++ 3.1.

П.2.1. IDE MS Visual Studio C++ 6.0.


Создание программного проекта

Интегрированная среда проектирования программ (IDE) пред­


ставляет собой комплект программных инструментов - Tools (рис.
106). Этот комплект инструментов - хороший, инструментов — мно­
го, но среда не русифицирована (в ней используется английский
язык).

408
Проекты (Projects). Проекты IDE характеризуются следую­
щими особенностями.
1. Единицей работы IDE является проект. Проект — это ком­
плект файлов.
2. Виды файлов в составе проекта:
• исходные файлы, написанные программистом {*.срр — С Plus Plas
— тексты на языке 0 + + и *./; — Header — заголовочные файлы), IDE
содержит инструменты, которые позволяют автоматизировать со­
ставление исходных файлов;
• служебные файлы, которые автоматически создаются IDE, но по
инструкциям программиста.
3. Каталог проекта. Служебные файлы обязательно располага­
ются в этом каталоге. Исходные файлы хотя и могут располагаться
где угодно, но, чтобы не запутаться, их тоже следует поместить в
каталог проекта.
4. Проекты IDE и проекты программного обеспечения. Про­
стые программы представляют собой просто один проект IDE.
Сложное программное обеспечение реализуется в виде некоторого
множества проектов IDE.

Редактор Символичес­
Компилятор Компоновщик
текстов (Text кий отладчик Tools
СИ/С++ (Linker)
Editor) (Debugger)
Рис. 106. Интегрированная среда проектирования программ
MS Visual Studio С-ь+ 6.0

Создание нового проекта для консольного прило:исения. Для


того чтобы создать новое приложение (программу), необходимо соз­
дать новый проект. Для этого в IDE выполните команду New... из
меню File, в результате чего на экране появится диалоговое окно
New.
В этом окне необходимо выполнить следующее:
• Выбрать тип создаваемого приложения. В данном случае
следует выбрать опцию Win32 Console Application, поскольку мы
создаем консольное приложение, которое является Windows-
аналогом старого доброго знакомого - программы для MS DOS.
• Выбрать место расположения нового проекта. Информация
о расположении новой рабочей области проекта
(диск:\путь\подкаталог) вводится в поле Location (местоположение).

409
Это можно сделать, набрав путь вручную, или воспользовавшись
расположенной справа кнопкой Browse... (просмотр). Разумеется,
что соответствующий подкаталог должен быть предварительно соз­
дан.
• Указать утилите Project имя файла проекта. Одновременно
с вводом имени проекта в поле Project Name (имя проекта) это же
имя автоматически добавляется в качестве подкаталога в поле Loca­
tion.
После выполнения указанных действий для создания проекта
следует нажать кнопку [ОК], в результате чего на экране появится
диалоговое окно мастера создания консольного приложения. В этом
окне выбираем переключатель An empty project (пустой проект).
При этом создаются только служебные файлы проекта. Для того
чтобы наполнить созданный проект, необходимо добавить в него
файл(ы), содержащий(ие) текст программы.
Это можно сделать двумя способами.
1. Добавить в проект уже существующий файл(ы), создан-
ный(ые) ранее в текстовом редакторе и имеющий(ие) расширение
*.с/?/?. Повторно обращаем внимание на то, что следует предвари­
тельно поместить существующий(ие) файл(ы) в каталог проекта
(лучше все иметь в одном месте).
2. Создать новый файл и вставить его в проект.
Добавление в проект существующего файла. Для этого необ­
ходимо выбрать в меню Project пункты Add to Project (добавить в
проект) и Files.... В результате этих действий на экран будет выве­
дено диалоговое окно Insert Files into Project (добавление файлов в
проект). Здесь следует выбрать те файлы, имеющие расширение
.срр, которые хотите включить в проект. Это можно сделать, либо
дважды щелкнув кнопкой мыши на имени файла, либо выделив
нужные файлы и нажав кнопку [OKJ.
Создание нового файла и включение его в проект. Для созда­
ния нового файла необходимо из меню File выполнить команду
New... и в появившемся диалоговом окне New выбрать вкладку
Files, где представлены все типы файлов, которые можно создавать.
Флажок Add to Project (добавить в проект) должен быть установ­
лен, чтобы создаваемый файл автоматически был добавлен в проект.
В списке Files выберите тип создаваемого файла — C/C++ Header
File или C++ Source File, а в поле File name: - имя файла. Осталось
нажать кнопку [ОК]. В результате Visual C++ создаст файл и откро­
ет пустое поле редактирования текста.
После набора и сохранения всех текстов можно переходить к
следующему этапу - отладке программного проекта.

410
Открытие для работы существующего проекта. Для суще­
ствующего проекта необходимо из меню File выполнить команду
Open Workspace ... и в появившемся диалоговом окне Open Work­
space войти в каталог проекта и "кликнуть" по файлу с расширением
.dsw. В результате проект загружается в IDE для последующей рабо­
ты.

П.2.2. IDE Borland C++ 3.1.


Создание программного проекта

Создание нового проекта П08'Прило:>§сения, Для того чтобы


создать новое приложение (программу), необходимо создать новый
проект. Для нового проекта следует предварительно создать каталог.
С помощью встроенного в IDE текстового редактора в каталоге про­
екта следует создать файлы проекта с исходным текстом с расшире­
ниями *.срр, *.h или *.hpp.
После создания исходных файлов надо создать файл проекта.
Для этого надо выбрать в меню Project команду Open Project..., в
появившемся окне Open Project File ввести имя файла проекта и
нажать кнопку [ОК].
Для включения в проект файлов с расширениями *.с/?р (в ча­
стном случае в проекте такой файл может быть единственным) сле­
дует на рабочем столе активизировать окно Project, выбрать в меню
Project команду Add Item..., в появившемся окне Add to Project
List "кликнуть" по каждому из файлов с расширением *.ср/7 и на­
жать кнопку [Done]. В результате этого будет создан требуемый
программный проект.
После создания программного проекта необходимо проверить
и, при необходимости, скорректировать информацию о местополо­
жении каталогов стандартных включаемых файлов. С этой целью
достаточно в меню Options выполнить команду Directories, в поя­
вившемся окне Directories указать расположение каталогов стан­
дартных включаемых файлов и нажать кнопку [ОК]. После этого
программный проект готов к работе.

Открытие для работы существующего проекта DOS-


прилоэн:ения. Для существующего проекта необходимо из меню
Project выполнить команду Open Project..., в появившемся окне
Open Project File в каталоге проекта выбрать имя файла проекта и
нажать кнопку [ОК]. В результате проект загружается в IDE для по­
следующей работы.

411
Приложение П.З. Рекомендации по структуре
однофайловой программы с одной функцией
и пример оформления исходного текста

приведенный ниже пример оформления исходного текста про­


стейшего однофайлового программного проекта с единственной
функцией попутно преследует и другую цель — он демонстрирует
какой должна быть структура программы:

Файл TASK01.CPP

Проект однофайловый с единственной


функцией (главной)

Назначение пример простой программы г


вычисляющей с := а + b

Состав проекта файл проекта TASK01.PRJ/


файл TASK01.СРР (главная функция);
файл TASK01.DAT (файл данных);
файл TASK01.OUT (файл результатов)

ЭВМ IBM 80386

Среда программирования: ВС31 (C++)

Операционная система DR DOS 6.0

Дата создания 08.11.2000


Дата корректировки

Иванов И. И,, каф. АВТ, ФТК, гр. 1081/4


Санкт-Петербургским государственный политехнический
университет
V
// Для работы с функциями ввода-вывода: STanDard Input Output
^include <stdio.h>

±nt main( void. ) // Возвращает 0 при успехе


{
±nt a, Ь, // Аргументы функции
с, // Значение функции
ret code; // Возвращаемое значение для fscant
FILE *£_±Пг // Указатель на структуру со
// сведениями о файле для чтения
*f_out; // Указатель на структуру со
// сведениями о файле для записи

// Открываем файл для чтения


±£( ( f_ln = fopen( "task01.dat", "г" ) ) == NULL )
{
printf( "\n Ошибка 10. Файл taskOl.dat для чтения не"
" открыт \п" ) ;

412
retvLm 10;
}

// Читаем значения аргументов функции


retcode = fscanf( f_±n, " %i %i", &a, &b ) ;
±£( retcode != 2 )
{
printf( "\n Ошибка 20. Произошла ошибка чтения из"
" файла taskOl.dat \п" ) ;
jtretujm 20;
}

// Закрываем файл для чтения


±f( fclose( f_in ) == EOF )
{
printf( "\n Ошибка 30. Файл task01.dat не "
"закрыт \п" ) ;
return 30;
}

// Открываем файл для записи


з.£( ( f_out = fopen( "task01.out"r "w" ) ) == NULL )
{
printf( "\n Ошибка 40. Файл taskOl.out для записи "
"не открыт \п" ) ;
return 40;
}

// Печатаем заголовок и аргументы функции


fprintf( f_out,
"\п Иванов И. И. г каф. АВТ, ФТК, гр. 1081/4
"\п С.-Петербургский государственный политехнический унверситет "
"\п (семестр 1, программный проект 1) \п"
"\п с := а + b \п"
"\п Аргументы функции: a=%i b=%i \л", а, b ) ;
// Закрываем файл для записи
±f( fclose( f_out ) -= EOF )
{
printf( "\n Ошибка 50. Файл taskOl.out не закрыт \n" ) ;
return 50;
}

// Вычисляем значение функции - в этом месте обычно делается


// довольно много работы: проверяется область допустимых
// значений прочитанных данных (аргументов функции) и
// выполняется решение задачи
с = а + Ь;
// Открываем файл для дозаписи
±f( ( f_out = fopen( "taskOl. out ", "a" ) ) == NULL )
{
printf( "\n Ошибка 60. Файл taskOl.out для дозаписи "
"не открыт \п" ) ;
return 60;
}

// Печатаем значение функции


fprintf ( f_outr

413
"\n Значение функции: с=%1'\ с ) ;
// Закрываем файл для записи
±f( fclose( f_out ) == EOF )
{
printf( "\n Ошибка 10. Файл taskOl.out не закрыт \n" ) ;
r&tuxm 10;
}

return 0;

Рекомендуем использовать этот пример как "образец для под­


ражания". Обращаем внимание на следующие особенности.
1. В программе используется файловый ввод-вывод. Это наи­
более рациональный способ ввода-вывода.
2. Программа имеет следующую структуру:
• открытие файла данных для чтения, чтение из него данных и за­
крытие файла (обратите внимание, что файл данных закрывается
сразу же после завершения из него чтения, а не в конце работы
программы - так выгоднее);
• открытие файла результатов для записи, вывод в него заголовка и
прочитанных данных и закрытие файла (обратите внимание, что
файл результатов закрывается сразу же после завершения вывода
в него значений прочитанных данных, а не в конце работы
программы - так также выгоднее);
• проверка области допустимых значений прочитанных данных
(это обязательно нужно делать) и содержательное решение зада­
чи, которое в большинстве случаев является довольно сложным —
поэтому-то на это время держать файл результатов открытым
невыгодно;
• открытие файла результатов для дозаписи, вывод в него результа­
тов решения задачи и закрытие файла.
3. Для обработки ошибок в программе используются значения,
возвращаемые функциями библиотеки Си. Это нужно делать всегда.
4. Чтобы сделать программу наглядной, легко читаемой в про­
грамме используется рациональная ступенчатая запись и коммента­
рии. Никогда не пренебрегайте подобными элементами оформления
и внимательно изучите пример с этой точки зрения.

Приложение П.4. Методика отладки программы

Известное высказывание о том, что после обнаружения по­


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

414
шое внимание. Как же вести отладку программы?
Все обнаруживаемые в программе ошибки можно разделить на
три большие категории.
1. Синтаксические ошибки, которые автоматически выявляют­
ся на этапе компиляции. Уяснить смысл синтаксических ошибок и
устранить их достаточно легко, так как здесь в качестве достаточно
хорошего помощника выступает компилятор. В зависимости от язы­
ка программирования, компилятор лучше или хуже выявляет такие
ошибки. В ряде случаев синтаксическая ошибка в программе влечет
за собой неадекватную реакцию компилятора. Например, отсутствие
скобки часто приводит к тому, что компилятор обнаруживает ошиб­
ку через десятки строк кода. В последнем случае можно рекомендо­
вать одновременный набор открывающей и закрывающей скобок
(например, { }) с последующим вводом текста между ними.
2. Логические (часто их также называют алгоритмическими)
ошибки. Их бывает наиболее трудно обнаружить и исправить. Часть
из них выявляется на этапе отладки, часть на этапе сопровождения,
а некоторые приводят к тяжелым последствиям.
3. Информационные ошибки. В частности, к Появлению ин­
формационных ошибок может привести отсутствие обработки оши­
бок ввода-вывода, попытки деления на ноль, переполнение разряд­
ной сетки компьютера и т.п. Для исключения и/или обработки ин­
формационных ошибок в ряде случаев приходится значительную
часть исходного кода программы отводить для всевозможных про­
верок.

П.4.1. Компиляция и компоновка программного проекта.


Устранение синтаксических ошибок

На этапе отладки необходимо устранить все синтаксические и


большую часть логических ошибок в программном проекте, допу­
щенных на предыдущих этапах создания приложения. Для этого не­
обходимо скомпилировать каждый созданный файл. Это можно сде­
лать несколькими способами.
• Скомпилировать каждый файл с расширением .срр. Для этой
цели в IDE MS Visual Studio C++ 6.0 можно использовать команду
Build I Compile имя_файла или комбинацию клавиш <CtrH-F7>, а в
IDE Borland C++ 3.1 - команду Compile | Compile имя_файла или
комбинацию клавиш <Alt+F9>. Компиляцию отдельного файла
удобно использовать в больших проектах, чтобы сосредоточиться на
конкретном файле. При этом следует иметь в виду, что ссылки меж­
ду файлами не проверяются.
• Скомпилировать и скомпоновать все файлы проекта ("со­
брать" или "построить" исполняемый файл), воспользовавшись для

415
этого в IDE MS Visual Studio C++ 6.0 командами Build | Build
имя_файла эквивалентно <F7> или Build | Rebuild All. Единствен­
ным отличием этих команд является то, что команда Rebuild All не
проверяет даты создания файлов проекта и компилирует все файлы,
а не только те, которые были модифицированы после компиляции.
Аналогичным образом, в IDE Borland C++ 3.1 можно использовать
команды Compile | Маке имя_файла эквивалентно <F9> или Com­
pile I Build All. В результате создается исполняемый файл с расши­
рением .ехе,
• Можно сразу запустить приложение, выполнив в IDE MS
Visual Studio C++ 6.0 команду Build | Execute имя_файла или по
комбинации клавиш <Ctrl+F5>, а в IDE Borland C++ 3.1 -команду
Run I Run имя_файла или по комбинации клавиш <Ctrl+F9>. Если
в программный проект были внесены какие либо изменения, то в
IDE MS Visual Studio C++ 6.0 на экране будет высвечено диалоговое
окно с запросом на построение исполняемого файла. Для построе­
ния указанного файла следует нажать кнопку [Да]. В IDE Borland
C++ 3.1 подобный запрос не выполняется.
Если программный проект содержит синтаксические ошибки,
то в IDE MS Visual Studio C++ 6.0 при выполнении любой из пред­
ставленных команд информация об ошибках автоматически отобра­
жается во вкладке Build окна Output, по умолчанию расположенно­
го в нижней части окна IDE. Если окно Output было удалено с экра­
на, то его можно вывести снова на экран с помощью команды View |
Output или по комбинации клавиш <Alt+2>. Каждое сообщение об
ошибке или предупреждении начинается с имени файла, где они об­
наружены, за которым следует номер строки, где это произошло, а
далее идут двоеточие и слово "error" (ошибка) или "warning" (пре­
дупреждение) и соответствующий номер. В конце приводится крат­
кое описание ошибки или предупреждения. Если дважды щелкнуть
левой кнопкой мыши на строке с сообщением или предупреждени­
ем, то ошибочная строка будет отмечена стрелкой в левой части в
соответствующем окне редактирования. Лучше всего добиться, что­
бы в окончательном варианте не было ни того, ни другого, хотя с
предупреждениями исполняемый файл создается и может быть за­
пущен.
Аналогичным образом, в IDE Borland C++ 3.1 при наличии
синтаксических ошибок при выполнении любой из представленных
выше команд информация об ошибках автоматически отображается
в появившемся окне Message. Каждое сообщение об ошибке или
предупреждении начинается со слова "error" (ошибка) или
"warning" (предупреждение), за которым следуют имя файла и но­
мер ошибочной строки, а далее идут двоеточие и приводится крат­
кое описание ошибки или предупреждения. Если дважды щелкнуть

416
левой кнопкой мыши на строке с сообш^ением или предупреждени­
ем, то в соответствуюш[ем окне редактирования в ошибочную строку
будет помещен курсор, а текст сообщения будет повторен в нижней
части окна редактирования.
После устранения синтаксичесих ошибок следует запустить
программу, выполнив команду Build | Execute имя__файла либо по
комбинации клавиш <Ctrl+F5> (IDE MS Visual Studio C++ 6.0) или
команду Run | Run имя_файла либо по комбинации клавиш
<CtrI+F9> (IDE Borland C++ 3.1). При этом также можно получить
сообщение об ошибке (или ошибках). Это тот самый случай, когда
программный проект не содержит синтаксических ошибок, а при­
ложение не работает. Вызвано это так называемыми логическими
(алгоритмическими) ошибками, для обнаружения которых можно
использовать разные методы (например, закомментировать фраг­
менты программы).
Однако лучше всего воспользоваться имеющимся в IDE
встроенным отладчиком.

П.4.2. Отладка программного проекта.


Устранение логических (алгоритмических) ошибок

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


• Пошаговое выполнение программы.
а Просмотр значений переменных в любом месте программы.
Для пошагового выполнения программы отладчик предостав­
ляет несколько возможностей, основные из которых мы и рассмот­
рим вначале для IDE MS Visual Studio C++ 6.0, a затем и для IDE
Borland C + + 3 . 1 .

Встроенный отладчик IDE MS Visual Studio C++ 6,0. Для за­


пуска исполняемого файла в режиме отладки можно выполнить ко­
манду Build I Start Debug | Go (выполнить) или эквивалентно на­
жать клавишу <F5>. Однако если просто выполнить эту команду, не
предпринимая никаких предварительных действий, работа програм­
мы не будет отличаться от запуска в обычном режиме, разве что при
завершении во вкладке Debug в нижней части окна Output интегри­
рованной среды разработки появится информация о параметрах за­
вершения работы программы.
Чтобы перейти в режим пошагового выполнения, предвари­
тельно перед выполнением команда Go необходимо установить так
называемые точки останова {breakpoints), которые можно рассмат­
ривать как стоп-сигналы для отладчика. Обычно они устанавлива­
ются в местах, которые вызывают сомнение в правильности выпол­
нения. При этом предполагается, что все операторы, предшествую-

417
ш^ие первой точке останова, выполняются правильно. Самый про­
стой способ установки точки останова заключается в следующем.
Курсор устанавливается на строку, на которой нужно остановить
работу программы, и нажимается клавиша <F9>. Повторное нажатие
клавиши <F9> удаляет точку останова. Строка останова в окне ре­
дактирования отмечена темно-красным кружком в крайней левой
позиции. Если, после задания точки останова, программу запустить
по команде Build | Start Debug | Go, либо нажав клавишу <F5>, то
все операторы программы, предшествующие точке останова, будут
выполняться в обычном режиме и только перед строкой останова
выполнение программы приостановится.
При этом внешний вид интегрированной среды разработки
существенно изменится. Во-первых, изменипся состав основного
меню. Во-вторых, строка, которая будет выполняться следующей,
будет отмечена желтой (по умолчанию) стрелкой. И, наконец, поя­
вится два новых окна - Variables (переменные) и Watch (наблюде­
ние), которые позволяют просматривать и менять значения пере­
менных. Если одно из окон или оба окна на экране отсутствуют, то
их можно поместить на экран, используя комбинации клавиш
<Alt-4-3> для переменных и/или <Alt+4> для наблюдения.
Для пошагового выполнения в отладчике имеются следующие
команды.
• Debug I Step Over (шаг через) или эквивалентно <F10> -
выполняет текущий оператор или функцию и переходит к следую­
щей строке.
• Debug I Step Into (шаг внутрь) или эквивалентно <F11> -
выполняет текущий оператор языка C++ или переходит к первому
оператору функции.
а Debug | Step Out (шаг вне) или эквивалентно < S h i f t + F l l > -
завершает выполнение текущей функции и переходит к строке, не­
посредственно следующей за ее вызовом.
• Debug I Run to C u r s o r (выполнить до курсора) или эквива­
лентно <Ctrl+F10> - выполняет программу до строки, где в текущий
момент находится курсор.
В окне Variables (переменные) автоматически отображаются
только локальные переменные текущего блока. После каждого шага
выполнения программы значения этих переменных обновляются. В
строке Context указывается, в какой функции (блоке) в данный мо­
мент находимся.
Переменные, которые нужно контролировать или изменять по
желанию программиста, можно задать в окне Watch (наблюдение).
Для этого в свободной строке столбца Name для контроля значения
переменной достаточно набрать идентификатор переменной и на-

418
жать клавишу [Enter]. Для изменения значения переменной в про­
цессе отладки следует выбрать строку с именем этой переменной, с
помощью клавиши [Tab] перейти в столбец Value, набрать там но­
вое значение и нажать клавишу [Enter]. При дальнейшей отладке,
вместо прежнего значения, будет использовано модифицированное
таким образом значение переменной.
Для просмотра значения переменной в реэюиме отладки, наря­
ду с использованием окон Variables и Watch, можно в окне редак­
тирования поставить курсор на имя интересующей нас переменной.
Если переменной было присвоено значение, то появится всплываю­
щее окно со значением этой переменной Эта возмолсностъ наибо­
лее удобна и мы рекомендуем ее использовать как моэюно чаще.

Встроенный отладчик IDE Borland C++ 5.7. Для запуска ис­


полняемого файла в режиме отладки следует также предварительно
задать точки останова (breakpoints). Предполагаем, что все опера­
торы, предшествующие первой точке останова, выполняются пра­
вильно. Самый простой способ установки точки останова заключа­
ется в следующем. Курсор устанавливается на строку, на которой
нужно остановить работу программы, и вводится комбинация кла­
виш <Ctrl+F8>. Повторный ввод этой комбинации удаляет точку
останова. Строка останова в окне редактирования отмечена красным
цветом. Если, после задания точки останова, программу запустить
по команде Run | Run, либо введя комбинацию клавиш <Ctrl+F9>,
то все операторы программы, предшествующие точке останова, бу­
дут выполняться в обычном режиме и только перед строкой остано­
ва выполнение программы приостановится. При этом строка остано­
ва сохранит подсветку, но изменит цвет подсветки.
Для пошагового выполнения в отладчике имеются следующие
команды.
а Run I Trace over (шаг поверх) или эквивалентно <F8> - вы­
полняет текущий оператор или функцию и переходит к следующей
строке.
а Run I Trace into (шаг внутрь) или эквивалентно <F7> - вы­
полняет текущий оператор языка C++ или переходит к первому опе­
ратору вызываемой функции.
• Run I Go to cursor (выполнить до курсора) или эквивалент­
но <F4> - выполняет программу до строки, где в текущий момент
находится курсор.
Переменные, которые нужно контролировать в точке останова
можно посмотреть в окне Watch. Чтобы это окно появилось в IDE и
отображало значение требуемого объекта, достаточно поместить
курсор на идентификатор объекта, ввести комбинацию клавиш
<Ctrl+F7> и в появмвшемся окне Add Watch нажать кнопку [ОК].

419
П.4.3. Тестирование программного проекта

Как и любой другой продукт производства, программа перед


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

Требования к контрольным примерам. Какие же требования


следует предъявить к контрольным примерам?
Таких требований всего два.
• Набор контрольных примеров должен быть достаточным,
чтобы показать выполнение всех требований технического задания
и обеспечить полную проверку программного проекта — протестиро­
вать все ветви, имеющиеся в программе.
• Контрольные примеры должны быть простыми в том смыс­
ле, чтобы анализ ожидаемых результатов был несложным (примеры
должны быть небольшой размерности со значениями исходных дан­
ных, удобными для анализа).
Отметим, что создание достаточного и простого набора кон­
трольных примеров является нетривиальной задачей.

Структура контрольного примера. Структура контрольного


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

Выбор точек останова. При выборе точек останова можно


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

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

Методика тестирования программы для контрольных при-


меров с нормальным завершением.
При тестировании программы выполняются следующие шаги.
• Программа запускается до первой точки останова так, как
это указывалось выше. Если полученные машинные результаты сов­
падают с результатами анализа, приведенного в контрольном при­
мере, то аналогично программа запускается до следующей точки ос­
танова и т.д.
• Если в очередной точке останова машинные результаты от­
личаются от ожидаемых, то текущий сеанс отладки с помощью ко­
манды Debug I Stop Debugging (IDE MS Visual Studio C++ 6.0) или
Run I Program Reset (IDE Borland C++ 3.1) прекращается. Програм­
ма повторно запускается до последней точки останова с хорошими
результатами и с этого места выполняется по шагам с проверкой по­
лученных результатов (выполняется трассировка ошибочного участ­
ка). По результатам пошаговой проверки обнаруживается ошибка и
текущий сеанс отладки также прекращается. Затем в исходный текст
программы вносятся необходимые исправления и трассировка оши­
бочного участка повторяется. Этот процесс заканчивается после ис­
правления ошибок, о чем будет свидетельствовать получение в оче­
редной точке останова ожидаемых результатов.

Приложение П.5. Рекомендации по созданию


многофайлового программного проекта
с несколькими функциями и пример оформления
исходного текста.

П.5.1. Спецификация программного проекта

Работа над программным проектом начинается с разработки

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

Основные особенности использования функций в программ­


ных проектах рассмотрены выше в подразд. 2.1 и 3.5. Функциональ­
ный типовой состав программного проекта, как минимум, включает
главную функцию, из которой последовательно вызываются функ­
ции ввода исходных данных, их печати, решения задачи и печати
полученных ответов. Для удобства использования функций универ­
сальные функции с широкой областью применения целесообразно
размещать в отдельных файлах, причем взаимосвязанные универ­
сальные функции можно помещать в отдельный общий файл. Спе­
циализированные же функции, напротив, размещают обычно в том
же файле, где находится главная функция программного проекта.
Приведем пример спецификации программного проекта для
решения следующей задачи. Выполнить обработку матрицы, заклю­
чающуюся в том, что в каждой строке матрицы ищется максималь­
ный элемент. Элементы, стоящие после максимального элемента,
следует заменить нулями и поместить в начало строки. Исходную и
вновь полученную матрицы напечатать. Предусмотреть запуск про­
граммного проекта с использованием командной строки. В файле
исходных данных последовательно содержатся строчный размер
матрицы, число столбцов матрицы и значения элементов матрицы,
разделенные символами пробельной группы (' ', '\^', '\«'). Матрица
размещается в статической памяти.

Файловая и функциональная структура программного проекта


(рис. 107).

422
Файл Main.cpp Файл ErWarnW.cpp Файл CheckCS.cpp

ErrorWarningWork()
main()
Обработка ошибок и
Главная функция
предупреждений

Proglnfo()
CheckComString()
Информация о прог­
Контроль командной
рамме и командной
строки
строке

1, 2, ..., 6 - порядок вы­


зова функций
ReadMatrix() OpenFile()
Чтение матрицы Открытие файла Функция OpenFile()
вызывается из функций
ErrorWarningWork(),
WriteMatrix() ReadMatrix() и
CloseFile() WriteMatrix()
Печать матрицы с
Закрытие файла
заголовком
Функция CloseFile()
Файл FileOC.cpp вызывается из функций
SwapUnits() ErrorWarningWork(),
Перестановка ReadMatrix() и
элементов строк WriteMatrix()
Функция ErrorWarningWork() вызывается из функ­
Файл Matrix.срр ций ReadMatrix() и WriteMatrix()
^ Вызов функции
Возврат из функции

Рис. 107. Файловая и функциональная структура программного


проекта

Желаемый состав и интерфейс функций. Чтобы функции


ввода и печати массива стали универсальными, надо их снабдить
следующим интерфейсом:

/ / Прототипы функций
void ReadArray(
±nt Arr[ N ], // Вводимый массив
// Файл ввода
СЪАГ Filelnp[ ] ) ;

void. PrlntArray (
int Arr [ N ] r // Выводимый массив
// Файл вывода
char FileOutf 7,
cha.r Mode [ ] r // Режим открытия файла вывода

423
/ / Заголовок для печати
char Header[ ] ) ;

Перечисленные функции целесообразно разместить в отдель­


ном файле как универсальные и взаимосвязанные.
Типовыми универсальными операциями являются операции
открытия-закрытия файлов. Поэтому их следует реализовать в виде
функций, расположенных в общем отдельном файле:
FILE * OpenFile ( // Возвращает указатель на структуру
// со сведениями об открытом файле
// Открываемый файл
сЬлг FileName[ ],
сЬа.г Mode [ ] , // Режим открытия файла
хпЬ ErrCode ) ; / / Код ошибки
void CloseFile(
// За крыв аемый файл
char FileName [ ],
// Указатель на структуру со сведениями о закрываемом
// файле
FILE *pStrInfoFlle,
int ErrCode ) ; / / Код ошибки

Об использовании командной строки при запуске программного


проекта. С этой целью можно использовать, например, командную
строку следующего вида:
Task02.еке Task02.1пр Task02,out [Enter]

При этом заголовок главной функции может иметь следующий


вид:
int main ( // Возвращает О при успехе
±пЬ АгдС, // ARGument Count: число аргументов
// командной строки (в примере 3)
char *ArgV[ ] ) / / Argument Value: массив указателей
// на аргументы командной строки
// ( в примере ArgV [ 1 ]
// эквивалентно "Task02,1пр"^
// ArgVf 2 ] эквивалентно
// "Task02.out")

Для обработки ошибок в формате командной строки можно


использовать функцию следующего вида (эта функция универсальна
и ее целесообразно поместить в отдельный файл):
лго±<1 WorkCS (
int ArgC, // Число аргументов командной строки

424
±nt ErrCode ) // Код ошибки
(
±f( ArgC != 3 )
{
printf(
"\n Ошибка %d. Непредусмотренный формат командной строки. "
" \ л Для запуска программы используйте командную строку вида:"
"\п Т4сполняемый_файл Файл_ввода Файл_вывода'\ ErrCode ) ;
e x i t ( ErrCode ) ;
}

Об обработке ошибок и предупреждений. Чтобы многократно не


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

// Прототип
•vo±(X ErrWarnWork ( xnt ErrWarnCode, сЬат Msg[ ] , char Type^
cbar FileOutl ] = "", сЬаг Mode[ ] = "" ) ;

// Определение
void ErrWarnWork(
// Код ошибки или предупреждения
±nt ErrWarnCode,
char Msg[ ], // Строка сообщения
char Type, // 'е' - ошибка, сообш,ение выдается
// на экран, 'w' - предупреждение,
// сообш,ение выдается в файл на МД
// Файл вывода
char FlleOutf ],
cha.r Mode [ ] ) // Режим открытия файла вывода
swibch( Туре )
{
ca.se 'е' :

printf( "\п Ошибка %d. %s ", ErrWarnCode, Msg ) ;


exit ( ErrWarnCode ) ;

case 'w*:

// Здесь открывается файл FileOut в режиме Mode


// (получаем указатель pFileOut с типом FILE *)
fprintf ( pFileOut, "\п Предупреждение %d. %s ",
ErrWarnCode, Msg ) ;
// Здесь закрывается файл с указателем pFileOut
break;

cLefavLl Ь:

printf(

425
"\п Ошибка %d. Использован недопустимый тип сообщения:"
"\п используйте \'е\' или \'w\' ", ErrWarnCode ) /
exit( ErrWarnCode ) ;
}

retvLrn;
}

// Пример вызова для предупреждения


// Для сообш,ения
ch&r buff 200 ];
// Формируем текст предупреждения
sprint f ( bufr " . . . Управляюш,ая строка с форматами . . . ",
список аргументов для форматов ) ;
ErrWarnWork( 40, buf, 'W, ArgV[ 2 ], "a" ) ;
// Пример вызова для ошибки
// Формируем текст сообщения об ошибке
sprint f ( buf, " . . . Управляющая строка с форматами . . .",
список аргументов для форматов ) ;
ErrWarnWork( 50, buf, 'е' ) ;
// !!! Два последних аргумента не нужны и их не записываем.
// Так можно делать, так как 2 последних параметра имеют
// значения по умолчанию - см. прототип

Обработка ошибок и предупреждений,

1. Ошибка открытия файла.


При возникновении данной ошибки программа прерывает ра­
боту, выдавая на экран следующее сообщение:
"Ошибка № XX. Ошибка открытия файла <имя.расширение> для чте­
ния/записи/дозаписи. "
Код ошибки и код возврата задаются в вызове функции откры­
тия файла,

2. Предупреждение о том, что файл не закрыт.


При возникновении данной ситуации программа выдает на эк­
ран следующее сообщение:
"Предупреждение № XX. Файл <имя.расширение> не закрыт. Выпол­
нение программы продолжено."
Код предупреждения задается в вызове функции закрытия
файла.

3. Неверный режим открытия файла.


При возникновении данной ошибки программа прерывает ра­
боту, выдавая на экран следующее сообщение:
"Ошибка № XX. Использован непредусмотренный режим <режим> от­
крытия файла <имя.расширение>. Используйте режимы "г", "rt",
"w", "wt", "а" или "at" (на любом регистре) ."
Код ошибки и КОД возврата задаются в вызове функции закры-

426
тия файла.

3. Неверный тип выдаваемого сообщения.


При возникновении данной ситуации программа выдает на эк­
ран^ следующее сообщение:
"Предупреждение № XX. Использован непредусмотренный <тип> вы­
даваемого сообщени вместо 'е' или 'w'. Применен режим 'w',
выполнение программы продолжено, "
Код предупреждения задается в вызове функции обработки
предупреждений и сообщений об ошибках.

4. Недопустимое значение количества строк матрицы.


При возникновении данной ситуации программа продолжает
работу, выдавая в файл результатов следующее сообщение:
"Предупреждение № XX. Из файла <имя.расширение> прочитано не­
допустимое значение количества строк матрицы, равное ... (коли­
чество строк должно лежать в диапазоне от 2 до ...) . Принимает­
ся количество строк 2 , выполнение программы продолжается."
Код предупреждения задается в вызове функции обработки
предупреждений и сообщений об ошибках.

5. Недопустимое значение количества столбцов матрицы.


При возникновении данной ситуации программа продолжает
работу, выдавая в файл результатов следующее сообщение:
"Предупреждение 1? XX. Из файла <имя.расширение> прочитано не­
допустимое значение количества столбцов матрицы, равное
(количество столбцов должно лежать в диапазоне от 2 до ...) .
Принимается количество столбцов 2, выполнение программы про­
должается. "
Код предупреждения задается в вызове функции обработки
предупреждений и сообщений об ошибках.

6. Ошибка чтения значения количества строк матрицы.


При возникновении данной ошибки программа прерывает ра­
боту, выдавая на экран следующее сообщение:
"Ошибка I? XX. Ошибка чтения значения количества строк матрицы
из файла <имя.расширение>."
Код ошибки и код возврата задаются в вызове функции обра­
ботки предупреждений и сообщений об ошибках.

7. Ошибка чтения значения количества столбцов матрицы.


При возникновении данной ошибки программа прерывает ра­
боту, выдавая на экран следующее сообщение:
"Ошибка № XX. Ошибка чтения значения количества столбцов мат­
рицы из файла <имя.расширение>."
Код ошибки и код возврата задаются в вызове функции обра­
ботки предупреждений и сообщений об ошибках.

427
8. Ошибка чтения значения элемента матрицы.
При возникновении данной ошибки программа прерывает ра­
боту, выдавая на экран следующее сообщение:
"Ошибка № XX. Ошибка чтения значения элемента матрицы с номе­
ром строки ... и номером столбца ... из файла <имя.расширение>. "
Код ошибки и код возврата задаются в вызове функции обра­
ботки предупреждений и сообщений об ошибках.

9. Преждевременный конец файла исходных данных.


При возникновении данной ситуации программа продолжает
работу, выдавая в файл результатов следующее сообщение:
"Предупреждение № XX. Файл ввода <имя .расширение> содержит
недостаточное количество данных (преждевременный конец фай­
ла) . Непрочитанные элементы матрицы инициализируются нулями.
Выполнение программы продолжается."
Код предупреждения задается в вызове функции обработки
предупреждений и сообщений об ошибках.

10. Избыточное количество данных в файле исходных данных.


При возникновении данной ситуации программа продолжает
работу, выдавая в файл результатов следующее сообщение:
"Предупреждение № XX. Файл ввода <имя .расширение > содержит
лишние данные г которые игнорируются программой. Выполнение
программы продолжается."
Код предупреждения задается в вызове функции обработки
предупреждений и сообщений об ошибках.

11. Неверное количество аргументов командной строки.


При возникновении данной ошибки программа прерывает ра­
боту, выдавая на экран следующее сообщение:
"Ошибка № XX. Использован неверный формат командной строки.
Обработка матрицы, заключающаяся в том, что в каждой
строке матрицы ищется максимальный элемент. Элементы, стояш^е
после максимального элемента, заменяются нулями и помещаются
в начало строки. Исходная и вновь полученная матрицы печата­
ются. Запуск программы выполняется с использованием командной
строки вида:
имя_выполняемого_файла . ехе файл__ввода файл_вывода . "
Код ошибки и код возврата задаются в вызове функции обра­
ботки командной строки.

П5.2. Пример оформления исходного текста программы


/*
Файл TASK02.CPP
Проект : многофайловый с функциями.

428
расположенными в отдельных файлах

Назначение : вычисление площади садового


участка Square := Length * Width

Состав проекта (файл


проекта TASK02.PRJ) : файл TASK02.СРР (главная функция
проекта);
файл TASK02_1. СРР (ввод длины и
ширины садового участка) ;
файл TASK02_2.СРР (печать длины и
ширины садового участка) ;
файл TASK02_3.СРР (вычисление
площади садового участка) ;
файл 4. СРР (печать площади
садового участка)

ЭВМ : IBM 80386

Среда программирования: ВС31 (C++)

Операционная система : DR DOS 6.О

Дата создания : 02.11.2002


Дата корректировки

Иванов И. И., ФТКг гр. 1081/4


Санкт-Петербургский государственный политехнический
университет
_V
// Стандартные включаемые файлы и прототипы функций
iinclude "task02.h"

±nt main ( void ) // Возвращает О при успехе


{
float Lenght,• // Длина садового участка
Width, // Ширина садового участка
Square; // Площадь садового участка
// Ввод длины и ширины садового участка
ReadData( Lenght, Width ) ;

// Печать длины и ширины садового участка


WriteDat( Lenght, Width ) ;

// Вычисление площади садового участка


Area ( Lenght, Width, Square ) ;

WriteRes ( Square ) ; // Печать площади садового участка

jcebvLxrn. О;
} // Конец файла TASK02.CPP

429
Файл TASK02.H
Включаемый файл для проекта TASK02.PRJ: содержит стандарт-
ные включаемые файлы и прототипы функций.

// предотвращение многократного включения данного файла


#ifndef TASK02__H
^define TASK02_H

^include <stdio.h> // Для функций ввода-вывода


^include <stdlib.h> // Для функции exit

// Прото типы функций


void. ReadData ( float &Length, float &Width );
void WriteDat( float Length, float Width );
void. Area ( float Length, float Width, float &Square )
void WriteRes( float Square );

#endif // Конец файла TASK02.H

Файл TASK02_1.CPP
Чтение исходных данных из файла TASK02.DAT. Используется в
поограммном проекте TASK02.PRJ
V
// Стандартные включаемые файлы и прототипы функций
^include "task02,h"

void ReadData(
float &Lenght, // Длина садового участка
float &Width ) // Ширина садового участка
{
FILE *f__in; // Указатель на структуру со
II сведениями о файле для чтения

// Открываем файл для ввода


±f( ( f_in = fopen( "task02.dat", "г" ; ; =- NULL )
{
printf( "\n Ошибка 10. Файл task02,dat для чтения не"
" открыт \п" ) ;
exit ( 10 ) ;
}

// Читаем данные
±f( fscanf( f__in, " %f %f", &Lenght, &Width ) != 2 )
(
printf( "\n Ошибка 20. Произошла ошибка чтения из"
" файла task02.dat \п" );
exit ( 20 );
}

// Закрываем файл для чтения

430
±f( fclose ( f_in ) == EOF )
{
printf( "\n Ошибка 30. Файл task02.dat не "
"закрыт \n" ) ;
exit ( 30 );
}

iretuirn/
// Конец файла TASK02_1.CPP

/*
Файл TASK02 2.CPP
Печать исходных данных в файл TASK02.RES. Используется в
программном проекте TASK02.PRJ

// Стандартные включаемые файлы и прототипы функций


^Include "task02.h"

void WrlteDat(
float Lenght^ // Длина садового участка
float Width ) // Ширина садового участка
(
FILE *f_out/ // Указатель на структуру со
// сведениями о файле для записи

// Открываем файл для записи и печатаем исходные данные


±f( ( f_out = fopen( "task02.res", "w" ) ) == NULL )
{
printf( "\n Ошибка 40. Файл task02.res для записи не"
" открыт \п" );
exit ( 4 0 );
}
fprlntf ( f_OUtr
"\n Вычисление площади садового участка \п"
"\п Длина садового участка: %д "
"\п Ширина садового участка: %д'\ Lenght, Width );
// Закрываем файл для вывода
if( fclose( f_out ) == EOF )
{
prlntfi "\n Ошибка 50. Файл task02.res не "
"закрыт \n" ) /
exit ( 50 ) ;
}

return/
} // Конец файла TASK02 2.GPP

Файл TASK02_3.CPP
Вычисление плош,ади прямоугольника. Используется в про-
граммном проекте TASK02.PRJ

431
// Стандартные включаемые файлы и прототипы функций
iinclude "task02.h"
void. Area (
float Lenght, // Длина садового участка
float Widths // Ширина садового участка
float & Square ) // Площадь садового участка
{

// Вычисление площади садового участка


Square = Lenght * Width;

retxum;
} // Конец файла TASK02_3.CPP
__
Файл TASK02_4.CPP
Печать результатов в файл TASK02. RES. Используется в про­
граммном проекте TASK02.PRJ
*/
/ / Стандартные включаемые файлы и прототипы функций
^include "task02.h"

void WriteRes(
float Square ) // Площадь садового участка
{
FILE *f_out; // Указатель на структуру со
// сведениями о файле для записи
// Открываем файл для дозаписи
±f( ( f_out = fopen( "task02.res", "a" ) ) == NULL )
' {
printf( "\n Ошибка 60. Файл task02.res для дозаписи"
" не открыт \п" ) ;
exit ( 60 ) ;
}

// Печать результатов работы программы


fprintf ( f_out,
"\п\п Площадь садового участка: %д", Square ) ;

// Закрываем файл
±f( fclose ( f_out ) = - EOF )
{
printf( "\n Ошибка 70. Файл task02.res не"
" закрыт \n" ) ;
exit ( 70 ) ;
}

return;
} // Конец файла TASK02 4.CPP

432
в приведенном примере рекомендуем.
1. Обратить внимание на оформление заголовочного файла и
его подключение к файлам проекта.
Повторим здесь еще раз, что обычно в заголовочный файл по­
мещают директивы ^include', прототипы функций; определения
встроенных (inline) функций; объявления (extern) данных, опреде­
ленных в другом файле; определения (const) констант; перечисления
(епит), директивы условной трансляции (#ifndef, i^endif VL др.), мак­
роопределения (Udeflne), именованные пространства имен (name­
space), определения типов (class, struct), объявления и определения
шаблонов (template).
Заголовочные файлы никогда не должны содержать определе­
ния невстроенных функций, определения данных (объектов), опре­
деления массивов и неименованные пространства имен.
2. Обратить внимание на оформление функций и, особенно, на
оформление заголовка функции.
3. Обратить внимание на оформление заголовочных коммента­
риев файлов программного проекта.
Приложение П.б. Примерная программа дисциплины
"Программирование и основы алгоритмизации".
МИНИСТЕРСТВО ОБРАЗОВАНИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ

УТВЕРЖДАЮ
Начальник Управления образовательных программ
и стандартов высшего и среднего профессиональ­
ного образования

2000 г.

ПРИМЕРНАЯ ПРОГРАММА д и с ц и п л и н ы

'Программирование и основы алгоритмизации"

Рекомендуется Минобразованием России для подготовки бакалавров по на­


правлению 5502 "Автоматизация и управление" (и подготовки специалистов по
направлению 6519 "Автоматизация и управление")

434
1. Цели и задачи дисциплины
Цель дисциплины состоит в поэтапном формировании у студентов следую­
щих слоев знаний и умений,
• Слой 1: знание основных понятий программирования.
• Слой 2: знание базового языка программирования.
• Слой 3: умение решать задачи на вычислительных машинах ВМ.
Формированию отмеченных уровней (слоев) знаний и умений соответствуют
разделы дисциплины. Изучение курса предполагает, что студенты знакомы с прин­
ципами работы ВМ, десятичной, двоичной, восьмеричной и шестнадцатеричной
системами счисления, а также основными понятиями информатики.

2. Требования к уровню освоения содержания дисциплины


В результате изучения дисциплины студенты должны:
1. Знать основные понятия программирования.
2. Знать базовый язык программирования.
3. Уметь решать задачи на ВМ.
Примечание. Слой 3 в полном объеме и слой 4 знаний и умений -умение про­
ектировать программное обеспечение — формируются у студентов в процессе изуче­
ния дополнительной дисциплины "Технология программирования", которая являет­
ся органическим продолжением данной.

3. Объем дисциплины и виды учебной работы


Вид учебной работы Всего часов Семестр
Общая трудоемкость дисциплины 130 2
Аудиторные занятия 68 2
Лекции 34 2
Практические занятия (упражнения) 17 2
Курсовая работа 17 2
Самостоятельная работа 62 2
Вид итогового контроля (зачет, экзамен) Экзамен 2

4. Содержание дисциплины
4.L Разделы дисциплины и виды занятий
iN» п/п Раздел дисциплины Лекции ПЗ ЛР
(упражнения) (курс, рабо­
та)
1 Основные понятия профаммрфования *•
2 Си/С+-1-: базовый язык программирования (алфавит, * *
синтаксис, типы данных, выражения и операции,
структурное программирование, операторы ветвлений
и циклов, массивы и структуры, модальное програм­
мирование, функции, ввод и вывод, описатели класса
хранения, области действия и время жизни данных,
указатели, препроцессор)
3 Прикладное профаммирование (сортировки массивов, •
*
рекурсия и итерация, транспортная задача, элементы
обработки списков, работа с динамической памятью)

435
4,2. Содермсание разделов дисциплины
Раздел 1. Основные понятия программирования
(лекции и самостоятельная работа: 16 часов)
1. Алгоритм, данные, программа, структура данных.
2. Регистр: разрядность, графическое изображение. Совокупность регистров как ос­
нова оперативного запоминающего устройства (ОЗУ). Адрес, хранимое слово.
Работа ОЗУ в режимах чтения и записи.
3. Простейший набор арифметических операций. Выполнение простейших арифме­
тических операций.
4. Работа ВМ при последовательном выполнении команд. Ветвления в программах.
Команды условных переходов и безусловной передачи управления.
5. Прямая и косвенная адресация. Зачем нужна косвенная адресация?
6. Классификация и краткая характеристика языков программирования. Машинно-
зависимые язьпси: машинные (О GL - О Generation of the Language), ассемблеры (1
GL) и макроассемблеры (2 GL). Машинно-независимые языки: процедурные (3
GL), проблемные (4 GL) и универсальные (5 GL).
7. Введение в языки ассемблера (1 GL). Операторы языка ассемблера ВМ с "очень
простой" архитектурой. Символические команды. Псевдокоманды. Схема
трансляции в два прохода.
8. Периферийные устройства ВМ и их разновидности. Накопители на магнитных
дисках (НМД) с жёсткими дисками. Прямой и последовательный доступ к ин­
формации. Монитор, работа в текстовом и графическом режимах. Клавиатура и
мышь. Принтеры. Взаимодействие программ с ПУ.
9. Программные продукты. Основные виды, этапы проектирования и жизненный
цикл.

Раздел 2. Си/С++: базовый язык программирования


(лекции, практические занятия и самостоятельная работа: 80 часов)

Цель этой части курса - овладение подмножеством языка C++, изобразитель­


ные средства которого обеспечиваются большинством языков третьего поколения (3
GL).

\. Программирование на языках высокого уровня (на примере Си/С++). Язык Си:


история, первоначальная область применения (системное программирование).
Принцип построения: компилируемые конструкции и интерпретируемые средст­
ва (библиотека стандартных функций). Раздельная трансляция, компилятор и ре­
дактор связей.
2. Алфавит языка. Способы описания синтаксиса языка: металингвистические фор­
мулы и синтаксические диаграммы. Определение понятия "идентификатор".
Служебные слова. Комментарии.
3. Типы данных. Имена и объявления. Целые типы данных: int, short, long, char.
Кодовый формат. Описание данных, литералы. Тип целых констант. Арифмети­
ческие операции для целых операндов.
4. Плавающие типы данных: float, double. Кодовый формат. Описание данных, ли­
тералы. Тип констант с плавающей точкой. Арифметические операции для веще­
ственных операндов. Математические функции стандартной библиотеки Си. На­
значение стандартных заголовочных файлов. Компоновка программы из объект­
ных модулей и библиотек.
5. Понятие преобразования данных. Зачем нужны преобразования? Примеры пре­
образований. Явное преобразование типа. Правила преобразования операндов в

436
процессе вычислений.
6. Оператор-выражение. Операции уменьшения и увеличения, префиксная и пост­
фиксная форма. Операции простого и составного присваивания. Приоритеты опе­
раций.
7. Оператор-выражение. Операции отношения. Результат вычисления отношений.
Представление булевских значений "ложь", "истина" в Си. Логические операции:
!, II, &&. Операции простого и составного присваивания. Приоритеты операций.
8. Структурное программирование. Операторы ветвления: if и switch. Оператор if,
синтаксис, выполнение оператора. Операции отношения. Составной оператор.
Сложные условия и логические операции: !, ||, &&. Вложение операторов if, опе­
ратор if... else if... else. Оператор switch, выполнение, использование оператора
break.
9. Структурное программирование. Операторы цикла с предусловием (while) и по­
стусловием (do-while). Примеры применения циклов. Изменение хода выполне­
ния цикла с помощью операторов break и continue.
10. Одномерные массивы, пример использования. Связь массивов и указателей. Об­
ращение к элементу массива, адресная арифметика. Структурное программиро­
вание. Оператор цикла с шагом: for. Строки, кодовый формат, строковые литера­
лы. Двумерные массивы. Функция индексации для двумерного массива.
11. Структуры, описание, пример использования. Операция выбора элемента струк­
туры. Операции над структурой в целом. Структуры как аргументы функций.
Объявление типа: typedef. Размещение структур в памяти.
12. Модульное программирование. Функции. Рациональные размер и количество па­
раметров функции. Пример функции. Аргументы и параметры. Передача аргу­
ментов по значению и по ссылке. Прототипы функций. Преобразование аргумен­
тов в точке вызова. Оператор return.
13. Ввод и вывод (передача) с преобразованиями. Понятие набора данных и файла.
Открытие и закрытие потоков. Функции передачи: fscanf и fprintf. Управляющая
строка, форматы при вводе и при выводе. Передача без преобразований (в кодо­
вых форматах).
14. Время жизни и способ размещения данных. Спецификация класса памяти. Ста­
тический способ размещения. Спецификаторы класса памяти extern и static. Ди­
намический процесс исполнения программы и концепция памяти auto / register.
Данные с управляемым способом размещения, операции new и delete. Инициали­
зация данных.
15. Объявления и определения. Область действия описаний. Структура программы
на языке Си. Переобъявления во вложенных блоках. Определения и объявления
на внешнем уровне. Определения и объявления на внутреннем уровне.
16. Указатели, адресная арифметика, указатели и массивы. Введение в обработку
списков. Создание и уничтожение узлов списка, вставка и исключение узлов спи­
ска.
17. Препроцессор Си. Директива препроцессора #define. Терминология: макроопре­
деление, макрообращение, макрорасширение. Макросы с параметрами, аналогия
с функциями. Директива препроцессора #include. Директивы условной компиля­
ции.
По материалу разд. 2 в рамках практических занятий и самостоятельной рабо­
ты выполняются следующие практические работы.
Проверочные работы по следующим темам (каждая длительностью 20-30 ми­
нут):
- ввод-вывод с использованием функций fscanf - fprintf;

437
- ветвления и циклы;
- структуры;
- функции;
- области действия и время жизни объектов;
- указатели;
- операции с линейным списком.
Простой программный проект, предусматривающий решение трех элементар­
ных задач с использованием языка C++ (однофайловые программы с одной функци­
ей):
- программирование формулы:
- программирование ветвления;
- программирование цикла.
В процессе выполнения программного проекта изучаются программная документа­
ция:
- единая система программной документации (ЕСПД);
- состав ЕСПД, назначение и содержание программных документов (пять из них -
"Техническое задание", "Текст программы", "Описание программы", "Программа и
методика испытаний" и схема программы - используются при оформлении про­
граммного проекта).
Раздел 3, Прикладное программирование
(лекции, практические занятия и самостоятельная работа: 34 часа)
Цель этого раздела - овладение основами науки программирования, а также
профессиональным стилем программирования на Си/С++.
Выделено шесть часто встречающихся в приложениях типов задач: сортиров­
ка массивов, рекурсивные методы, обработка списков, работа с таблицами, работа с
файлами, обработка текстов. Решение задач включает алгоритмизацию и програм­
мирование. Умение решать эти задачи составляет основу профессиональной квали­
фикации любого программиста.
Профессиональный стиль программирования подразумевает разработку про­
стых и понятных исходных текстов программ. Важное внимание уделяется выбору
наиболее подходящих в каждом случае изобразительных средств языка и оформле­
нию программ с учётом особенностей психики человека.
Рассматриваемые ниже задачи демонстрируют использование при проектиро­
вании программного продукта иерархической декомпозиции задачи.
1. Сортировка: виды, терминология, обозначения. Простые алгоритмы сортировки
(выбором, вставками, обменом). Разработка функций, оценка производительно­
сти.
2. Сортировка сложным выбором: с помощью двоичного дерева. Идея, пример ра­
боты, разработка функции, оценка производительности.
3. Сортировка сложными вставками: метод Шелла. Идея, пример работы, разработ­
ка функции, оценка производительности.
4. Сортировка сложным обменом: быстрая сортировка Хоора (нерекурсивный вари­
ант). Идея, пример работы, разработка функции, оценка производительности.
5. Рекурсия и итерация. Рекурсия как метод вычислений. Рекурсивный вариант бы­
строй сортировки Хоора. Когда не следует использовать рекурсию? Поиск пути
минимального суммарного веса во взвешенном неориентированном графе.
6. Элементы обработки списков. Инвертирование списка ссылок в задаче поиска
пути минимального суммарного веса во взвешенном неориентированном графе.
7. Сортировка массива сложным обменом: быстрая сортировка Хоора (рекурсив-

438
ный вариант).
По материалу разд. 3 в рамках практических занятий и самостоятельной работы
выполняется более сложный программный проект, реализующий решение некоторой
содержательной задачи. Проект содержит несколько файлов с исходным текстом и
несколько функций, расположенных в этих файлах. Большое внимание в проекте
уделяется методике тестирования спроектированного программного продукта.
Как указывалось выше, для более полной подготовки в области программиро­
вания требуется формирование дополнительных знаний и умений, включая даль­
нейшее развитие слоя 3 и освоение дополнительного слоя 4: умение проектировать
программное обеспечение. Достижению этой цели способствует последующий курс
"Технология программирования".
5. Лабораторный практикум (не предусматривается)
6. Учебно-методическое обеспечение дисциплины
6.7. Рекомендуемая литература

Основная литература:
1. Трои Д. Программирование на языке Си для персонального компьютера IBM PC:
Пер. с англ. - М.: Радио и связь, 1991.
2. Уэйт М., Прата С, Мартин Д. Язык Си: Пер. с англ. - М.: Мир, 1988.
3. Керниган Б., Ритчи Д., Фъюер А. Язык программирования Си: Задачи по языку
Си: Пер. с англ. - М.: Финансы и статистика, 1985.
4. Давыдов В.Г. Теория и технология программирования. Конспект лекций. Ч. 1. ~-
Санкт-Петербургский государственный технический университет, СПб.: 2000.
5. Давыдов В.Г. Теория и технология программирования. Конспект лекций. Ч. 2. -
Санкт-Петербургский государственный технический университет, СПб.: 2001.
Дополнительная литература:
1. Рассохин Д. От Си к Си++. М.: Издательство "ЭДЕЛЬ", 1993.
2. От Си к Си++ / Е.И. Козелл, Л.М. Романовская, Т.В. Русс и др. М.: Финансы и
статистика, 1993.
3. Эллис М., Строуструп Б. Справочное руководство по языку программирования
C++ с комментариями: Пер. с англ. - М.: Мир, 1992.
4. Бруно Б. Просто и ясно о C++: Пер. с англ. - М.: БИНОМ, 1995.
5. Пол Ирэ. Объектно-ориентированное программирование с использованием C++:
Пер. с англ. / Ирэ Пол. - К.: НИПФ "ДиаСофт Лтд", 1995.
6. Ю. Тихомиров. Visual C++ 6 - СПб.: БХВ - Санкт-Петербург, 1999.
7. Давыдов В.Г., Пекарев М.Ф. Учебный машинно-ориентированный язык (ПМ-
ассемблер): Учебное пособие. Санкт-Петербургский государственный техниче­
ский университет, СПб.: 2000.
6.2, Средства обеспечения освоения дисциплины
Любая интегрированная среда программирования языка C++ (желательно бо­
лее простая, чтобы сосредоточить внимание на вопросах программирования). Выби­
рается по усмотрению вуза.

439
7. Материально-техническое обеспечение дисциплины
Компьютерный класс, оснащенный ПК не ниже Pentium 75 МГц с ОС
Windows 95 и выше или Windows NT.
8. Методические рекомендации по организации изучения
дисциплины
Использование специального методического обеспечения не предусмот­
рено.

Программа составлена в соответствии с Государственным образователь­


ным стандартом высшего профессионального образования по направлению
5502 "Автоматизация и управление" подготовки бакалавров и по направлению
6519 "Автоматизация и управление" подготовки специалистов.

Программу составили:

Лекарев Михаил Федорович, д.т.н., профессор, СПбГТУ (ЛПИ)


Давыдов Владимир Григорьевич, к.т.н., доцент, СПбГТУ (ЛПИ)

Программа одобрена на заседании учебно-методического совета по на­


правлению 5502 "Автоматизация и управление" 14.11.2000, протокол №

Председатель Совета УМО


Приложение П.7. Прилагаемый компакт-диск.

На прилагаемом компакт-диске содержатся исходные тексты


всех примеров программ. Имеются также и исполняемые файлы
этих программ, так что Вам не надо обязательно компилировать за­
интересовавшие Вас примеры. Все программные проекты примеров
"самодостаточны". Это означает, что ни одному из них не требуются
файлы других проектов.
Кроме исходных текстов примеров программ на компакт-диске
имеются:
• описание ПМ-ассемблера в формате текстового редактора Word
2000;
• интегрированная среда программирования ПМ-ассемблера (рабо­
тает как DOS-приложение);
• файл с полным текстом приложений к учебному пособию в фор­
мате текстового редактора Word 2000 и др.
Более полные сведения о содержимом компакт-диска и работе
с этой информацией имеется в файле ReadMe, расположенном в
корневой папке компакт диска.
Обратите внимание, что пользование данной книгой возможно
и без компакт-диска, но его наличие обеспечит Вам большие удоб­
ства и дополнительный сервис. В частности, без компакт-диска Вы
не будете располагать описанием ПМ-ассемблера, специально раз­
работанного для использования в учебном процессе, и его интегри­
рованной средой программирования.
ЛИТЕРАТУРА

1. Давыдов В.Г., Пекарев М.Ф. Учебный машинно-


ориентированный язык (ПМ-ассемблер): Учебное пособие. - СПб.:
Санкт-Петербургский государственный политехнический универси­
тет, 2002.
2. Пекарев М.Ф. Модули с двумя выходами в программных
проектах. - СПб.: СПбГТУ, 2000.
3. Рассохин Д. От Си к C++. - М.: Издательство "ЭДЕЛЬ",
1993.
4. От Си к C++ / Е.И. Козелл, Л.М. Романовская, Т.В. Русс и
др. - М.: Финансы и статистика, 1993.
5. C/C++. Программирование на языке высокого уровня / Т.А.
Павловская. - СПб.: Питер, 2001.
6. Давыдов В.Г. Теория и технология программирования: Кон­
спект лекций. Ч. 2. СПб: Издательство СПбГПУ, 2001.
СОДЕРЖАНИЕ
ПРЕДИСЛОВИЕ 3
1. ВВЕДЕНИЕ 5
1.1. Системы счисления 5
1.2. Классификация языков программирования и их
краткая характеристика 8
1.2.1. Машинные языки 9
1.2.2. Ассемблерные языки (на примере
ПМ-ассемблера) 12
1.2.3. Макроассемблерные языки 12
1.2.4. Машинно-независимые языки 14
ЧАСТЬ 1. БАЗОВЫЙ ЯЗЫК 16
2. ЯЗЫК ПРОГРАММИРОВАНИЯ ВЫСОКОГО
УРОВНЯ C++ 16
2.1. Введение. Структурное и модульное
программирование 17
2.1.1.Алгоритм и способы его записи 17
2.1.2. Структурное и модульное программирование 21
2.2. Язык программирования и его описание 27
2.3. Структура и конструкция программы на Си/С++ 31
2.3.1. Комментарии 31
2.3.2. Идентификаторы 32
2.3.3. Слуэюебные слова 32
2.3.4. Константы 33
2.3.5. Структура Си-программы 38
2.4. Простой ввод-вывод в языках Си/С++ 41
2.4.1. Ввод-вывод потока 41
2.4.2. Ввод с использованием функций scanf-fscanf 43
2.4.3. Вывод с использованием функций printf-fprintf 49
2.4.4. Упразднения для самопроверки 54
3. ТИПЫ ДАННЫХ И ИХ АТРИБУТЫ 56
3.1. Имена 56
3.2. Типы данных 57
3.3. Класс хранения: область действия и время жизни 60
3.4. Внешние и внешние статические данные 61
3.5. Функции 68
3.6. Автоматические, регистровые и внутренние
статические данные 74
3.7. Инициализация данных 78
3.8. Упражнения для самопроверки 79

443
3.9. Производные типы данных 81
3.9.1. Массивы 81
3.9.2. Массивы — как аргументы функций 86
3.9.3. Упраэюнения для самопроверки 88
3.9.4. Структуры 88
3.9.5. Структуры в качестве аргументов функций 91
3.9.6. Упраэюнения для самопроверки 93
4. ОПЕРАТОРЫ И УПРАВЛЕНИЕ ИХ ИСПОЛНЕНИЕМ .... 95
4.1. Пустой оператор 95
4.2. Операторы-выражения 95
4.3. Операторы break и continue 96
4.4. Блок операторов 96
4.5. Оператор return 97
4.6. Оператор if 97
4.7. Оператор switch 100
4.8. Оператор while 101
4.9. Оператор do-while 104
4.10. Оператор for 105
4.11. Оператор goto и метки операторов 108
4.12. Упражнения для самопроверки 108
5. ВЫРАЖЕНИЯ И ОПЕРАЦИИ 111
5.1. Операции ссылки 113
5.2. Унарные операции 116
5.3. Бинарные операции 118
5.4. Тернарная операция 121
5.5. Операция присваивания 121
5.6. Операция "запятая" 122
6. У К А З А Т Е Л И 123
6.1. Зачем нужны указатели? 123
6.2. Указатели и их связь с массивами и строками 123
6.3. Определение и применение указателей 124
6.4. Указатели на структуры 128
6.5. Использование указателей в качестве аргументов
функций 129
6.6. Указатель как значение, возвращаемое функцией 133
6.7. Массивы указателей 134
6.8. Замена типов указателей 137
6.9. Упражнения для самопроверки 141
7. ПОЛЯ БИТОВ И ПОБИТОВЫЕ ОПЕРАЦИИ 143
7.1. Поля битов 143
7.2. Побитовые операции 144

444
8. ДИНАМИЧЕСКОЕ РАЗМЕЩЕНИЕ ОБЪЕКТОВ В
ПАМЯТИ. ОДНОНАПРАВЛЕННЫЙ НЕКОЛЬЦЕВОЙ
ЛИНЕЙНЫЙ СПИСОК И ОПЕРАЦИИ С НИМ 148
8.1. Понятие об однонаправленном линейном списке.
Динамическое размещение объектов в памяти 148
8.2. Инициализация линейного списка 152
8.3. Добавление элемента в начало списка 163
8.4. Добавление элемента в конец списка 163
8.5. Создание ЛС с первым занесенным элементом
в начале 164
8.6. Создание ЛС с первым занесенным элементом
в конце списка 164
8.7. Удаление элемента из начала списка 165
8.8. Удаление элемента из конца списка 166
8.9. Разрушение ЛС с освобождением занятой им
динамической памяти 166
8.10. Печать содержимого ЛС 167
8.11. Добавление элемента после каждого элемента ЛС,
содержащего заданное значение 167
8.12. Добавление элемента перед каждым элементом ЛС,
содержащим заданное значение 168
8.13. Удаление элемента после каждого элемента ЛС,
содержащего заданное значение 168
8.14. Удаление элемента перед каждым элементом ЛС,
содержащим заданное значение 170
8.15. Зачем нужен линейный список 171
8.16. Упражнения для самопроверки 172
9. ПРЕПРОЦЕССОР ЯЗЫКА СИ/С++ 173
9.1. Директивы препроцессора 173
9.2. Подстановка имен 173
9.3. Включение файлов 177
9.4. Условная компиляция 178
9.5. Указания по работе с препроцессором 180
10. РЕДКО ИСПОЛЬЗУЕМЫЕ СРЕДСТВА
ЯЗЫКОВ СИ/С++ 182
10.1. Объявление имени типа typedef 182
10.2. Объекты перечислимого типа 183
10.3. Объединения 186
11. МОДЕЛИ ПАМЯТИ 189
11.1. Адресация near, far и huge 190
11.2. Стандартные модели памяти для
шестнадцатибитной среды DOS 193

445
11.3. Изменение размера указателей в
стандартных моделях памяти для
шестнадцатибитной среды DOS 194
11.4. Макроопределения для работы с указателями 195
11.5. Работа с памятью для среды WINDOWS 196
12. НОВЫЕ ВОЗМОЖНОСТИ ЯЗЫКА C++, НЕ
СВЯЗАННЫЕ С ОБЪЕКТНО-ОРИЕНТИРОВАННЫМ
ПРОГРАММИРОВАНИЕМ 197
12.1. Прототипы функций. Аргументы по умолчанию 198
12.2. Доступ к глобальным переменным, скрытым
локальными переменными с тем же именем 199
12.3. Модификаторы const и volatile 200
12.4. Ссылки 201
12.5. Подставляемые функции 202
12.6. Операции динамического распределения памяти 202
12.7. Перегрузка функций 203
12.8. Шаблоны функций 205
12.9. Перегрузка операций 206
13. ТЕХНОЛОГИЯ СОЗДАНИЯ ПРОГРАММ [5] 208
13.1. Кодирование и документирование программы 208
13.2. Проектирование и тестирование программы [5] 212
13.2.1. Этап 1: постановка задачи 213
13.2.2. Этап 2: разработка внутренних
структур данных 214
13.2.3. Этап 3: проектирование структуры
программы и взаимодействия модулей 214
13.2.4. Этап 4: структурное программирование 215
13.2.5. Этап 5: нисходящее тестирование 216
ЧАСТЬ 2. ПРИКЛАДНОЕ ПРОГРАММИРОВАНИЕ 218
14. ДИНАМИЧЕСКИЕ СТРУКТУРЫ ДАННЫХ [5] 218
14.1. Линейные списки 219
14.2. Бинарные деревья 220
14.3. Очереди и их частные разновидности 221
14.4. Реализация динамических структур с
помощью массивов 222
15. СОРТИРОВКА 224
15.1. Сортировка массивов 226
15.2. Сортировка массива простым выбором 228
15.3. Сортировка массива простыми включениями 248
15.4. Сортировка массива простым обменом
(метод "пузырька") 250
15.5. Выводы по простым методам сортировки 251

446
15.6. Сортировка массива сложным выбором
(с помощью двоичного дерева) 252
15.7. Сложная сортировка вставками (сортировка Шелла) ... 257
15.8. Сложная сортировка обменом (сортировка Хоора) 259
15.9. Сравнительные показатели производительности
различных методов сортировки массивов 264
16. ГРАФЫ. ТРАНСПОРТНАЯ ЗАДАЧА (ЗАДАЧА
КОММИВОЯЖЕРА) 266
16.1. Терминология 266
16.2. Формы задания графа 268
16.3. Почему для решения задачи подходит
рекурсивный алгоритм 269
16.4. Представление кратчайшего пути до каждой
вершины 270
16.5. Как найти минимальный путь 271
16.5.1. Требуется ли полный перебор путей 271
16.5.2. Организация перебора путей 271
16.6. Пример поиска минимального пути в графе 285
16.7. Печать информации о наилучшем пути 286
17. ПОИСК 288
17.1. Постановка задачи внутреннего поиска 288
17.2. Последовательный поиск 290
17.3. Логарифмический (бинарный) поиск 305
17.4. Поиск с использованием перемешанной
таблицы (хэш-таблицы) 308
18. ОТВЕТЫ И РЕШЕНИЯ К УПРАЖНЕНИЯМ
ДЛЯ САМОПРОВЕРКИ 312
18.1. Для подраздела 2.4.4 312
18.2. Для подраздела 3.8 314
18.3. Для подраздела 3.9.3 315
18.4. Для подраздела 3.9.6 317
18.5. Для подраздела 4.12 319
18.6. Для подраздела 6.9 321
18.7. Для подраздела 8.16 321
ПРИЛОЖЕНИЯ 327
Приложение П. 1. Тесты и программные проекты.
Варианты заданий 327
П. 1.1. Тесты (контрольные работы) 327
П. 1.1.1. Программирование на ПМ-ассемблере.
Варианты тестов 327
П.1.1.2. Ввод в языках Си/С++. Варианты тестов 330
П. 1.1.3. Вывод в языках Си/С++. Варианты тестов 337

447
п. 1.1.4. Простейшие ветвления. Варианты тестов 342
П.1.1.5. Циклы. Варианты тестов 345
П. 1.1.6. Структуры. Варианты тестов 350
П.1.1. 7. Функции. Варианты тестов 359
П. 1.1.8. Области действия определений. Варианты тестов 361
П. 1.1.9. Массивы и указатели. Варианты тестов 373
П. 1.1.10. Операции над линейным списком. Работа
с динамической памятью. Варианты тестов 379
П.1.1.11. Препроцессор, перечисления, функции с умалчиваемыми
значениями аргументов, перегрузка функций, шаблоны
функций, перегрузка операций. Варианты тестов 388
П.1.2. Программные проекты 392
П. 1.2.1. Программирование на ПМ-ассемблере. Варианты
программных проектов 393
П. 1.2.2. Структурное программирование средствами языков
Си/С-^+ . Варианты программных проектов 395
П. 1.2.3. Средства модульного программирования в языке C++.
Варианты программных проектов 402
П.1.3. Экзаменационное тестирование 405
Приложение П.2. Создание программного проекта 408
П.2.1. IDE MS Visual Studio C++ 6.0.
Создание программного проекта 408
П.2.2. IDE Borland C++ 3.1.
Создание программного проекта 411
Приложение П.З. Рекомендации по структуре
однофайловой программы с одной функцией и пример
оформления исходного текста 412
Приложение П.4. Методика отладки программы 414
П.4.1. Компиляция и компоновка программного
проекта. Устранение синтаксических огиибок 415
П.4.2. Отладка программного проекта. Устранение
логических (алгоритмических) огиибок 417
77.4.3. Тестирование программного проекта 420
Приложение П. 5. Рекомендации по созданию
многофайлового программного проекта с несколькими
функциями и пример оформления исходного текста 421
77.5.7. Спецификация программного проекта 421
П. 5.2. Пример оформления исходного текста
программы 428
Приложение П.6. Примерная программа дисциплины
"Программирование и основы алгоритмизации" 434
Приложение П.7. Прилагаемый компакт-диск 441
ЛИТЕРАТУРА 442

448

Вам также может понравиться