Davydov G.v. Programmirovanie i Osnovy Algoritmizacii (2003)
Transcript of Davydov G.v. Programmirovanie i Osnovy Algoritmizacii (2003)
В.Г. Давыдов
Программирование и основы
алгоритмизации
Рекомендовано УМО по образованию в области радиотехники, электроники и биомедицинской техники и автоматизации
в качестве учебного пособия для студентов высших учебных заведений, обучающихся по специальности
«Управление и информатика в технических системах»
Москва «Высшая школа» 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: [email protected] http://www.v-shkola.ru Отдел реализации: (095) 200-07-69, 200-59-39, факс: (095) 200-03-01. E-mail: [email protected]
Отдел «книга-почтой»: (095) 200-33-36. E-mail: [email protected] Отпечатано в ФГУП ордена «Знак Почета» Смоленской областной типографии им. В.И. Смирнова.
214000, г. Смоленск, пр-т им. Ю. Гагарина, 2.
I S B N 5 - 0 6 - 0 0 4 4 3 2 - 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 академических часов занятий. В рамках такой траектории, по усмотрению преподавателя, не изучается только часть материала, пропущенного в предыдущей траектории.
Ваши отзывы об учебном пособии, конструктивные замечания и критику направляйте по адресу
Автор
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 1 2 3 4 5 6 7
Двоичная триада 000 001 010 011 100 101 по 111
Шестнадцатеричная цифра 0 1 2 3 4 5 6 7 8 9 А В С D Е F
Двоичная тетрада 0000 0001 0010 ООН 0100 0101 оно 0111 1000 1001 1010 1011 1100 1101 1110 1111
Примеры перевода чисел из одной системы счисления в другую:
1. 42)=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 з аменяем полученные тетрады шест-н а д ц а т е р и ч н ы м и цифрами и получаем
^ 1 6 ) = - ^ ' ^ ^ ( 1 6 )
3 . 4,6)-^^1,^^06) 4 2 ) = ? в соответствии с табл . 1 з аменяем шестнадцатеричные ц и ф р ы
тетрадами 1010 1111 0001 , 1 1 1 1 1110
и получаем ^2) =101011110001,1111111(2)
4. 48)=710,526(,,) 4 2 ) = ? Этот пример предлагаем выполнить самостоятельно . В о з м о ж е н быстрый и простой перевод чисел из восьмеричной
системы в шестнадцатеричную систему счисления и обратно . Такой перевод м о ж н о выполнить в два этапа - сначала из исходной системы счисления в д в о и ч н у ю систему и затем из двоичной в новую систему счисления . П р и м е р ы такого рода также предлагаем в ы п о л н и т ь самостоятельно .
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)
Имя переменной
А В С
"1,5" Е
Табл. 3. Таблица символов (ТС) Адрес в ОЗУ (для удобства используется десятичный адрес
вместо двоичного адреса) 00000 00001 00002 00003 00004
2. Распределить память для команд программы. Н а п р и м е р , условимся размещать программу, начиная с адреса 01000 (для удобства используется десятичный адрес вместо двоичного адреса) .
3. Составить собственно программу, т.е. записать коды машинных команд с указанием места их размещения в ОЗУ. Напоминаем, что реально в Э В М используются двоичные коды команд и
10
адресов, но мы для собственного удобства воспользуемся и здесь десятичными кодами. Машинная программа приведена в табл. 4.
Проанализировав содержимое табл. 3, оценим возможности и особенности программирования на машинных языках.
В оперативном запоминающем устройстве (ОЗУ) между местом размещения данных и команд программы осталась "дырка" -ячейки с адресами 00005...00999, которую для полного использования памяти нужно ликвидировать. Однако заранее оценить размер памяти, необходимой для размещения данных и команд, нельзя или, по крайней мере, очень трудно.
1 Адрес команды
01000 01001 01002 01003 01004 01005 01006
01007
01008
01009
01010
01011
01012
01013
КОП
07 08 07 08 07 08 01
06
02
01
05
03
02
08
Табл. Адрес
операнда 00000 00000 00001 00001 00002 00002 00002
00003
00004
00000
00001
00004
00004
00004
4. Машинная программа Действие команды
УВв -> ОЗУ по адресу 000000 (А) Содержимое ячейки ОЗУ с адресом 00000 (А) - ^ УВыв УВв -> ОЗУ по адресу 000001 (В) Содержимое ячейки ОЗУ с адресом 00001 (В) —> УВыв УВв -> ОЗУ по адресу 000002 (С) Содержимое ячейки ОЗУ с адресом 00002 (С) —> УВыв Содержимое ячейки ОЗУ с адресом 00002 (С) -^ регистр А АЛУ Содержимое регистра А АЛУ (С) разделить на содержимое ячейки ОЗУ с адресом 00003 (1.5) и результат поместить в регистр А АЛУ Содержимое регистра А АЛУ (С/1.5) - ^ ОЗУ по адресу 00004 (Е) Содержимое ячейки ОЗУ с адресом 00000 (А) —> регистр А АЛУ Содержимое регистра А АЛУ (А) умножить на содержимое ячейки ОЗУ с адресом 00001 (В) и результат поместить в регистр А АЛУ Содержимое регистра А АЛУ (А*В) сложить с содержимым ячейки ОЗУ с адресом 00004 (С/1.5) и результат поместить в регистр А АЛУ Содержимое регистра А АЛУ (А*В+С/1.5) -^ ОЗУ по адресу 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 ОПЕРАЦИЯ
к Стандартное начало К придумывает прог
MACRO ЧТЕНИЕ * ЗАПИСЬ ЧТЕНИЕ * + / / ЗАПИСЬ MEND
21 ОПЕРАНД , MACRO -раммист SUB(X, Y, X X Х2 Y Y Х2 X Y R
40
ключевое
R)
КОММЕНТАРИЙ слоъо, SUB(X, Y, R) -
Тело
макроопределения Стандартный конец
Пусть, например, необходимо вычислить С = (А^+В')/(А-В) и G-={E^+F^)/(E-F)
Это можно сделать в программе с помощью однострочных макровызовов:
13
2 10 МЕТКА
11 20 ОПЕРАЦИЯ
21 40 ОПЕРАНД
41 КОММЕНТАРИЙ
S U B ( A , В , С) S U B ( 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, printf( " %f %f %f". A, D=A*B+C/2.0; printf( "\n %f", D ) ; return 0;
&B, &C ) ; B, С ) ;
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 switch
Как его записать?
Дейкстра
Циклы while 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
а) Точка входа или выхода
Операционный блок (функциональный узел)
Решающий блок (предикатный узел)
Циклическая конструкция
б) Поток управления
Цикл «Уcл4>^ Не «Усл4»
s7
Цикл «Усл4» \ /
s8
f Конец J
( )
ГЦикл «Усл2» Не «Усл2»
< ^ У с л З ^
s4
1 s6
1 Цикл «Усл2»
Нет
Да
s5
Рис. 3. Схема алгоритма: а) основные обозначения; б) пример использования
а) Программа
Простая конструкция
Условная конструкция
Циклическая конструкция
Заголовок программы Тело программы
Оператор
Условие Тело цикла
б)
т
Усл4
s2
S3
Пример программы s1
Усл1
Усл2
s7
s8
т \ s4
F
УслЗ / ^
s5
s6
Рис. 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; I += 1;
while( I <= 20 ) ;
// Эквивалентно SUMMA = SUMMA + J ,
SUMMA = 1 + 2 + ... + 20
SUMMA:=0; I := 1;
SUMMA := SUMMA + I; I : = ! + 1;
ТЕЛО-ЦИКЛА («ДЕЛАТЬ»)
(«ПОКА»)
УСЛОВИЕ
Нет («ВСЕ»)
Вариант по ГОСТ
SUMMA:=0; I := 1;
Цикл «I» I > 2 0
SUMMA := S U M M A + 1 ; I := 1 + 1;
Цикл «I»
Рис. 7. Цикл с предусловием (while)
Наряду с методологией структурного программирования, хороший стиль программирования рекомендует также обязательное использование методологии модульного программирования. Модульное программирование предполагает последовательную декомпозицию (разбиение) исходной задачи на функционально закончен-
25
ные подзадачи, оформленные в виде отдельных модулей, которые в языках Си/С-ь+ называются функциями.
Вариант по ГОСТ
SUMMA := 0;
Цикл «I» I > 2 0
SUMMA := SUMMA + I
Цикл «I»
1, 20, +1 - начальное, конечное значения "1" и шаг изменения "1"
Рис. 8. Цикл с предусловием {for)
Для определения рационального размера функции и количества ее параметров можно использовать "правило семь ± два". Смысл этого правила заключается в том, что человек хорошо воспринимает до семи некоторых элементов - параметров функции, операторов языка программирования и т.п. Таким образом, при хорошо выполненной декомпозиции размер функции не превосходит обычно 2 5 - 8 1 строк текста, а количество параметров не превышает 5 - 9 . Размер функции 2 5 - 8 1 строк текста получается, если в ее блоке содержится не более 5 - 9 элементарных конструкций, каждая из которых занимает не более 5 - 9 строк. Модули (функции Си) можно хранить в отдельных файлах, отлаживать параллельно, что способствует сокращению сроков проектирования программных проектов и привлечению к работе над проектами коллективов программистов.
Модульное программирование, получившее также название нисходящего программирования, для сложных программных проектов может носить иерархический характер, т.е. полученные вначале программные модули, в свою очередь, при необходимости, также декомпозируются, с тем, чтобы достичь указанных выше показателей, соответствующих хорошему стилю программирования.
26
Вариант по ГОСТ
SUMMA= 1 + 2 + ... + 20
SUMMA:=0; I := 1;
SUMMA := SUMMA + I; I := I + 1;
SUMMA:=0; I := 1;
(«ПОВТОРЯЙ»)
ТЕЛО-ЦИКЛА
Цикл «I»
SUMMA := SUMMA + I I := I + 1;
(«ПОКА»)
УСЛОВИЕ I > 2 0
Цикл «I»
Нет
Рис. 9. Цикл с постусловием
2.2. Язык программирования и его описание (на примере языков Си/С++)
Совокупность понятий, относящихся к указанной в заголовке теме, иллюстрирует рис. 10. На нем сразу же обращают на себя внимание слова "ЯЗЫК ПРОГРАММИРОВАНИЯ", "АЛФАВИТ", "СИНТАКСИС + СЕМАНТИКА" и "цветок с лепестками". Обсудим эти понятия подробнее.
Алгоритмический язык (язык программирования), как средство записи алгоритмов, является формализованным средством, предназначенным не столько для общения между людьми, сколько для общения между человеком и ЭВМ. Это определяет следующие разумные требования к языку программирования.
1. Он должен быть достаточно простым^ чтобы быть доступным широкому кругу людей.
2. Язык должен допускать однозначное истолкование алгоритма, чего нельзя сказать об обычном разговорном языке.
3. Алгоритм, записанный на некотором языке, должен быть сначала переведен на машинный язык ЭВМ. Переводом занимается специальная программа - транслятор. Язык должен быть простым и в том плане, чтобы не было особых сложностей при создании и функционировании транслятора.
27
Этим требованиям в достаточной степени удовлетворяют языки Си/С++.
Языки Си/С++, как и любые другие языки программирования, полностью определяются заданием их алфавита (словаря исходных символов), точным описанием их синтаксиса (грамматики) и семантики (смысла) - "СИНТАКСИС + СЕМАНТИКА" (см. рис. 10).
ж - напоминает о том, что, например, буквы греческого алфавита
использовать нельзя
нельзя пользоваться римскими символами
J..
Строчные и
прописные латинские буквы
/* */ \ \п \г \t \" \'
и др. / * + - ; % » « < > < = > = == !=
& I - ' ! . - > { } ( ) && : = + = - = *= /= %= » = « =
&= 1= ' = [ ] ++ - ,
Только в языке C++ :: // .* ->*
и др.
Специальные символы
АЛФАВИТ (ЛИТЕРЫ) СИНТАКСИС + СЕМАНТИКА
Простота Однозначность
ЯЗЫК ПРОГРАММИРОВАНИЯ Рис. 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
-Буква> ::= ( <Прописная_буква> | <Строчная_буква> )
-Ненул_восьм_цифра> 1|2|...|7
-Восьмеричная_цифра> ::= ( <Ненул_восьм_цифра>|0 )
<Ненул__дес_цифра> ::= ( <Ненул_восьм_цифра>|8|9 )
<Десятичная_цифра> :;= ( <Ненул^ес_цифра>|0 )
<Идентификатор> ::= ( <Буква>|_ ) { ( <Буква>|
<Десятичная_цифра>|_)}
!!! ДЛИНАИДЕнтификатора !!! Эквивалентно
Ж ДЛИНАИДЕ 8! > К .
Н. В и р т Синтаксические
диаграммы Прописная_буква Строчная_буква
Буква ^ ^ Прописнаябуква
Строчнаябуква • — J ^
Ненул_восьм_цифра
Восьмеричная_цифра
1 Ненул_восьм
_цифра I
Ненул_дес_цифра
X Ненул_восьм
_цифра
Десятичная_цифра
Ненул_дес_ цифра
I Идентификатор
Буква Буква
Дес_цифра
о-Рис. 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 case const delete dynamic cast
auto catch const cast do else
bool char continue double enum
break class default
explicit
32
export for Inline namespa.ce protected return static template try vmion void.
extern friend int new public short static_cast this typedef unsigned volatile
false goto long operator register signed struct throw typeid using wchar t
float if xmitable private reinterpret^cast sizeof switch true typename virtual 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-константа
е H h F-константа
F-константа
10. Ю.Гэкв. 10.F 0.0054 0/00541 экв. 0.0054L .0054 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 cha.i: *type ) ; // Указатель на вид доступа к файлу
const char *path.
Функция открывает файл path в режиме доступа type. Символьная строка type задает вид доступа к файлу в соответствии с табл. 6. Функция/open возвращает указатель на открытый файл. Нулевой указатель (NULL) означает ошибку. Многочисленные примеры открытия файлов с контролем ошибок приведены ниже.
Вид доступа type "г"
'V'
"л"
•V+"
"w+"
"а+"
Табл. 6. Виды доступа к файлу Назначение
Открывается файл для чтения. Если файл не существует или не может быть найден, то возникает ошибка Открывается пустой файл для записи. Если файл уже существует, то его содержимое будет уничтожено Открывается файл для записи в конец (добавления). Если файл не существует, то он сначала будет создан Открывается файл для чтения и для записи (файл должен существовать, иначе - ошибка) Открывается пустой файл для чтения р для записи. Если файл уже существует, то его содержимое будет уничтожено Открывается файл для чтения и для записи в конец (добавления). Если файл не существует, то он сначала будет создан
Закрытие потока. Для закрытия потока служит функция fclose(), которую следует вызвать сразу же после окончания работы с потоком:
/ / Возвращает О при успехе и EOF при // ошибке
stream ) ; / / Закрываемый поток
ilnclude <stdio.h>
±nt fclose(
FILE
Примеры закрытия файлов с контролем ошибок приведены ниже.
Предопределенные указатели потока. В начале выполнения Си-программы автоматически открывается пять потоков:
42
• стандартный ввод (предопределенный указатель stdin); • стандартный вывод (предопределенный указатель stdout); • стандартный вывод сообщений об ошибках (предопределенный
указатель stderr); • стандартный дополнительный поток (предопределенный указа
тель stdaux); • стандартная печать (предопределенный указатель stdprn).
По умолчанию stdin соответствует клавиатуре терминала, stdout и stderr - экрану терминала, stdaux - дополнительному порту и stdprn - печатающему устройству.
Предопределенные указатели пяти перечисленных стандартных потоков можно использовать в любой функции ввода-вывода, которая в качестве аргумента требует указатель потока.
Функции чтения из потока и записи в поток. Функции чтения из потока и записи в поток, имеющиеся в языке Си, перечислены в табл. 7.
Табл. 7. Функции чтения из потока и записи в поток Объект
операции
Серия байтов Символ
Данное int Строка Формат, данные
Чтение из stdin
getc getchar
gets scan/
Чтение из любого потока
fread
fgetc fgetchar
getw /gets fscanf
Чтение из стро
ки Си
sscanf
Запись в stdout
put putchar ungetc
puts print/ vprintf
Запись в любой поток
fwrite
fputc fputchar
putw /puts /print/ v/print/
Запись в строку Си
sprint/ vsprint/ 1
На данном этапе среди функций, перечисленных в таблице, рассмотрим лишь универсальные функции для ввода scan/-/scan/ и для вывода printf-fprint/
2.4.2. Ввод с использованием функций scanf-fscanf
Вначале рассмотрим и проанализируем несколько примеров. Общей особенностью приведенных ниже программ-примеров является их оформление в виде, предусматривающем возможность их выполнения на ЭВМ.
/* Программа-пример 1 (начало) .
43
ввод в из файла "
ЯЗЫКЕ Си exl
же значения: ±пЬ
float long char
Написать "exl. dat") -/
• написать фрагмент Си-программы^ которая dat" на магнитном диске прочитает указанные ни-
1 г а. 12; f; 1; ch. str[ вид
// // // // // //
20 ];//
OxFA -22 074 1.57 -125874 'Z '
"Нам Тхань" читаемых данных (вид строк в файле
^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" ния:
±пЬ short ±nt float long-char
на магнитном диске прочитает указанные ниже
1; 11/ 12; f; 1; ch.
// Ох FA // -22 // 074 // 1.57 // -125 // 'z'
файла значе-
45
str[ 20 ];// "Нам Тхань" 2, Написать вид читаемых данных
"exl.dat") */
(вид строк в файле
1 Символ за%
d D о О
х,Х
i
I
и
и \e,E,f,
с
S
%
/ ? •
Р
Табл. 8. Способы преобразования символов при вводе Тип, ожидаемый при вводе
Десятичное целое Десятичное целое Восьмеричное целое Восьмеричное целое Шестнадцатеричное целое без префиксов Ох или ОХ Десятичное, шестнадцатеричное или восьмеричное целое Десятичное, шестнадцатеричное или восьмеричное целое Десятичное целое без знака Десятичное целое без знака
Величина с плавающей точкой из мантиссы и порядка Символ. Пробельные символы, которые обычно пропускаются, считываются, если указано "с". Чтобы прочесть из потока следующий не пробельный символ, используйте формат Vols Символьная строка
Символ %
Из потока ничего не читается
Величина в виде XXXX:YYYY^ где цифры X и Y являются шестнадцатеричными цифрами верхнего регистра
Тип аргумента
Указатель на int Указатель на long int Указатель на int Указатель па long int Указатель на int
Указатель на int
Указатель на long int
Указатель на unsigned int Указатель на unsigned long int Указатель из.float
Указатель на char
Указатель на символьный массив, достаточно боль-пюй, чтобы разместить вводимое поле и завершающий нуль-символ '\0', добавляемый автоматически Не преобразуется, участвует во вводе как символ '%' Указатель на переменную типа int, в которую записывается количество символов, считанных из потока вплоть до этой точки при текущем вызове функции Указатель на объект (far* или 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" ни я:
±пЬ
float long-char
на Си-программы, которая из
магнитном диске прочитает указанные ниже
±. // 11, // 12; // f; // 1; // ch, // str[ 20 ];//
OxFA или 250 74 18 1,57 -125874
"Нам Тхань" 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). Обратите внимание, что "точность", в отличие от "ширины", может вызвать усечение выводимой величины или ее округление (для переменной с плавающей точкой).
Тип d, i, и, о, X, X
Е, e,f
g^G
с S
Табл. 10. Действие поля формата ".точность" Смысл
Указывает минимальное число выводимых цифр. Если точность меньше, чем надо, то число не усекается. Если точность больше, чем надо, то число дополняется слева нулями. Указывает число цифр, выводимых после десятичной точки (в случае усечения пос л е д и ^ цифра округляется).
Указывает максимальное число значащих цифр выводимого числа. Ни на что не влияет. Точность указывает максимальное число выводимых символов. Лишние символы строки не выводятся.
Умолчание Если точность равна нулю, или вообще опущена, или стоит просто точка, то в качестве точности берется единица. Точность равна шести. Если она равна нулю или стоит просто точка, то десятичная точка не выводится. Выводятся все значащие цифры. Выводится символ. Символы выводятся до тех пор, пока не будет достигнут нуль-символ.
"Префиксы 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
и 0
X или \х /
\Е, е
G'S
с S
п
р
int int int int
float
float
float
int Строка
Указатель на целое
Указатель типа void far *
Десятичное целое со знаком Десятичное целое без знака Восьмеричное целое без знака Шестнадцатеричное целое без знака с использованием цифр "abcdef' или "ABCDEF" Величина со знаком вида [-]dd.dd, где d - десятичная цифра. Число цифр до точки определяется величиной числа, а после - точностью. Величина со знаком вида [-]d.ddddde[3HaK]ddd или [-ld.ddddd£[3HaK]ddd Величина со знаком, выводимая в формате /или е, Е, в зависимости от того, что компактнее при заданной точности Отдельный символ i Символы выводятся или до достижения нуль-символа, или после вывода количества символов, заданного в поле "точность" Выводится число символов, успешно записанных к данному м о м е т у . Эта величина присваивается переменной inl с адресом в аргументе. Выводит адрес, на который указывает аргумент, в виде ХХХХ : УУУУ (сегмент : смещение) с использованием шестнадцатеричных цифр верхнего регистра
Префикс "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 ±nt cha.r ±nt
a, Ь; i J/ cl , c2^ c3 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 ±nt char ±nt char
a; ^f jr cl, c2, c3; retcode; 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 wchart
int bool
float
Размер в байтах 1 2
Зависит от реализации 1
4
Назначение Для символа (-128 ... +127) Для символа из расширенного набора (-32768 ... +32767) Для целого значения Для логического значения (falsey true) Для значения с плавающей точкой (по абсолютной величине от 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
unsigned или unsigned int short или short int unsigned short или unsigned short int long или long int
unsigned long или unsigned long int double или long float long double
Размер в байтах 1
Зависит от реализации 2
2
4
4
8
10
Назначение Байт с неотрицательным целым значением (0 ... 255) Для неотрицательногЬ целого значения Целое значение (от -32768 до +32767) Беззнаковое короткое целое (0 ... 65535) Целое длинное (-231...+ (2^1 -1) ) 1 Беззнаковое целое длинное (0. . . (232-1)) Вещ. с двойной точностью ±1,7 .10- '^ . .±1 ,7 .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 >
и short У
и long У
•( char У
•Г long V ^
•Г float \
> 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;
// Возвращает О при успехе
// Вызов функции: определяет 11, 12 %d 12 = %d", 11, 12 ) ;
int {
}
main ( void )
save( ) ; prlntf( "\n
return 0;
11
Файл 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 ) {
savel( ) ;
prlntf( "\n 12
return 0;
// Возвращает 0 при успехе
// Вызов функции: определяет // значение 12
%d", 12 ) ;
Файл 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 а).
Гипотетический программный проект f l . cpp f2.cpp fS.cpp
1 m i l l ^ ^ ^ ^ ^ ^ И н 1 extern int i 1 ; ^ ^ H 1 o v f o r n И - ^ ^ ^ H
{
float i 1 ;
)
a) Гипотетический программный проект
f l . cpp f2.cpp fS.cpp
• " s t a t i c l ^ ^ H 1 static char ch; ^ H 1 ctatin i9- ^ ^ ^ ^ 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 Arg1 MaxMin
double &Max
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 Arg1 Max double
Процесс Наибольшее из Arg1 и Arg2 получаем как возвращаемое значение
Рис. 27. Спецификация функции с возвращаемым значением
Исходные данные (передаются по значению)
Теперь запишем исходный текст в виде законченной программы, содержащий записи прототипа, вызова функции и ее определения.
/* Файл МАХ.СВР Однофайловый программный проект с двумя
иллюстрирует работу с функцией: ции, определение чением
функции и вызов объявление функции с
функциями. (прототип)
Пример функ-
возвращаемым зна-
^include <stdio.h> // Для функций ввода-вывода
// Прототип функции: Argl^ Агд2 - параметры функции, функция // имеет возвращаемое значение. В данном случае прототип // функции является обязательным, так как вызов функции // выполняется раньше, чем функция определена dovLble Мах ( double Argl, double Arg2 ) ;
±пЬ main ( void ) {
double al
// Возвращает 0 при успехе
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 \0
12 ?
13 ?
14 ? kaf_name |
Рис. 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.
Строки/ столбцы
0 1 2
6 7
0
С 1 2
6 7
1
о
2
с
Т а б л . 3
т К К
К К
4
а а а
а а
1Ь. 5
в Ф Ф
Ф Ф
С т р у к т у р а 6
е е
е е
7
Ф Д
д
д д
8
Г р р
р р
м а с с и в а 9
К а а
а а
10
\0
11
? А Т
С с
12
9
В к А У
13
?
т \0
п Д
14
? \0 ?
р \0
15
? 7 ?
\0 ?
В этой таблице элемент массива аггjkaf_name[ 6][ 7 ] хранит код символа 'д'. Элементы этого массива в оперативной памяти компьютера располагаются в подряд идущих байтах по строкам:
arr_kaf_name [ О ][ О ] arr_kaf_name [ О ] [ 15 J arr_kaf_name [ 1 ][ О ] агг kaf_name[ 1 ][ 15 ]
arr_kaf__name[ О ][
агг kaf namef 1 ] [
arr_kaf__name [ 7 ] [ О ] arr_kaf__name [ 7 ][ 15
arr_kaf_name [ 7 ] [
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 // Группа char char float
current =
fiol 20 ];// ФИО
group^^name [ 7 ]; date[ 9 ];// Дата поступления в университет stlp;
{
};
// Размер стипендии // Определение объекта
"ФТК:", "Иванов И. И. "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 char // Группа сЬа.г char float
current =
fak__name[ 30 ]; fio[ 20 ];// ФИО
group__name [ 7 ]; date[ 9 ];// Дата поступления stip; // Размер стипендии
// Определение объекта
в университет
{
};
"ФТК:", "Иванов И. И. "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
Оператор
->( return
->^ break
->r continue
- ^ goto J
•
' ^ •
V ^
' ^ L ^
< < >
Выражение
Выражение
A
4'> ^ ^
ь ^ Идентификатор
jf
Блок
while
L ^ V ' У ^
^
^
switch
do-while
for
u i ic ; |^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; )
В соответствии с синтаксической диаграммой для операторов, приведенной на рис. 31 , после операторов-блоков символ ";" не ставится. По этой причине символ "точка с запятой" отсутствует после символов " } " , завершающих блоки.
Поскольку в качестве "оператора 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
case ' N I Константное I ^ / ' ^ T ^ [ J \ _выражение | I V ' У I f I
Ц^ default y
Оператор V-ffi }
Рис. 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
do - ^ / d o j - H Оператор V-U while V ^ / ( V H Выражение Н->Г ) \>( \ )—•
Рис. 37. Синтаксическая диаграмма оператора do-while
Работа оператора поясняется следующей эквивалентной последовательностью операторов:
cycle: оператор ±£( выражение ) goto cycle;
Из эквивалентного представления следует, в частности, что в отличие от цикла while, тело цикла do-while, независимо от значения "выражения", будет выполнено не менее одного раза. Для изменения хода выполнения операторов, составляющих тело цикла можно воспользоваться операторами break и continue (рис. 38).
do {
Ьгяак"
} while( выражение );
do {
continue*
} 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
int
namel 20 ] = "Кафедра АВТ"^ kaf__name [ 20 ]; 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( с else ±£( с =^ 3 ) а += 1/
2 ) а-
while( выражение ) {
while( выражение ) {
lf( /* Ошибка */ . . . ) goto loop_end;
(oop_end: < Рис. 41. Использование оператора go/ о для выхода
из гнезда циклов
2. Записать фрагмент программы, соответствующий следующему фрагменту схемы программы (выполнить действие, противоположное предыдущему):
<^а <= Ь ^
1 Да к:=п; г:= 1;
Нет г:=3; i к
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 Наименование
операций
Отношения бинарные
Отношения бинарные
Поразрядная "И" бинарная
Поразрядная "ИСКЛЮЧАЮЩЕЕ
ИЛИ" бинарная Поразрядная
"ВКЛЮЧАЮЩЕЕ ИЛИ" бинарная Логическая "И" бинарная(and)
1 Логическая "ИЛИ" бинарная (or)
Условная тернарР1ая
Простое присваивание
Совмещенное присваивание
Генерация исключения Запятая
(последовательность)
Знаки операций
< - меньше: выражение < выражение > - больше: выражение > выражение <= - меньше или равно: выражение <= выражение >= - больше или равно: выражение >= выражение == - равно: выражение == выражение != - не равно: выражение != выражение & - поразрядное умножение: выражение & выражение ^ - выражение '^ выражение
1 - выражение | выражение
&& - логическое умножение: выражение && выражение ii - логическое умножение: выражение || выражение
?: - выражение ? выражение : выражение
= - 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 i n t ; .
Для доступа к элементам двумерного массива вычислительной работы еще больше - на каждый элемент два умножения и два сложения. Поэтому для повышения быстродействия надо избегать использования массивов вообще и особенно массивов высокой размерности.
Ссылка на элемент структуры. Ссылка на элемент структуры осуществляется с помощью операции, обозначаемой точкой. Выражение
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;
X = X + 1; а = а г г [ X ];
/' эквивалентно /* эквивалентно
/* эквивалентны
*•/ ++х; V --у;
= arrf -h-i-x 7/
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
unsigned. ciiax- или
double (наибольший тип) double float
unsigned long int long int
unsigned int int
char или unsigned short int 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
НеО НеО
0 0
Операнд2 НеО
0 НеО
0
Операнд1 && Операнд2 1 0 0 0
Операнд1 |j Операнд2 1 1 1 0
Вычисление выражения с логическими операциями прекращается, как только результат становится однозначно определенным:
О && ( А > в ) 2 \\ ( ( А+В ) < 4 )
В этих выражениях правый операнд не будет вычисляться. Пример. Логическая функция задана следующей таблицей ис
тинности (табл. 20).
Табл. 20.Задание Параметр 1 ( р1 )
НеО НеО
0 0
логической функции таблицей истинности Параметр 2 ( р2 )
НеО 0
НеО 0
Значение функции 0 0
НеО 0
// Прототип ±nt lf( ±nt pi, ±nt p2 )
// Определение функции ±nt If(
±nt ±nt
( ±f( (
P2 )
!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; offset = offset / 2; s = s * ( f-h2 ) ; a[ i+2 ] = a[ i+2 ] - 10;
Сокращенная форма count += 2; offset /= 2; s *= f + 2; 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. Определение и применение указателей
Определение указателей. Синтаксис определения указателя имеет вид:
описатель_класса_хранения спецификация^ типа *идентификатор;
"Спецификациятипа" может относиться к любому основному или производному типу данных. Символ "*" означает "указатель на".
Примеры:
/ / Определяет iptr как указатель на // целое // Объявляет cptr как указатель на // символ // Определяет fptr как указатель на // статическое данное с плавающей // точкой
±nt
ex t em chctr
static float
*iptr;
*cptr;
*fptr;
Операции над указателями. Один из способов получения адреса данного состоит в применении унарной операции взятия адреса &, приоритет и порядок выполнения которой указаны выше в табл. 16:
±xit main ( void ) {
int Xr // Целое *px; // Указатель на целое: пользоваться
// указателем пока нельзя^ так как // его значение еще не определено
}
X = 2; рх = &х;
*рх = 3;
return 0;
// Теперь указателем рх пользоваться // можно: *рх равно 2
// Теперь *рх равно х равно 3
Единственное ограничение применения операции взятия адреса & состоит в том, что ее нельзя применить к объекту с классом хранения 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 ] studJnfo[ 1 ] studJnfo[ 2 ] studJnfo[ 3 ]
Адрес
15000 1-15004 L 15016 V
1—•
Адрес
15000 15001 15002 15003
15004 15005 15006 15007 15008 15009 15010 15011 15012 15013 15014 15015
Содержимое
'Ф' •т 'К' ЛО' 'И' 1 'в' 'а' 'н' 'о' 'в'
'И'
'И'
'\0
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++ ) ; }
j re t ixm 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 У:
Г Р |
[т1 "Т1
ПГ" ГТ" FR" Гз" ГТ]
б;
срр [ / )
\0
\0
i О
N
\0 \0
4 и
*( *( ++СРР ) )
1
2
Операции одинакового приоритера, выполняются справа налево.
Будет напечатано: POINT
( *( - ( *( ++СРР ) ) ) ) + 3
1
2
4 5-
Операции одинакового приоритета, выполняются справа налево.
Будет напечатано: 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)
\0
" p l " Q | "Tl "NI T l
гт~ гг ГЦ ГЦ ГТ"
( *( срр[ -2 ] ) ) + 3 I I
2 3
Приоритет [ ] выше, чем *.
Будет напечатано с учетом предыдущей печати: POINTER ST
Прод. рис. 43
Так как a[i] означает адрес первого элемента строки / массива "(з", то *л[/] есть значение этого элемента. Аналогично, a+i эквивалентно &а[/], *{a+i) эквивалентно a[i], *(a-^i)+i эквивалентно a[i]-^i и
ср
с
срр
1 ^ 'W
н^^^^ /
/ ( < | \ \ Е N Т Е R \0
(
N Е Р \0
Р О 1
N Т
1 \о
г_ F 1 R S Т
1 \о 1 ( срр[ -1 ] ) [ -1 ] ) 1 1 1
1
+
г.
1 3
Операции одинаков
1
ого приоритета, выполняются справа налево.
Будет напечатано с учетом предыдущей печати: Р OIN" ГЕК STEF 3
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 ce tu im О; )
Ответы можно посмотреть в разд. 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 unsigned, int unsigned int unsigned int unsigned int
} sd;
в виде структуры с битовыми полями
blink: 1; // Мерцание bkgrd: 3; // Цвет фона in tens: 1; // Интенсивность символа forgrd: 3; // Цвет символа ch : 8; // Код символа
// 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. С помощью операции "&" удобно проверять и обнулять биты, операции "|" - устанавливать биты, а операции "^" - проверять несовпадение битов.
&-1 А
« » ~
Табл. 21. Операция
бинарная - бинарная - бинарная - бинарная - бинарная - унарная
Побитовые операции Назначение
И ИЛИ j Исключающее ИЛИ Сдвиг влево Сдвиг вправо Дополнение до единицы
Табл. 22. Определение операций " & " , "|", ' Бит левого
операнда (Ь/) 0 1 0 1
Бит правого операнда (Ьг)
0 0 1 1
Ы&Ьг
0 0 0 1
Ы\Ьг
0 1 1 1
Л "
Ы" Ьг
0 1 1 0 i
Операция \ : 0001001101100011 0100001100100001
0101001101100011
Операция ^: 0001001101100011 0100001100100001
0101000001000010
Операция &: 0001001101100011 0000000000000001
0000000000000001
Операция сдвига влево. Формат операции:
операнд « выражение
В результате биты "операнда" будут сдвинуты влево на число битов, задаваемое значением "выражения". Освобождающиеся справа биты заполняются нулями. Допустимые значения "выражения" изменяются в диапазоне от О до 8*5'/zeoy( операнд ).
/* Файл РЗЗ. СРР Однофаиловый
иляюстрируе т V
программный действие
проект с операции сдвига
одной влево
функцие (ВС+ + 3.
й. 1)
Пример
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.
&= 1= Л=:
~= « = » =
Табл. 23. Сокращенная Операция
запись побитового присваивания Назначение
Операция "И" и присваивание Операция "ИЛИ" и присваивание Операция "^" и присваивание i Дополнение до единицы и присваивание Сдвиг влево и присваивание Сдвиг вправо и присваивание
8. ДИНАМИЧЕСКОЕ РАЗМЕЩЕНИЕ ОБЪЕКТОВ В ПАМЯТИ.
ОДНОНАПРАВЛЕННЫЙ НЕКОЛЬЦЕВОЙ ЛИНЕЙНЫЙ СПИСОК И ОПЕРАЦИИ С НИМ
8.1. Понятие об однонаправленном линейном списке. Динамическое размещение объектов в памяти
Сущность однонаправленного линейного списка (ЛС), предназначенного для хранения символов, представлена на рис. 45.
Т'
W
'е'
W
'к'
W
'с'
W
'т'
NULL
I start Рис. 45. Однонаправленный линейный список
Каждый элемент ЛС содержит две части (два поля): • основное поле, в котором хранится содержательная информация
(в нашем примере - символ); • вспомогательное поле, в котором хранится указатель на следую
щий элемент линейного списка. Список содержит два элемента, которые являются особенны
ми, отличными от других. Это первый (головной) элемент ЛС - его особенность состоит в том, что он снабжен указателем (на рис. 45 таким указателем является Start), Без этого указателя нельзя работать с линейным списком. Последний элемент ЛС также является особенным, так как в его вспомогательном поле хранится указатель NULL, означающий, что следующего элемента нет.
Из сказанного следует, что элемент ЛС в терминах языка С++ можно определить в виде структуры:
stxract EL ЕМ {
char ELEM
} ;
ELEM
ch; *neKt;
*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( ' С ) , // Добавление в конец // элемента 'С' // Удаление после '3'
списка
After_Del ( '3'); Pr±nt_ls ( ) ; Dest_ls ( ) ; // Разрушение списка // Заполнение линейного списка символами из файла LS.DAT: // последний прочитанный символ - в конце списка Create_end( ) ; Print Is( ) ; Dest_ls ( Add__end (
Add_end (
Add beg( Add_beg ( Print Is ( Del_end(
Del_beg(
) ; 'C 'D' ^B^ 'A'
r ) ; ) ;
) ;
) ;
) ;
) ; ) ;
// // // // // // // // // //
Разрушение списка Добавление в конец списка
элемента 'С' Добавление в конец списка
элемента 'D' Добавление в начало элемента Добавление в начало элемента
Удаление последнего элемента списка
Удаление первого элемента спи
'В 'А
СК(
Print_ls ( ) ; Dest_ls ( ) / // Заполнение for( Int i = 1;
Add__end( '2' Print_ls ( ) ; // Добавление '1 Before_Add( '2', Print_ls ( ) ; // Добавление '3 After_Add( '2', Print_ls ( ) ; // Добавление '1 Before_Add( ' 1 ' , Print_ls ( ) ; // Добавление '2 After_Add( '2', Print_ls( ) ; // Добавление '2 After_Add( '2', Print_ls( ) / // Добавление '3 After__Add( '3', Print_ls ( ) ; After_Del ( '3'); Print_ls ( ) ; Before_Del( '1') Print_ls( ) ; Dest_ls ( ) ;
return 0;
// Разрушение списка списка пятью двойками
i-^+ ) i <= 5; ) ;
' перед Ч' ) ;
' после '3' ) ;
' перед Ч' ) /
' после '2' ) ;
' после '2' ) ;
' после '3' ) /
//
//
//
Удаление после
Удаление перед ' 1
Разрушение списка
// Инициализация списка
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 не закрыт " ) ; e x i 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
До выполнения операции Общий случай Особый случай
• h> • W NULL Start = NULL;
t Start После выполнения операции
2: занесение данного «с» 2 занесение данного «с»
•—Н NULL NULL 3: temp->next =
Start;
t 1: динамическое размещение
temp = new EL;
t 4: Start = temp;
t 1. динамическое размещение
temp = new EL;
t 4: Start = temp;
Рис. 46. Добавление элемента в начало линейного списка
8.5. Создание ЛС с первым занесенным элементом в начале
Для создания ЛС с первым занесенным элементом в начале списка достаточно в теле цикла вызывать функцию занесения одного элемента в конец списка с аргументом, представляющим собой очередное прочитанное число. Прототип функции Create_beg, выполняющей создание списка с первым прочитанным элементом в его начале, определение функции и пример вызова содержатся в примере, приведенном выше.
8.6. Создание ЛС с первым занесенным элементом в конце списка
Эта операция аналогична предыдущей. Для создания ЛС с первым занесенным элементом в конце списка достаточно в теле цикла вызывать функцию занесения одного элемента в начало списка с аргументом, представляющим собой очередное прочитанное число. Прототип функции Create end, выполняющей создание списка с первым прочитанным элементом в его конце, определение функции и пример вызова содержатся в том же примере.
164
• — h >
До выполнения операции Общий случай Особый случай
Start = NULL,
t Start
t Start
NULL
После выполнения операции 2: 2: занесение данного « о
# • 4в:
NULL NULL 3: temp->next = NULL;
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
До выполнения операции Общий случай Особые случаи:
а) в ЛС один элемент б)
• — — • ! • — ^ > I I
• • ^ NULL NULL Start = NULL;
Start t Start
После выполнения операции
• ^ NULL
Вывод сообщения и возврат из
функции
1: del = Start;
t 2: Start = del->next;
3: delete del;
t 1: del = Start;
t 2: Start = del->next = NULL; 3- delete del;
Рис. 48. Удаление элемента из начала списка
8.8. Удаление элемента из конца списка Реализация операции, представленная в графической форме,
дана на рис. 49. Прототип функции Delend, выполняющей удаление последнего элемента списка, ее определение и пример вызова содержатся в программном проекте, приведенном в подразд. 8.1.
8.9. Разрушение ЛС с освобождением занятой им динамической памяти
Разрушение линейного списка с освобождением занятой им динамической памяти может быть выполнено путем циклического выполнения операции удаления элемента из начала списка.
Прототип функции Destls, выполняющей разрушение списка, ее определение и пример вызова содержатся в программном проекте, приведенном в подразд. 8.1.
166
До выполнения операции Общий случай
t Start
L__
• — и • — — •
t start
Особые случаи: а) в ЛС один элемент б)
NULL 1 I NULL
t Start
После выполнения операции
Start = NULL;
NULL
Вывод сообщения и возврат из
функции
t 1: prev
t t end 1: prev = NULL;
end 2: delete end; 2: delete end;
3 prev->next = NULL; 3: Start = NULL; Рис. 49 . Разрушение линейного списка
8.10. Печать содержимого ЛС
Печать содержимого линейного списка может быть выполнена путем циклического выполнения печати содержимого текущего элемента списка и продвижения по списку от начала до конца. Операция тривиальна и не требует особых пояснений.
Прототип функции P r i n t l s , выполняющей печать содержимого линейного списка на экран, ее определение и пример вызова содержатся в программном проекте, приведенном в подразд. 8.1.
8.11. Добавление элемента после каждого элемента ЛС, содержащего заданное значение
Реализация операции, представленная в графической форме, дана на рис. 50.
Прототип функции AfterAdd, выполняющей добавление элемента с данным add после каждого элемента ЛС, содержащего заданное значение y?«(i, ее определение и пример вызова содержатся в программном проекте, приведенном в подразд. 8.1.
167
До выполнения операции Общий случай
t Start
^
find
w ,, . NULL
После выполнения операции 2:
find
4: W
add 3:
• w NULL
Особые случаи
Start = 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. Добавление элемента перед каждым элементом ЛС, содержащим заданное значение
Реализация операции, представленная в графической форме, дана на рис. 51.
Прототип функции BeforAdd, выполняющей добавление элемента с данным add перед каждым элементом ЛС, содержащим заданное значение find, ее определение и пример вызова содержатся в программном проекте, приведенном в подразд. 8.1.
8.13. Удаление элемента после каждого элемента ЛС, содержащего заданное значение
Реализация операции, представленная в графической форме, дана на рис. 52.
168
До выполнения операции Общий случай
•—ь>
find
NULL
t Start После выполнения операции
2:
• \-> 4:
•• W
add 3:
' W
find
—• NULL
t start
Особые случаи
a) Start = NULL; 6) cur = Start;
a) Вывод сообщения и
возврат из функции
б) См. реализацию
шага 4 prev t cur 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.
До выполнения операции Общий случай
1 t start
w
find
w w ^ NULL
После выполнения операции
find
2: w w NULL
t start
Особые случаи
а) Start = NULL, б) Start->next =
NULL,
а) Вывод сообщения и
возврат из функции б) Вывод
сообщения и возврат из функции
t t cur 1 • del = cur->next; 2: cur->next = del->next;
3. delete del; 4: если после удаления текущий элемент -
последний, то выполняется выход из функции
Рис. 52. Удаление элемента после каждого элемента ЛС, содержащего заданное значение
Прототип функции BeforDel , выполняющей удаление элемента перед каждым элементом ЛС, содержащим заданное значение find, ее определение и пример вызова содержатся в программном проекте, приведенном в подразд. 8.1,
При реализации операции удаления производится просмотр элементов линейного списка, начиная со второго элемента до последнего элемента списка.
Отличительной особенностью операции является то, что если удаление элемента выполнено, то продвижение указателя на пред-предыдущий элемент не требуется. Указатель же на текущий элемент должен продвигаться всегда.
Другой особенностью операции является то, что реализация шагов 1: и 2: в случае, когда текущий элемент является вторым в ЛС, отличаются (см. рис. 53).
170
8.15. Зачем нужен линейный список Для хранения и обработки информации наряду с ЛС можно
использовать и массивы. Например, вместо ЛС, приведенного на рис. 45, можно использовать символьный массив из пяти элементов, причем это сэкономило бы оперативную память. Значит ли это, что ЛС не следует использовать? Конечно же, нет!
До выполнения операции Общий случай
t Start
" •
w
... w
find
, NULL
После выполнения операции
2: w
find
NULL
t start
t t t pprev cur
1 • del = pprev->next или del = Start, если текущий элемент в ЛС второй
2: pprev->next = del->next или Start = Start->next, если текущий элемент в ЛС второй
3: delete del,
Особые случаи
а) Start = NULL, б) Start->next =
NULL,
а) Вывод сообщения и
возврат из функции б) Вывод
сообщения и возврат из функции
Рис. 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 ");
struct INDEXJNFO index[MAX_SIZE];
ifOine[pos]=-^NULL) printf("3mo NULLW):
После препроцессора
if (scanner 0==l)myprintf("Cmon \n ");
struct INDEX JNFO index[50];
if(line[pos]=='\0') 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 Радиус while( radius < 10.5f ) {
printf( "%f radius -h= 1 . Of ;
Площадь \n" ) ;
%f \ л " , radius, AREA( radius ) ) .
геЬмхпл 0;
Табл. 25. Отмена макроопределения директивой i^undef До препроцессора
include <stdio h> Ые/гпе print/myprintf int main( void )
\{ printf( "Введите дату: ");
^undefprintf printf( "Введите время: "), return 0,
J
После препроцессора
Текст файла stdio.h после обработки его препроцессором ini main( void) {
myprintf( "Введите дату: "),
printf( "Введите время- "); return 0;
}
В этом примере второй аргумент 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 идентификатор
Mfndef идентификатор
Uelse
Uendif
Функция Компилировать строки, следующие за директивой, если <константное выражение> отлично от нуля ("истина") Компилировать строки, следующие за директивой, если "идентификатор" определен с помощью Udefine Компилировать строки, следующие за директивой, если "идентификатор" не определен с помощью директивы define или определение отменено с помощью Uundef Используется в сочетании с директивами Ш/, Uifdef, Uifndef как отрицание условия Заверщает область действия директив #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> Mefine TRACE void gedine( void ) ; int main( void) и mfdef TRACE
printf( "Main \n"); Uendif
getline(); return 0;
} void getlinef void) { mfdefTRACE
printf( "Getline \n"); ^endif
return; }
После препроцессора Текст файла stdio.h после обработки его препроцессором void getline( void); int main( void ) {
printfC'Main \n");
getline(); return 0;
} -void getline( void ) {
printf( "Getline \n");
return; }
Очень часто директивы условной трансляции используются для предотвращения многократного включения заголовочных файлов:
/-"
V
stdlo h Definitions for stream Inpu t/OL itput.
#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, typedef int SHORT, typedef long LONG, typedef char * STRPTR; typedef struct
{ double r; double i;
} COMPLEX:
Пример применения INTEGER a, b, SHORT c,d, LONGe,f STRPTR ^, h, COMPLEX k.
Значение int a, b; int c, d; longe,f. char *g, *h, struct {
double r; double i.
Из приведенной таблицы следует, что объявление имени типа в общем виде записывается следующим образом:
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, LIGHTBLUE, LIGHTGREEN, LIGHTCYAN, LIGHTRED, LIGHTMAGENTA, YELLOW, WHITE
/* light colors */
enum graphics_errors {
grOk grNoIni t Graph grNotDetected grFileNotFound grInvalidDriver grNo Loa dMem grNoScanMem grNoFlооdMem gr Font Not Found grNoFon tMem grInvalidMode
grError grIOerror grInvalidFont grinvalidFontNum grInvalidVersion
= = = - = = = = = = = = = = = =
0, -i. -2, -3, -4, -5, -6, -7, -8, -9, -10,
-11. -12, -13, -14, -18
graphresult error return codes */
generic error */
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 -ptr2 -ptr3 -
- 5F20 ; - 5F21 ; - 5F41 :
: 0210, : 0200, : 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. программ
блока V
СРР на
(расширение C+-h) .
.СРР принято Ра змещение
для файлов определении
с данных
текста-внутри
^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++ можно использовать не только для передачи параметров в функции, но и для создания псевдонимов данных:
/ / Ссылка хг становится псевдонимом // к // Все равно, что к = 2; // Все равно, что х++;
int int
xr = 2; ХГ+ + ;
X ^ 1; &xr = X
Однако, 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 - e tum/
•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 8 10
2, 3, 4, 5 - вершины 6, 7, 8, 9, 10-листья
Пример двоичного дерева для 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). Кроме массива элементов, соответствующих типу данных стека, достаточно иметь одну переменную целого типа для хранения индекса элемента массива, являющегося вершиной стека. При помещении элемента в стек индекс увеличивается на единицу, а при выборке - уменьшается.
Для реализации очереди требуются две переменных целого типа - для хранения индексов элементов массива, являющихся началом и концом очереди.
Для реализации линейного списка на базе массива требуется вспомогательный массив целых чисел и еще одна переменная (массив данных, вспомогательный массив и вспомогательную переменную можно оформить в виде полей структуры), например:
- массив данных - вспомогательный массив - индекс первого элемента в списке
/-ЫЙ элемент вспомогательного массива содержит для каждого /-го элемента массива данных индекс следующего за ним элемента. Отрицательное число используется как признак конца списка. Тот же массив после сортировки будет иметь следующий вид:
- массив данных - вспомогательный массив ~ индекс первого элемента в списке
Аналогичным образом, для создания бинарного дерева можно использовать два вспомогательных массива с таким же размером, как и массив данных (содержат индексы вершин его левого и правого поддерева). Отрицательное число в этих массивах означает
отсутствие соответствующего поддерева. Как и в предыдущем случае, массив данных и вспомогательные массивы можно офор-
222
10 1 0
25 2
20 3
6 4
21 5
8 6
1 7
30 -1
10 2 6
25 7
20 4
6 5
21 1
8 0
1 3
30 -1
мить как поля структуры. В качестве упражнения можно предлагается самостоятельно и для этого случая составить иллюстрирующий пример.
Обращаем Ваше внимание на то, что при работе с такими структурами необходимо контролировать возможный выход индексов за границы массива (нарушение индексации).
И, наконец, важное заключительное замечание. Рассмотренный способ реализации позволяет использовать преимущества динамических структур (например, сортировать структуры из громоздких элементов данных без их физического перемещения в памяти) и при этом не расходовать время на выделение и освобождение динамической памяти для каэюдого элемента данных.
15. СОРТИРОВКА
Под сортировкой понимают процесс перестановки объектов данного множества в определенном порядке. Цель сортировки - облегчить последующий поиск элементов в отсортированном множестве. В этом смысле элементы сортировки присутствуют во многих задачах прикладного программирования.
Зависимость выбора алгоритмов решения задачи от структуры данных - явление довольно частое. В случае сортировки эта зависимость настолько сильна, что методы сортировки обычно разделяют на две категории:
• сортировка массивов; • сортировка последовательных файлов.
Эти две разновидности сортировок часто называют соответственно внутренней (сортировка массивов) и внешней (сортировка файлов) сортировками. Это объясняется тем, что массивы располагаются во "внутренней" (оперативной) памяти ЭВМ и для нее характерен быстрый произвольный доступ (прямой доступ). Файлы же хранятся в более медленной, но более вместительной "внешней" памяти, т.е. на запоминающих устройствах с механическим передвижением (магнитных дисках и лентах). Указанное существенное различие можно наглядно продемонстрировать на примере сортировки пронумерованных карточек.
1. Представление карточек в виде массива с прямым доступом (рис. 58) означает, что все карточки одновременно видны и равнодоступны.
0
2
8
6
9
4
1
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
44 44
44 м— Об
06
06
06
55
55 55 ^ 18
18
18
12
12
12 12
12
12
."1 18
18
42
42 42
42
42 1 Ф 42
42
42
94
67 м 06
06 1 44
44
44
44
18
18
«"1 55
55
55
55
55
06
06 1 67
67
67
67
67
67
67
94 94
94
94
94
94
94 Рис. 63. Сортировка массива простым выбором
Полный текст программы, иллюстрирующей все разработанные к настоящему времени методы сортировок массивов, приводится ниже. На данном этапе в этой программе не следует рассматривать функции, осуществляющие методы сортировки, отличные от сортировки массива простым выбором. Всю остальную часть программы следует внимательно изучить.
/* Файл TestSortArr.срр. Тестирование сортировки динамически
размещенных массивов с использованием различных методов. Определение методов сортировки приведено в файле
SortArr.срр. Определение Функций размещения массива в динамической па
мяти и освобождения занятой динамической памяти находится в файле А1 ocFreeDM. срр,
Определение функций ввода и печати значений элементов массива дано в файле SortlnOut.срр.
Заголовочный файл программного проекта дан в файле SortArr. 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 ( 4 0 ) ; }
// Закрытие файла ввода RetCodel = fclose ( pStrlnp ) ; ±f( RetCodel =- EOF ) {
prmtf( "\n Ошибка 50. Файл %s не закрыт \n ", pInpFile ) ;
exit ( 50 ) ; }
return/
236
// Печать значений элементов массива в файл результатов void. Wri teArr (
ELEMENT arr[ ]^ // Сортируемый массив char '^'pMSG^ // Указатель на строку-сообщение о
// печатаемом массиве *pOutFile^// Указатель на имя.расширение
// файла результатов *Mode ) // Указатель на режим открытия файла
{
char
char
FILE
int
*pStrOut; // Указатель на структуру со // сведениями о файле результатов
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 ±nt ELEMENT
int
*arr/ size; *arrl;
sizel;
// Указатель на сортируемый массив // Размер сортируемого массива // Указатель на сортируемый массив // для простой сортировки // вставками // Увеличенный на единицу размер // //
сортируемого массива для простой сортировки вставками
// Эти объекты определяем в данном месте, чтобы при // рекурсивных вызовах // создавались заново ELEMENT сору,
median/
int 1 ,
J/
в сортировке Хоора они не
// Копия элемента массива // Медиана разделяемого сегмента в // сортировке Хоора // Индекс кандидата на обмен слева // Индекс кандидата на обмен справа // в сортировке Хоора
// Сортировка массива простым выбором - по неубыванию void SelectSort (
ELEMENT arrf ] ) // Сортируемый массив {
// Индекс последнего элемента из // пока неупорядоченных // Индекс наибольшего элемента среди // 1..L
// Индекс анализируемого элемента // Для наибольшего элемента среди // 1. .L
JLXm
ELEMENT
•LJr
indmax.
к; max;
// Просмотр неотсортированных начальных сегментов массива // (вначале - весь массив, затем - сегмент из первых // size-1 элементов и т.д.) £ог( L ^ size-1; L >= 1; L-- ) {
// Присвоить indmax индекс элемента массива // наибольшим значением ключа из агг[ О ]. indmax = О; max = arr[ О ]; for( к = 1; к <= L; к++ )
.агг[ 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; ar r / " k-1 ] = temp; change = 1;
} } sorted = !change;
}
retvum; }
// Сортировка массива сложным выбором с использованием // пирамиды - двоичного дерева
// Просеивание void Sift(
Int rooty // Корень дерева или поддерева Int last у // Последняя вершина в дереве ELEMENT arr[ ] ) // Сортируемый массив
{ // 1 - позиция "дырки" (объект определен на внешнем // уровне) Int j l , // 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
6
6 61
массива простым выбором. Массив до сортировки 55 12 42 94 18 61
Массив после сортировки 12 18 42 44 55 94
Сортировка, массива простыми включениями. Массив до сортировки 44
6
6 61
55 12 42 94 18 61
Массив после сортировки 12 18 42 44 55 94
Сортировка массива методом "пузырька". Массив до сортировки 44
6
6 61
Сортировка 44
6
6 61
Сортировка 44
6
6 61
Сортировка 44
6
6 61
55 12 42 94 18 61
Массив после сортировки 12 18 42 44 55 94
массива сложным выбором. Массив до сортировки 55 12 42 94 18 61
Массив после сортировки 12 18 42 44 55 94
массива методом Шелла. Массив до сортировки 55 12 42 94 18 61
Массив после сортировки 12 18 42 44 55 94
массива методом Хоора. Массив до сортировки 55 12 42 94 18 61
Массив после сортировки 12 18 42 44 55 94
Рекурсивная сортировка Хоора. Массив до сортировки \ 44
6
6 61
55 12 42 94 18 61
Массив после сортировки 12 18 42 44 55 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 44
12
12
12
12
06
55 ^ 1 55 1
44 ^ 42 42
18
12
12
12 1 55 1 44
44
42
18
42
42
42 1 55 1 55
44
42
94
94
94
94
94 1 55
44
18
18
18
18
18
94 1 55
06
06
06
06
06
06
941
67
67
67
67
67
67
67
i = 2
1 = 3
i = 4
1 = 5
1 = 6
i = 7
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 элементов массива (size = 8)
Конец первого просмотра 44 12 42 55 18 Обратите внимание как «пузырек» 94 «всплыл» вправо!
< • м •
55
12
12
55
42
42
55
94
18
18
94
06
06
94 <—
6
— • 06
Конец второго просмотра
12
12
44
Конец третьего просмотра 12
Конец четвертого просмотра 12
42 42
42 18
18
44 44
18
18 -• 42
18
18
55 06 06
44 06 06
44 44 I 55
06 06
42 42 ( 44
Конец пятого просмотра 12 4-
06 06
18 18 I 42 44 55
67 I 94
55 55 I 67
55 67
06 12 Конец шестого просмотра 06 12 | 18 42 44 55 67 И конец сортировки, так как больше перестановок нет!
Рис. 66. Пример сортировки массива простым обменом
94
67 94
94
67 94
94
Эффективность сортировки. За один проход среднее число сравнений С СРЕД равно 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^ / \
44 55
. 4 2 , / \
12 42
, 9 4 / \
94 18
67 / \
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
/ 67
42
А
^ ^ 55
о \..^^ 94
с:
12
18 06
Рис. 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 В корне соответствующего поддерева образуем «дыру» путем копирования соответствующего корню элемента в сору. Ищем подходящее место для вставки сору и помещаем в него сору.
«Дыра»
67
42 4
1) Г — ; -• j 1
сору W 67 4
1 42 j ^ ^ сору 3) ^ 42
67 4
8 8 Рис, 70. Подготовка двоичного дерева для вершины 4
Size/2 - 1 = 3
/ 18
12 3
1) Г 1 -• 1 1 \ 06
сору ^ 18 <; 06 w
3
3)
12 сору
18
12 6 6
Рис. 71. Подготовка двоичного дерева для вершины 3
06
55 1) Г :
X 2 \ ^ сору _д 67 94 W
Size/2 - 2 2
= 2 сору 1 55 1
X 2)^\Ч 3) 67 94 ^
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 }.
/ 42
44
4
67
2 55
fS
Size/2 - 3 = = 1 a) 6) в)
94
44
1
1) Г - - 1
18 copy к
94 1
copy ( i
i 44 i
18 ^
94
copy
1 44 1
• ^ " ^ l " ^ " " ^
18 1 1 1 1 1 1 1 1 ^)ш\ L 1 1
2 3 2 3 - ^ 2 ^ ; ^ 3
с)) в итоге nonv ЧИМ 67 55
Рис. 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. Далее, каждый из полученных сегментов можно сортировать автономно, аналогично предыдущему, до получения сегментов единичной длины. Тогда массив в целом будет отсортирован.
Исходные значения ключей элементов массива агг[ i ].кеу ( i = О, 1, 2, . ., size-1 ), size = 8
left 44 55 12 1 42 1 94 18 06
hpht 67
п median
median = arr[ (left+right )/2 ] Рис. 75. Идея алгоритма сортировки Хоора
2. Как выполнить разделение на сегменты? Решение этой задачи представлено на рис. 76.
Анализ рисунка показывает, что за size сравнений {size - число элементов массива) исходный массив разделяется на два сегмента, которые можно сортировать автономно. Всего потребуется Xo^^i^ize) таких разделений, в результате которых получатся сегменты единичной длины. Следовательно, общее число сравнений пропорционально size • 1о§2 {size).
259
left
a) i = left, while( arr[ i ].key < median.key ) i++;
Всегда ли закончится цикл? Да, всегда, поскольку, как минимум, справа есть median. После выхода из цикла получим агг[ I ] key >= median.key п
median 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 Исходные значения ключей элементов массива arr[i] key (1 = 0 , 1 , .., size-1), size = 8
right
44 55 12 42 94 18 06 67 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 (разбиение на сегменты закончено) В результате получаем:
' " right
Сегменты.
left 06 18 12 42 94 55 44 67
4 5 6 7 median=arr[(left+right)/2]
J левый left J и правый » nght
(большего размера) б) Каждый из полученных сегментов также разбиваем на сегменты, причем это выгоднее начать с меньшего сегмента (см. обоснование ниже) Поэтому больший из полученных сегментов запоминаем в стеке для последующего разбиения
left j i g h t \=o,^^, j=2; arr[1] меняем местами с агг[2], 06 18 12
0 1 2 Н Н nned
left right 06 12
0 1
1
18
2 med
lan t
lan
•++; J--; i>J (разбиение на подсегменты закончено) В результате получаем
агг[|].кеу
агг[|].кеу
Левый сегмент left j и правый i .right (меньшего размера) Рис. 77. Пример сортировки Хоора для общего случая
4. В каком порядке сортировать полученные сегменты? Сразу оба полученных сегмента сортировать нельзя - какой-то из них нужно отложить на более позднее время, например, запомнить его в стеке.
struct STACK (
±пЬ 1; ±пЬ г;
} s[ М ]; int sp;
// Тип элемента стека
// Левая граница сегмента // Правая граница сегмента
// Указатель вершины стека
261
в) Больший из полученных сегментов (левый) запоминаем в стеке для последующего разбиения. Работа с правым (меньшим) сегментом закончена, так как в нем один элемент. Извлекаем из стека и разбиваем очередной сегмент
left right arr[j] key 06 12 i=0; j=1,0; arr[0] меняем местами с arr[0]; i++, j -
i>j (разбиение на сегменты закончено). Оба полученных сегмента единичной или нулевой
median А^'^чь! и работа с ними закончена
г) Извлекаем из стека и разбиваем очередной сегмент left right
агг[1].кеу 94 55 44 67
left
6 7 median
right arr[i].key 44 55 94 67
6 7 median
i=4; j=7,6; arr[4] меняем местами с агг[6]; i++; j — ; i<=j (продолжаем обмен) i=5; j=5; агг[5] меняем местами с arr[5]; i++; j — ; i>j (разбиение на сегменты закончено). Получаем левый left..j и правый 1..right (большего размера) сегменты. Больший сегмент запоминаем в стеке для последующего разбиения, а работа с меньшим сегментом закончена (в нем один элемент).
arr[i].key 94 67
д) Извлекаем из стека и разбиваем очередной сегмент left right
i=6; j=7; агг[6] меняем местами с агг[7]; i++; j — ; 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
Исходные значения ключей элементов массива агг[ i ].кеу (i = О, 1, .. , size-1), size =
left nght 3 2 - 7 5 4 О
Пример 3
Исходные значения ключей элементов массива агг[ i ].кеу (i = О, 1, ..., size-1), size =
median=arr[(left+right)/2]
left right 3 2 5 - 7 4
1
Пример 4
Исходные значения ключей элементов массива агг[ 1 ].кеу (1 = О, 1, ..., slze-1), size =
median=arr[(left+right)/2]
left right 3 2 4 - 7 5 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, милисекунд
356
509
1026
Время сортировки
для size=512, ми
лисекунд 1444
1956
4054
Соотношение методов по производительности (относительное время
сортировки)
1
1.3
3
Сложные методы сортировки
60
Н О
127
116
241
349
1 1 1.7
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
0
0
20
0
а О 20 а
ь | 2 0 I О I b а b а
а) б) Рис. 82. Способы задания графа
b
-ю
±nt
2. С помощью списка ребер:
\сЬ А
±nt ±nt float
Num Top , NumArc;
first; last / weight/
// Число вершин // Число ребер
// Ребро графа
// 1-я вершина ребра // 2-я вершина ребра // Вес ребра
};
268
/ / Адрес первого элемента массива структур с информацией о // ребрах графа А *рАгс;
Задание графа на основе списка ребер удобйо свести в одну структуру:
/ / Структурный тип для графа struct GRAPH {
±nt NumTop; // Число вершин Izit NumArc; // Число ребер А *рАгс; // Указатель на начало массива ребер
// в динамической памяти } ;
Сопоставление указанных форм задания графа позволяет заключить следующее.
1. Использование матрицы соединений требует хранения в памяти NumTop*NumTop элементов.
2. Использование списка ребер требует хранения в памяти Ъ"^NumArc элементов.
3. При Ъ'^NumArc < NumTop"^NumTop эффективнее использовать задание графа с помощью списка ребер.
4. Использование списка ребер алгоритмичнее. Это означает, что алгоритмы решения задач с использованием графов, заданных списком ребер, проще и эффективнее.
Для примера, приведенного на рис. 81, информация для списка ребер имеет следующий вид:
1г 2, i , 4г 2 , 3 , 2г 5 , 3 , 4, 4, 5 ,
80. 10. 20. 10. 20. 10.
.0
.0 ,0 .0 О 0
Здесь в первой строке 1 и 2 - номера вершин, а 80.0 - вес соединяющего их ребра.
16.3. Почему для решения задачи подходит рекурсивный алгоритм?
в общем случае путей из вершины start до вершины finish может быть несколько, но только один путь будет наилучшим (в частном случае, путь может вообще не существовать, например, в несвязанном графе, или может быть несколько наилучших, эквивалент-
269
ных путей). Нас при поиске оптимального пути интересует на каждом этапе только один путь, а не все сразу, т.е. требуется последовательный перебор путей. Из информации на рис. 83 следует, что для перебора путей хорошо подходит рекурсивный алгоритм (аналогия с вычислением факториала).
Путь
р,
Вершина
Ребро •
Рис. 83. Рекурсивный перебор путей
16.4. Представление кратчайшего пути до каждой вершины
Сведения о любом пути должны содержать следующую информацию.
1. Имеется ли путь до вершины графа? 2. Суммарный вес пути, начиная от заданной начальной вер
шины (start)? Но только этих сведений недостаточно, так как нет указаний,
откуда и как двигаться. Для задания недостающих сведений можно организовать линейный список, который для графа, представленного выше на рис. 81, будет иметь вид, показанный на рис. 84 а. Такой линейный список можно организовать на базе массива структур (рис. 84 б).
stmjct W {
int ±nt
float } ;
// Путь до одной вершины
exist; // (!= 0) - путь имеется ref; // Предыдущая вершина^ через
// которую проходит путь SumDlst; // Суммарная длина минимального пути
// Адрес первого элемента массива с информацией о минимальном // пути между заданными вершинами W *pMinWay/
270
start finish start finish 1
1
0.0
0
4 1
10.0
1
5 1
20.0
4
2 1
30.0
5
exist
SumDist
ref
1 1
0.0
0
2 1
30.0
5
3 1
30.0
4
4 1
10.0
1
5 1
20 0
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) до финиша (finish) - рис. 86.
271
start а) intermediate = finish
б) intermediate != finish intermediate
О • intermediate
О finish
Вершина Конец
•>\ Ребро Путь
Рис. 86. Прохождение пути от достигнутой вершины до финиша
Попытку шага вперед из достигнутой вершины по заданному ребру будем делать с помощью функции ForStep, а прохождение пути (если он есть) от достигнутой вершины до вершины finish - с помощью функции Pass Way (взаимно рекурсивный вызов PassWay -For Step).
Для решения транспортной задачи спроектируем программу и в ней разработаем функции PassWay -ForStep. Спецификация функции, выполняющей шаг вперед, представлена на рис. 87. Обратите внимание, что в список параметров этой функции включены только три параметра - topl^ IndArc, top2. Это важно, так как для рекурсивных функций, как это было показано выше, число параметров следует минимизировать. Поэтому Gr, pMinWay определены как глобальные объекты. Исходный текст программы для решения транспортной задачи, включающий определение функции ForStep., приведен ниже. На данном этапе в этом тексте рекомендуем рассмотреть только введенные типы, данные и определения всех функций, кроме solution и PassWay. Указанные в конце функции будут рассмотрены позже.
Достигнутая вершина int top1 -Индекс ребра, по которому шагаем int IndArc-Вершина на конце ребра int top2 -Граф GRAPH Gr-
input
ForStep W *pMin\/Vay
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/ ±nt last; £1олt weight;
} ;
// 1-я вершина ребра // 2-я вершина ребра // Вес ребра
// Структурный тип для графа strvLcb GRAPH {
±nt NumTop; ±nt NumArc; A *pArc;
// Число вершин // Число ребер // Указатель на начало массива ребер // в динамической памяти
};
// Структурный тип пути до одной вершины struct W {
int
xnt
}.
exist;
ref;
// //
(!=0) в графе имеется путь до вершины
// Предыдущая вершина, через которую // проходит путь до данной вершины
£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 О 2 1 3 1 4 2 5 3
В ерши на - финиш: 1, в ерши на - с тар т: О Значение минимального пути: 30
Список вершин, образуюш;их этот путь (от finish до start) : 1 , 4 3 О
Спецификация функции прохождения пути от достигнутой вершины до finish, если он есть, представлена на рис. 88.
1 3 2 4 3 4
80. 10. 20. 10. 20. 10.
,000000 ,000000 ,000000 ,000000 ,000000 ,000000
283
Достигнутая вершина - отправная точка пути int InterMediate
Финиш пути Граф
int finish GRAPH Gr
PassWay
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
Граф GRAPH Gr
input
solution Массив с информацией о
- • наилучшем пути W *pMinWay
process output Рис. 90. Решение задачи в целом
Список параметров этой функции пуст. Объясняется это тем.
284
что start, Gr, pMinWay являются глобальными объектами (определены на внешнем уровне). Текст программы, включающий определение функции solution, приведен выше. Данная функция, в отличие от предыдущих функций Pass Way и ForStep, является интерфейсной функцией и вызывается для решения транспортной задачи.
16.6. Пример поиска минимального пути в графе
Схема графа приведена на рис. 91.
first last weight
0 1 2 3 4 5
0 0 1 1 2 3
1 3 2 4 3 4
80.0 10.0 20.0 10.0 20.0 10.0
Внимание! Нумерация вершин и ребер начинается с нуля, так как минимальный индекс элемента массива с языках Си/С++ равен нулю.
Рис. 91. Пример схемы графа
Состояние массиваpMinWay после подготовки в функции solution перед вызовом функции PassWay{ start ) показано на рис. 92.
pMinWay 1 1
0.0
-1
0
0.0
0
0
0.0
0
0
0.0
0
0
0.0
0
Индексы вершин exist SumDist
ref (REFerence - ссылка): -1 означает конец списка start finish
Рис. 92. Состояние массива,pMinWay после начальной подготовки
В качестве задания для самостоятельной работы предлагается проанализировать работу программы и убедиться, что в результате в массиве pMin Way получится информация, показанная на рис. 93.
Важное замечание! В данном частном случае массивpMinWay указывает наилучшие пути от start до всех остальных вершин. Но в общем случае это не гарантируется. Гарантируется лишь оптималь-
285
ность пути из start ъ finish.
pMJnWay 1
0.0
-1
1
30.0
4
1
30.0
3
1
10.0
0
1
20.0
3 start finish
i'
Индексы вершин exist
SumDist
ref (REFerence - ссылка): -1 означает конец списка
Рис. 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) и результатов, получаемых из функции поиска {output), равно пяти, все же в список параметров функции следует включить только три из них, так как два исходных данных - 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.
П Р о с м о т
| р | ключ данное -* Рис. 97. Пример таблицы для последовательного поиска
size-1
lesson
type
word
work
лекция
тип
слово
работа
Поиск выполняется в полностью заполненной таблице. Просмотр таблицы выполняется последовательно, в соответствии с ростом индексов строк таблицы. Если в какой-то строке таблицы поле ключа совпадает с 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 ±nt LKEY = 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 {
} EndTab = (i > size ) ,
1 = 2*i-i-l/
return;
// Преобразование символа ключа - строчная латинская буква, // цифра или пробел - в его порядковый номер (целое число)
// Порядковый номер символа // Преобразуемый символ
±п\
(
t Kod( char
switch ( S3 {
case case case case case case case case case case case case case case case case case case case case case case case case case case case case case case case case case case
symbol )
/mbol )
' a ': 'b' : 'c' : d' :
'e' : f : g' : h'; 'i ': J'.• ;c' : 1 ' : m ': n ': o' : p': q': r' : s' : t': u': V* : w' : X' : y ' : z ': 0' : 1 ': 2': 3' : 4 ' : 5' : 6' : 7':
return return return return return return return return return return return return return return return return return return return return return return return return return return return return return return return return return return
// //
0; 1; 2; 3; 4; 5; 6; 7; 8; 9; 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 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):
А В С D Е F G Н I J
Data Data Data Data Data Data Data Data Data Data
for for for for for for for for for for
string string string string string string string string string string
0 1 2 3 4 5 6 7 8 9 (size-1)
Двоичное дерево строится, как это было ранее рассмотрено, с использованием рекуррентного подхода (см. рис. 57). Для нашего примера после начальной подготовки двоичное дерево должно иметь вид, показанный на рис. 98.
Соответственно этому алгоритму можно записать рекуррентную функцию Round и использующую ее функцию InpTabLog для начального заполнения и подготовки таблицы, прототипы и определения которых содержатся в вышеприведенной программе. Обратите внимание на то, что функция Round является вспомогательной для функции InpTabLog.
Бинарный (логарифмический) поиск в таблице, подготовленной в форме двоичного дерева. Идея поиска состоит в следующем.
1. Исходный ключ сравнивается с ключом, соответствующим корню дерева (номер соответствующей вершины дерева / = 1, а индекс элемента массива - (/ - 1) ). Если при этом ключи совпадают, то нужная строка найдена {found = 1), ее индекс line = i - 1 и поиск за-
306
вершен.
/ А
} В
Л-с
D
2 ]\
F
-V Е
10
/ н
/ 1
3
J
Последние слова по алфавиту (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).
Эффективность хэш-поиска. Проведенный для линейного апробирования анализ показал, что среднее значение числа проб при поиске (длина поиска)
WP ~ \-al2
\-а '
где a = TabLen/size есть коэффициент заполненности таблицы (табл. 30).
а L,,,
Табл. 0.1
1.056
30. Эффективность хэш 0.2
1.125
0.3
1.214
-поиска 0.5
1.5
0.9
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 ±nt cbSLr ±nt ch^jc
a; i^ jr cl, c2r c3; retcode; 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, ¶m ) != 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 "¶m; 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 Studio 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 3 4 4
*рр-а -*рр-а = *рр-а = *рр-а =
4 3 4 3
**рр **рр **рр **рр
= 14 = 13 = 14 = 13
18.7. Для подраздела 8.16
Ответ к упражнениям 1-3.
Файл LS.CPP
321
struct ELEM {
Int ELEM
}
dat; *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 ) ;
e x i t ( l ) ; } ±nt re;
// Указатель на файл ввода // Создаем список wb±le( ( ГС = fscanfC f_in, " %d ", &i ) ) == 1 ) {
Add_end( start, i ); } // Закрываем файл ±£( ( fclose( f_in ) ) =- EOF ) {
printf( "\n Файл %s не закрыт \ л " , pFilelnp ); e x i t ( 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 О - 1
при X > О ^ при X = О, при X < О
Вариант 19. Ввести и напечатать значения переменных а, Ь, с, d вещественного типа. Определить и напечатать количество нулевых значений среди заданных.
Вариант 20. Ввести и напечатать значения переменных а, 6, с, d вещественного типа, причем два из них одинаковы. Найти и напечатать значение, отличное от этих двух.
329
п. 1.1.2. Ввод в языках Си/С++. Варианты тестов
Ниже приведены варианты фрагмента программного кода, содержащего определения некоторых переменных. В комментариях к определениям переменных указано, какие значения переменных нужно ввести.
Написать фрагмент программы, обеспечивающий: • открытие файла (потока Си) '4npuf^ для работы с файлом операцион
ной системы "Test2.m'^; • ввод из этого файла (потока Си) значений переменных, указанных в
комментариях к программному фрагменту соответствующего варианта;
• закрытие файла (потока Си). Указать, как при этом будут выглядеть строки исходных дан
ных в файле операционной системы ^^Test2Jn^^ (сделайте это обязательно, иначе Ваш ответ нельзя будет проверить).
Предусмотреть контроль корректности значений, возвращаемых функциями библиотеки Си ^^foperC\ ^^fscanf\ Подключить не-обходимые стандартные заголовочные файлы.
/ / / / / / / /
4. 7 "Ой 31 12
Вариант 7.
double d; char s[ 3 ]; unsigned long uli; short si; 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-
Вариант
float
±nt
char
Вариант
float ±nt unstgnedL cbeir
Вариант
double float
Int unsigned char
Вариант
long double float long ±nt unsigned char
Вариант
float int
char
J/
5. <a. b ;
if J/ Clr c2. c3 , c4. s[20];
6.
b; J/ u; c4. s[20];
7. d; a. b; i/ J/ Clr c2. c3 , c4. s[20] ;
8.
d; a; ±; j r Clr s[20] ;
9.
a; i r
J/ Clr c2r
//
// // // // // // // // //
// // // // //
// // // // // // // // // //
// // // // // //
// // // // //
0x12
1.5 14. 7 -21 12 'y' 'P' 'a ' ' / ' "Прочитa иная-строка
14.0 12 21 'P' "Зимний-вeчер"
2.0 1.5 12.21 -21 0x12 '1 ' '2' '3' '4' n n It
1.5 1.5 -1 13 '4' ngn
1.5 21 -12 'в' 'e'
^ 1
s[20]; // "Прочмтанная-строка'
Варит
float
long^ ±nt char
чт 10.
br k; a; Clr s[20] ;
// 5.0 // 15,123 // 27 // ' B ' / / "Строка
Вариант 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 ±nt char ±nt
a г b; ir j ; clr c2r c3; 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 1 7 1 7 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 chstr
а г Ь; i / J / clr s[ 20 ];
Написать фрагмент п р о г р а м м ы , о б е с п е ч и в а ю щ и й : открытие файла (потока Си) ''Input" для работы с файлом операционной системы "Test2.in"; ввод из этого файла (потока Си) следующих значений указанных ниже переменных:
а 1 с1
• 1.5 • -21
t г
Ь . J • S .
4. 7 12 "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 double ±nt unsigned
a; Ь; i; J r
Написать фрагмент программы, обеспечивающий: • открытие файла (потока Си) '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 1 0 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 ±nt cha.r
а, b; ^r J / 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 ±nt unsigned. char
b; J, retcode; u; 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 a n t unsigned. ci iajT
Ь; jr retcode; и; c4, s[ 20 ];
Написать фрагмент программы, обеспечивающий: • открытие файла (потока Си) ''Input" для работы с файлом операцион
ной системы "Test2Jn"; • ввод из этого файла (потока Си) следующих значений указанных ни
же переменных:
и : 1 • с4:
21 12 'Р'
Ь .
S .
14. 7
"Ура-вечер"
• закрытие файла (потока Си). При этом строки исходных данных в файле операционной систе
мы "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,Offprint 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 float t cha.r
d = 254; f = 1234.56; 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. Фрагмент Си-программы:
i n t 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 . 2 3 4 Е + 0 3 )
/Стр - - ' - - - / ' - ^/мв/
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;
R:=Y; Р:=Х: i к
Q:=1;
Рис. 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):
^а<=Ь^ Да
Нет R:=X; P:=Y;
a:=d;
Рис. 103. Фрагмент схемы программы
Вариант 15. Изобразить фрагмент схемы алгоритма, соответствующий следующему фрагменту программы:
±f( с <= 1 ) а + + ; else ±f( с == 5 ) а--; else а *= 2;
Вариант 16. Записать фрагмент программы, соответствующий следующему фрагменту схемы программы (рис. 104):
V""" у Да
a:=d;
R:=X; P:=Y; i L
Рис. 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 ] ; u n s i g n e d 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 ];// Фамилия экзаменуемого i n t 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 02
16
минтай щука
сметана
20 21
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 минтай 02 щука
16 сметана
20 21
15
100 120
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 true true
Второй false true
false true
Возвраща емый результат
false false 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 ( i n t 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 dovible
}
*рЫпк;
In fo ; *start;
// NODE: узел линейного списка
// Pointer LINK: // указатель на очередной узел // INFOrmat ion: инф ормация
Написать прототип, определение и пример вызова функции, которая должна определить, сколько в линейном списке имеется элементов с отрицательными значениями. В частном случае, перед вызовом этой функции линейный список может быть пуст.
Все исходные данные (указатель на начало линейного списка) и все результаты работы функции (количество найденных элементов) должны передаваться через список параметров — это обязательное требование.
Вариант 3. Определен следующий указатель на начало линейного списка:
stxnict Node {
Node
float }
// NODE: узел линейного списка
*pL±nk; // Pointer LINK: // указатель на очередной узел
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 I
Info; *start;
// INFOrm at ion: информа ция
Написать прототип, определение и пример вызова функции, которая в процессе просмотра списка выводит данные (числа) в файл на магнитном диске "f.ouf\ одновременно освобождая память, занятую линейным списком. В частном случае, перед вызовом этой функции линейный список может быть пуст.
Все исходные данные (указатель на начало линейного списка, указатель на **имя. расширение** файла вывода) и результаты работы функции (указатель на начало линейного списка) должны передаваться через список параметров. С целью обработки ошибок предусмотреть контроль значения, возвращаемого функцией библиотеки Си ^^fopen^\ Подключить необходимые стандартные заголовочные файлы.
Вариант 9. Определен следующий указатель на начало линейного списка:
stxnjct Node {
Node float
}
*рЫпк;
Info; *start;
// NODE: узел линейного списка
// Pointer LINK: // указатель на очередной узел // INFOrm ation : информа ция
Написать прототип, определение и пример вызова функции для удаления из списка к последних элементов с освобождением занятой ими памяти. В частном случае, перед вызовом этой функции линейный список может быть пуст или может содержать любое количество элементов.
Все исходные данные (указатель на начало линейного списка, количество удаляемых элементов) и результаты выполнения функции (указатель на начало линейного списка) должны передаваться через список параметров.
Вариант 10. Определен следующий указатель на начало линейного списка:
( stjract Node
Node
float
*рЫпк;
Info; *start;
// NODE: узел линейного списка
// Pointer LINK: // указатель на очередной узел // INFOrm ation: информа ция
Написать прототип, определение и пример вызова функции
383
для удаления из списка к первых элементов с освобождением занятой ими памяти. В частном случае, перед вызовом этой функции линейный список может быть пуст или может содержать любое количество элементов.
Все исходные данные (указатель на начало линейного списка, количество удаляемых элементов) и результаты выполнения функции (указатель на начало линейного списка) должны передаваться через список параметров.
Вариант 11, Определен следующий указатель на начало линейного списка:
stmict Node {
Node
±nt }
*pLink/
In fo ; *start;
// NODE: узел линейного списка
// Pointer LINK: // указатель на очередной узел // INFOrm ati on : ин форма ция
Написать прототип, определение и пример вызова функции для вставки в линейный список после каждого элемента, в котором хранится значение find, элемента, в котором будет храниться значение add. В частном случае, перед вызовом этой функции линейный список может быть пуст или может содержать любое количество элементов.
Все исходные данные (find^ add^ указатель на начало линейного списка) должны передаваться через список параметров. С целью обработки ошибок предусмотреть контроль значения, возвращаемого операцией new.
Вариант 12. Определен следующий указатель на начало линейного списка:
stmjct Node {
}
Node
±nt
*рЫпк; In fo ; *start;
// NODE: узел линейного списка
// Pointer LINK: // указатель на очередной узел // INFOrm at Ion: информа ция
Написать прототип, определение и пример вызова функции для удаления из линейного списка элемента, следующего после каждого элемента, в котором хранится знгченне find. В частном случае, перед вызовом этой функции линейный список может быть пуст или может содержать любое количество элементов.
Все исходные данные (Jind^ указатель на начало линейного списка) должны передаваться через список параметров.
384
Вариант 13. Определен следующий указатель на начало линейного списка:
stxract Node {
}
Node
±nt
*pLink;
Info; *start;
// NODE: узел линейного списка
// Pointer LINK: // указатель на очередной узел // INFOrmation: информация
Написать прототип, определение и пример вызова функции для удаления из линейного списка элемента, предшествующего каждому элементу, в котором хранится значение y?«(i. В частном случае, перед вызовом этой функции линейный список может быть пуст или может содержать любое количество элементов.
Все исходные данные {find^ указатель на начало линейного списка) и результаты выполнения функции (указатель на начало линейного списка) должны передаваться через список параметров.
Вариант 14. Определен следующий указатель на начало линейного списка:
зЬгасЬ Node {
Node
}
*pLink;
Info; *start;
// NODE: узел линейного списка
// Pointer LINK: // указатель на очередной узел // INFOrm at ion: информа ция
Написать прототип, определение и пример вызова функции для вставки в линейный список перед каждым элементом, в котором хранится значение find, элемента, в котором будет храниться значение add. В частном случае, перед вызовом этой функции линейный список может быть пуст или может содержать любое количество элементов.
Все исходные данные (find, add^ указатель на начало линейного списка) и результаты выполнения функции (указатель на начало линейного списка) должны передаваться через список параметров. С целью обработки ошибок предусмотреть контроль значения, возвращаемого операцией new.
Вариант 15. Определен следующий указатель на начало линейного списка:
stzTict Node // NODE: узел линейного списка
385
Node
±nt }
*pL±nk;
Info; *start;
// Pointer LINK: // указатель на очередной узел / / INFOrm at ion: мн ф орма ция
Написать прототип, определение и пример вызова функции для удаления из линейного списка второго, четвертого и т.д. элементов. В частном случае, перед вызовом этой функции линейный список может быть пуст или может содержать любое количество элементов.
Вариант 16. Определен следующий указатель на начало линейного списка:
struct Node {
Node ±nt
}
*рЫпк;
Info/ *start;
// NODE: узел линейного списка
// Pointer LINK: // указатель на очередной узел / / INFOrm at ion: ин форма ция
Написать прототип, определение и пример вызова функции для модификации каждого из элементов линейного списка, в котором хранится значение find. Модификация подобных элементов заключается в хранении в них значения add. В частном случае, перед вызовом этой функции линейный список может быть пуст или может содержать любое количество элементов.
Все исходные данные (find^ add^ указатель на начало линейного списка) должны передаваться через список параметров.
Вариант 17. Определен следующий указатель на начало линейного списка:
stxract Node {
Node ±nt
}
*pLink;
Info; *start;
// NODE: узел линейного списка
// Pointer LINK: // указатель на очередной узел // INFOrm at ion: информа ция
Написать прототип, определение и пример вызова функции для удаления первого и последнего элементов списка с освобождением занятой ими памяти. В частном случае, перед вызовом этой функции линейный список может быть пуст или может содержать любое количество элементов.
Все исходные данные (указатель на начало линейного списка) и результаты работы функции (указатель на начало линей-
386
ного списка) должны передаваться через список параметров.
Вариант 18. Определен следующий указатель на начало линейного списка:
stjTuct Node {
Node
}
*pLink;
Informs tart;
// NODE: узел линейного списка
// Pointer LINK: // указатель на очередной узел // INFOrmation: информация
Написать прототип, определение и пример вызова функции для вставки новых элементов и в начало (в него помещается значение addjbeg), и в конец (в него помещается значение add_end) линейного списка. В частном случае, перед вызовом этой функции линейный список может быть пуст или может содержать любое количество элементов.
Все исходные данные {addbeg^ add_end указатель на начало линейного списка) и результаты работы функции (указатель на начало линейного списка) должны передаваться через список параметров. С целью обработки ошибок предусмотреть контроль значения, возвращаемого операцией new.
Вариант 19. Определен следующий указатель на начало линейного списка:
stxTict Node {
}
Node
±nt
*pLlnk;
Info; *start;
// NODE: узел линейного списка
// Pointer LINK: // указатель на очередной узел // INFOrmation: информация
Написать прототип, определение и пример вызова функции для вставки в линейный список после каждого элемента, в котором хранится значение/?/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 Уа-b
при при при
х<\. 1<=:X<=2,
х>2
Вариант 2. Вычислить значения функций и значение факториала
V5^tg^(arcsin(x))^^: ШЫ
у = {" Y U
при при при
jc>3. 1<х<=3,
д:<=1
п\
Вариант 3. Вычислить значения функций и сумму ряда х^ к
е . , ,ч2,33 sin'^Cx+TT/Z) Vtg(in(A:)) j 2 . i g ( x )
11 6 , 7 J C + 9 , 2 X ^ -1 ,02OC^ «рм jc<=0,
• » — ^ при x>0 ax +b-x •sin(A:)
Вариант 4. Вычислить значения функций и сумму ряда
s2 (e l^4 .3
In* (л:) у = дЯ^В -*^»^ У ) .(2.3-^^1)
J =
a-\-bz-\-C'Z < d-\-n-z-¥f-z
g-bh-z+mz
при при
в
х=\. jc-2.
остальных случаях
х-\-\ х+Ъ х+п у = + + ... +
1 3 «
Вариант 5. Вычислить значения функций и сумму ряда
_ s'm(x^+x~^+x^^^) \0~^'k
397
у =
f а-х+4
Ja(l-e-^) I 0
при при
в
x>4, 0<=х<=4,
остальных случаях
У = Е{(-1Г/(2-а+1)} а=0
Вариант 6, Вычислить значения функций и сумму ряда
2 У =
sin'^(^2,8-e^+x) V- ^^ ^ л х ^ - а "
•9,110^
Д' =
Са+Ь
" [о
при при
в
а>Ь, а<-Ь и
остальных а>0,
случаях
1 V^2,3
а=1 а!
Вариант 7. Вычислить значения функций и сумму ряда
> 75,73-я • г И)-11.7
> = г к1
< \+х 1 sin(x)
при при
в
\х\>\ и |x|<=l и
остальных
а>0. а>0.
случаях
у = Z(2.a-l)-0,5 а=1
а-\
Вариант 8. Вычислить значения функций и сумму ряда
_ (е^^^+Ь/а)'Л: (л-/) У24-23,6
к1 5/ п
У = <
с 9 z^+\g(a+b'C)/x ^/z'X'Sm(a'X)
1
при при
в
х>0. x<=0 W (x*z)>0. остальных случаях
у = 2+ I {Ы)'''^^<-;г^+~^)} а=0 Ъа-\-\ 2'а+2
Вариант 9. Вычислить значения функций и сумму ряда
У = a+b
398 7г/4-\-х' -1/J
у = (a+b+c
J a+b
[ -
при при
в
\b\<=\a\ и \b\<=\a\ и остальных
\c\<=\b\.
к1>1*|. случаях
Вариант 10. Вычислить значения функций и произведение сомножителей
у = e«(^^>(^ps in(x) -x(^" ' -x ' / ' " ) )10-3
У =
Г(ж/2)-е<-^^'=*д<^)) } Я-/2
[ (^/2)е(*-*>
при при
в
х>0. х=0.
остальных случаях
у = \\+ П(2-а+1)
Вариант 11. Вычислить значения функций и сумму ряда
-4 ^ч sin(x)'10
|^c|-arcsin(x) •(7r/2 + e^^^' (а^-х^)))
у = <
х+2
. 2 - 5
зГ х+ух
при
при
в
jc<3,
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) < sin(a)
I 0
при при
в
(а'Ь+с)>5, 0<=(а'Ь+с)<=5,
остальных случаях
Вариант 14. Вычислить значения функций и сумму ряда
У = ^
X-Z
2 X 'Z
0
при
при
в
х>0 и
х<0 и
остальных
2 2 X >Z ,
2 2 X >Z ,
случаях
у = иК2-а+\Г а=0
Вариант 15. Вычислить значения функций и сумму ряда
у = \ ' 1" при
при в
л:>0. -l<=jc<=0.
остальных случаях
У = Z^^
Вариант 16. Вычислить значения функций и сумму ряда
?'^'^*^\-Ъ'Б\Г?{Х) , , In(a^)
а""}!^ 24,61-10"
У=< \ ' 1" при
при в
jc>0. -1<=л:<=0,
остальных случаях
400
п
Вариант 17. В ы ч и с л и т ь значения функций и сумму ряда
^ . ( ^ - ) 2 - a . ( , _ 5 ) 3 . s m ( ^ . 1 0 - 5
{sin(x) при х>0,
1 при х=0, ij—x в остальных случаях
п
а=\
Вариант 18. Вычислить значения функций и сумму ряда
е~^'^+] х^ . ^ - 4
^ 1 \а \%{х 1{а-\-Ь)) при (сг+^))>0, +6|-lg(jc) при (а+Ь)<=0
у = Е8-(2.а-1) а=1
Вариант 19. Вычислить значения функций и сумму ряда
^а-х
У = f'' г U
при при при
х>3. 1<х<=3,
л:<=1
й=1 2-0+1
Вариант 20. Вычислить значения функций и сумму ряда
1п*(х)
у =
г a-x+4 \ а (1 -е -^)
1 0
при при
в
х>4. 0<=д:<=4,
остальных случаях
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.
Компилятор СИ/С++
Компоновщик (Linker)
Редактор текстов (Text
Editor)
Символический отладчик
(Debugger) Tools
Рис. 106. Интегрированная среда проектирования программ MS Visual Studio С-ь+ 6.0
Создание нового проекта для консольного прило:исения. Для того чтобы создать новое приложение (программу), необходимо создать новый проект. Для этого в IDE выполните команду New... из меню File, в результате чего на экране появится диалоговое окно New.
В этом окне необходимо выполнить следующее: • Выбрать тип создаваемого приложения. В данном случае
следует выбрать опцию Win32 Console Application, поскольку мы создаем консольное приложение, которое является Windows-аналогом старого доброго знакомого - программы для MS DOS.
• Выбрать место расположения нового проекта. Информация о расположении новой рабочей области проекта (диск:\путь\подкаталог) вводится в поле Location (местоположение).
409
Это можно сделать, набрав путь вручную, или воспользовавшись расположенной справа кнопкой Browse.. . (просмотр). Разумеется, что соответствующий подкаталог должен быть предварительно создан.
• Указать утилите Project имя файла проекта. Одновременно с вводом имени проекта в поле Project Name (имя проекта) это же имя автоматически добавляется в качестве подкаталога в поле Location.
После выполнения указанных действий для создания проекта следует нажать кнопку [ОК], в результате чего на экране появится диалоговое окно мастера создания консольного приложения. В этом окне выбираем переключатель 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 Workspace войти в каталог проекта и "кликнуть" по файлу с расширением .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> или Compile 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 | Star t Debug | Go, либо нажав клавишу <F5>, то все операторы программы, предшествующие точке останова, будут выполняться в обычном режиме и только перед строкой останова выполнение программы приостановится.
При этом внешний вид интегрированной среды разработки существенно изменится. Во-первых, изменипся состав основного меню. Во-вторых, строка, которая будет выполняться следующей, будет отмечена желтой (по умолчанию) стрелкой. И, наконец, появится два новых окна - Variables (переменные) и Watch (наблюдение), которые позволяют просматривать и менять значения переменных. Если одно из окон или оба окна на экране отсутствуют, то их можно поместить на экран, используя комбинации клавиш <Alt-4-3> для переменных и/или <Alt+4> для наблюдения.
Для пошагового выполнения в отладчике имеются следующие команды.
• Debug I Step Over (шаг через) или эквивалентно <F10> -выполняет текущий оператор или функцию и переходит к следующей строке.
• Debug I Step Into (шаг внутрь) или эквивалентно <F11> -выполняет текущий оператор языка C++ или переходит к первому оператору функции.
а Debug | Step Out (шаг вне) или эквивалентно <Shif t+Fl l> -завершает выполнение текущей функции и переходит к строке, непосредственно следующей за ее вызовом.
• Debug I Run to Cursor (выполнить до курсора) или эквивалентно <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
main() Главная функция
Proglnfo() Информация о программе и командной
строке
ReadMatrix() Чтение матрицы
WriteMatrix() Печать матрицы с
заголовком
SwapUnits() Перестановка
элементов строк
Файл ErWarnW.cpp Файл CheckCS.cpp
ErrorWarningWork() Обработка ошибок и
предупреждений
CheckComString() Контроль командной
строки
OpenFile() Открытие файла
Файл Matrix.срр ^ Вызов функции
Возврат из функции
1, 2, ..., 6 - порядок вызова функций
Функция OpenFile() вызывается из функций ErrorWarningWork(), ReadMatrix() и WriteMatrix()
Функция CloseFile() вызывается из функций ErrorWarningWork(), ReadMatrix() и WriteMatrix()
Функция ErrorWarningWork() вызывается из функций ReadMatrix() и WriteMatrix()
CloseFile() Закрытие файла
Файл FileOC.cpp
Рис. 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,
Строка сообщения 'е' - ошибка, сообш,ение выдается
на экран, '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
char char
Msg[ ], Type,
// // // //
"\п Ошибка %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.PRJ TASK02.RES. Используется в
// Стандартные включаемые файлы и прототипы функций ^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), именованные пространства имен (namespace), определения типов (class, struct), объявления и определения шаблонов (template).
Заголовочные файлы никогда не должны содержать определения невстроенных функций, определения данных (объектов), определения массивов и неименованные пространства имен.
2. Обратить внимание на оформление функций и, особенно, на оформление заголовка функции.
3. Обратить внимание на оформление заголовочных комментариев файлов программного проекта.
Приложение П.б. Примерная программа дисциплины "Программирование и основы алгоритмизации".
МИНИСТЕРСТВО ОБРАЗОВАНИЯ РОССИЙСКОЙ ФЕДЕРАЦИИ
УТВЕРЖДАЮ Начальник Управления образовательных программ и стандартов высшего и среднего профессионального образования
2000 г.
ПРИМЕРНАЯ ПРОГРАММА д и с ц и п л и н ы
'Программирование и основы алгоритмизации"
Рекомендуется Минобразованием России для подготовки бакалавров по направлению 5502 "Автоматизация и управление" (и подготовки специалистов по
направлению 6519 "Автоматизация и управление")
434
1. Цели и задачи дисциплины Цель дисциплины состоит в поэтапном формировании у студентов следую
щих слоев знаний и умений, • Слой 1: знание основных понятий программирования. • Слой 2: знание базового языка программирования. • Слой 3: умение решать задачи на вычислительных машинах ВМ.
Формированию отмеченных уровней (слоев) знаний и умений соответствуют разделы дисциплины. Изучение курса предполагает, что студенты знакомы с принципами работы ВМ, десятичной, двоичной, восьмеричной и шестнадцатеричной системами счисления, а также основными понятиями информатики.
2. Требования к уровню освоения содержания дисциплины В результате изучения дисциплины студенты должны:
1. Знать основные понятия программирования. 2. Знать базовый язык программирования. 3. Уметь решать задачи на ВМ.
Примечание. Слой 3 в полном объеме и слой 4 знаний и умений -умение проектировать программное обеспечение — формируются у студентов в процессе изучения дополнительной дисциплины "Технология программирования", которая является органическим продолжением данной.
3. Объем дисциплины и виды учебной работы Вид учебной работы
Общая трудоемкость дисциплины Аудиторные занятия Лекции Практические занятия (упражнения) Курсовая работа Самостоятельная работа Вид итогового контроля (зачет, экзамен)
Всего часов 130 68 34 17 17 62
Экзамен
Семестр 2 2 2 2 2 2 2
4. Содержание дисциплины 4.L Разделы дисциплины и виды занятий
iN» п/п
1 2
3
Раздел дисциплины
Основные понятия профаммрфования Си/С+-1-: базовый язык программирования (алфавит, синтаксис, типы данных, выражения и операции, структурное программирование, операторы ветвлений и циклов, массивы и структуры, модальное программирование, функции, ввод и вывод, описатели класса хранения, области действия и время жизни данных, указатели, препроцессор) Прикладное профаммирование (сортировки массивов, рекурсия и итерация, транспортная задача, элементы обработки списков, работа с динамической памятью)
Лекции
* •
•
ПЗ (упражнения)
*
ЛР (курс, рабо
та)
*
*
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