Текст
                    Р. Уинер
Язык
ТУРБО СИ
Издательство
«Мир»


Turbo С at Any Speed Richard S. Wiener University of Colorado at Colorado Springs John Wiley & Sons New York • Chichester • Brisbane • Toronto • Singapore
Р. Уинер Язык ТУРБО СИ Перевод с английского М.П.Матекина под редакцией канд. фнз.-мат. наук В.В.Мартынюка Москва «Мир» 1991 pusto1.rrm.ru pustol @yrg.kuzbass.ret
ББК 32.97 У37 УДК 681.3 аигл.-М.: Мир, 1991. - Увиер Р. У37 Язык Турбо Си: Пер. с 384 с, ил. ISBN. 5-03-001779-8 Книга американского специалиста посвящена описанию входного языка компилятора Турбо Сн - одной нэ наиболее удачных реализаций популярного языка программирования Си, предназначенного для персональных компьютеров, работающих с операционной системой MS-DOS. Подробно рассматриваются синтаксис и семантика языка Турбо Сн, анализируются типичные ошибки использования его конструкций. Приводится множество примеров программ, которые не только поясняют синтаксис н семантику Турбо Си, но и подсказывают конкретные, наиболее целесообразные формы использования отдельных его конструкций. Для программистов и пользователей персональных ЭВМ. 2404010000-040 041(01)-91 124-91 ББК 32.97 Редакция литературы по информатике и робототехнике ISBN 5-03-001779-8 (русск.) ISBN 0-471-63478-6 (англ.) © 1988 John Wiley & Sons, Inc. All rights reserved Autorized translation from English language edition published by John Wiley & Sons, Inc. © перевод на русский язык, М.П.Матекнн, 1991.
Предисловие редактора перевода Язык Си давно завоевал особую популярность у программистов благодаря уникальному сочетанию возможностей языков высокого и низкого уровни. Знаменательной вехой его эволюции явилось поивление системы Турбо Си, получившей признание как одно из самых удачных инструментальных средств для современных персональных компьютеров. Система Турбо Си разработана фирмой Borland International для микропроцессоров серии 8086 (в частности, дли IBM PC и совместимых с ними ПЭВМ), работающих с операционной системой MS-DOS. В этой системе, согласно концепции Турбо, применяется новая схема быстрой компиляции, проводимой в значительной мере в оперативной памити с редкими обращениями к магнитным дискам. Качественно новый эффект быстрой компиляции делает экономичной многократную модификацию программ. В системе Турбо Си внесены некоторые изменения в сам язык Си, новая версия которого называется тоже Турбо Си. Данная система органично сочетает лучшие свойства концепции организации Турбо, воплощенной к настоящему времени применительно к ряду популирных языков программирования, и концепции языка Си, первоначально предложенной Б. Керниганом и Д. Ричи и впоследствии развитой в стандарте ANSI и в Турбо Си. Система Турбо Си предоставляет пользователю интегрированную среду для поддержки всех этапов разработки и отладки разнообразных средств программного обеспечения, от небольших программ до крупных проектов. Она качественно облегчает труд программиста, обеспечивая ему удобство автоматизированного прохождения всего «жизненного цикла» программы с автоматическим выявлением широкого класса ошибок и выдачей предупреждений. Все режимы, от компиляции до отладки реализуются нажатием одной-двух клавиш непосредственно из «окна» текстового редактора. В целом система Турбо Си обеспечивает самые разнообразные потребности современного программирования. Следует отметить, что в этом отношении с нею может успешно конкурировать коммерческая версии Турбо Паскаля, но принципиальное отличие состоит в том, что успех последней базируется на существенных отклонениях от стандарта Паскали, тогда как система Турбо Си реал изо-
6 Предисловие редактора перевода ваиа в основном в рамках стандарта ANSI, хотя и включает рид дополнительных языковых средств. В частности, одним из удобных отличий языка Турбо Си является наличие в нем вложенных комментариев. Важная особенность системы Турбо Сн состоит в возможности переключения ее в режим стандарта, когда исходный код воспринимается только по правилам стандарта ANSI. Богатые библиотеки функций обеспечивают доступ к возможностям операционной системы и аппаратуры иа различных уровнях. Все перечисленные выше особенности системы Турбо Си являютси выигрышными по сравнению с конкурирующей с нею системой Си фирмы Microsoft, хотя Турбо Си и уступает несколько последней по качеству изготовляемого кода программ. Основная цель, преследуемая в предлагаемой читателю книге, состоит в изложении основ языка Турбо Сн и демонстрации определенного стили программирования на этом изыке (и на Си). Описываемые лингвистические возможности синтаксиса и семантики Турбо Си на всем протяжении книги иллюстрируются тщательно подобранными примерами программ, которые подсказывают конкретные, наиболее целесообразные формы использования отдельных изыковых конструкций. Излагаемый материал сопровождается указанием типичных ошибок, связанных с применением той нли иной рассматриваемой конструкции языка. Наиболее характерные ошибки перечислены в приложении. В процессе перевода содержащиеся в книге программы проверены на персональном компьютере IBM PC с использованием версии 2.0 компилятора для Турбо Си и версии MS-DOS 3.30. В результате проверки в программы внесены отдельные исправления. Книга представляет несомненный интерес дли весьма широкого круга читателей: от начинающих программистов, впервые знакомящихся с Си, до высококвалифицированных специалистов в области программного обеспечения. В. В. Мартынюк
Предисловие Язык Си широко применяется при современном профессиональном программировании. В сущности, большинство программистов предпочитают использовать язык Си для своих серьезных разработок. Привлекают такие особенности изыка, как свобода выражения мыслей, мобильность и чрезвычайнаи доступность. Что касаетси языка Турбо Си, то наша фирма Borland создала его развивая язык Си; в результате получили идеальные средства для разработки программ. Мы сделали изык более мощным и универсальным, упростили процесс конструирования программ, что позволило пользователям создавать системный инструментарий, утилиты и прикладные программы быстрее и проще, чем когда бы то ни было. Невероятно высокая скорость иомпиляции при высоком качестве объектного кода, получаемого с помощью Турбо Си, не имеет аналогов. Такие великолепные перспективы программировании открывает студентам Ричард Уинер. Студенты смогут детально ознакомиться с языком, тогда как опытные программисты воспользуются программами и примерами, демонстрирующими возможности Турбо Сн. И те и другие по достоинству оценят эффективность и изящество Турбо Си. Мы поздравлием Ричарда с превосходной работой и желаем читателям успехов в программировании. Филипп Каан Президент фирмы Borland International
Дорогой жене Шейле и детям Эрику и Марку посвящается От автора Книга рассчитана иа программистов с различным уровнем знания языка Си, а также на желающих изучить Си программистов с опытом работы на других изыках. Начальное изложение Турбо Си ведется в манере, доступной для новичков. Множество примеров программ иа Турбо Си облегчит н ускорит изучение языка неопытными программистами. ч Впрочем, и опытные программисты откроют для себи много нового в программах и примерах, демонстрирующих как мощные средства Турбо Си, так и способы разработки программного обеспечении. Книга начинается с последовательного введения в Турбо Си. Главы 1 и 2 содержат ивчальные примеры программ, предназначенные для быстрого ознакомлении читателей с внутренней структурой и возможностями Турбо Си. Далее рассматриваютси компоненты, составляющие программу на Турбо Си, подробно описываютси структуры данных и управлении, операции, функции, типы и классы памяти, препроцессор Турбо Си, многие библиотеки Турбо Си. Большинство глав содержит многочисленные примеры. Я глубоко признателен своему сыну Эрику Уинеру за помощь и проверку правильности материала книги и за многие полезные советы. Автор благодарит Девииа Бен Гура за полезные обсуждения. Хочется выразить благодарность Диане Серра, редактору издательства John Willey & Sons, за ее помощь и поддержку. Я глубоко признателен фирме Borland International за выдающуюся работу, давшую великолепный язык - Турбо Си. Ричард С. Уинер Колорадо Спрингс, Колорадо
Глава I Обзор и введение 1.1. Языки Си и Турбо Си Если судить по числу зарезервированных слов, то Си - это маленький изык, и поэтому существует мнение, что его сравнительно легко выучить. В то же времи Си является исключительно эффективным изыком. Используй небольшое число конструкций Си, можно строить крупные и сложные программные системы и решать трудные задачи. Сам изык Си и стандартные библиотеки, поставлиемые с большинством реализаций языка, являются базовыми средствами для создания повторно используемых программных компонентов, которые можно применить во многих приложениях. По мнению автора этой книги Си - не самый лучший язык дли начального ознакомлении с программированием. Существуют более простые и менее мощные языки программирования, такие, квк Бейсик и Паскаль, использование которых позволиет освоить хороший стиль программирования и не требует от программиста детального зиакомстаа с конкретным компьютером. Но Си наряду с тем, что он поддерживает многие из тех же стилей программирования, что и более простые языки, дает возможность программисту осуществлять непосредственный доступ к ячейкам памяти и регистрам компьютера, требуя, впрочем, при этом знания особенностей компьютера. По сравнению с менее мощными и более простыми языками Си предоставляет программисту высокий уровень гибкости, возлагая на него более высокую ответственность. Увеличение гибкости может способствовать построению компактного и эффективного исполняемого кода, но может И привести к запутанной и тяжело читаемой программе. Поэтому программист должен ответственно относиться к написанию на Си легкочнтаемых н технологичных программ. Дли эффективного использования Си требуются обучение, опыт и навыки. Если сравнивать Си с такими большими изыками, как ПЛ/1 и Ада, то оказывается, что последние предоставляют программисту гораздо больше возможностей благодаря встроенным операциям н конструкциям. Но эти преимущества часто обеспечиваются за счет значительных потерь в ско-
10 Глава 1 рости компиляции, снижения эффективности объектного кода и увеличения его размера. Си был разработан Деннисом Ричи в лаборатории Bell в начале семидеситых годов. Таким образом, это относительно старый изык. Первоначальная реализация Си была выполнена для ЭВМ PDP-11 фирмы DEC. Может быть, благодаря этому факту Си до сих пор ассоциируют с малыми компьютерами, хотя ои был перенесен на машины всех классов. Си сравнивают с автомобилями марки Порше: скромный, быстрый, удобный. Называют Си иногда и вероломиым- здесь иет противоречия. Именно те средства, которые позволяют- писать иа Си быстрые и компактные программы, делают эти программы беззащитными для класса ошибок, от которых другие языки защищают. Мы рассмотрим возможные источники ошибок в программах на Си и дадим рекомендации, позволиющие избегать этих ошибок. Если Си - это Порше, то Турбо Си - это Турбо Порше. Код, генерируемый Турбо Си, в общем более компактен, чем у большинства популярных реализаций Си, он ис- полняетси быстрее, чем для большинства лучших систем программировании Си, и исключительно быстро генерируется при компилиции. Такой набор преимуществ является необычным. Часто сверхбыстрая компиляции осуществляется за счет плохой оптимизации кода - но это к Турбо Си ие относится. Существует ряд причин, по которым Си стал весьма популярен в последние годы. Основная из них состоит в высокой скорости выполнении получаемого кода и его ком- пвктностн, что особенно ценно для профессиональных приложений, ориентированных нв использование микро- н миии- компыотеров, дли которых и до иастоищего времени быстродействие и объем памити являютси критическими показателями. Экономичность изобразительных средств и богатый набор операторов - вот источник популярности Си среди программистов. Эти же характеристики порождают потенциальные проблемы - недостаточную читабельность текста программы. На самом деле недостаточная читабельность - это ие проблема самого Си, а скорее проблема программирующего на Си, и оиа может и должна быть устранена. При работе над настоящей книгой пришлось много потрудиться для того, чтобы иа примерах программ иа Турбо Си проде-
Обзор и введение 11 монстрировать хороший стиль оформления программ. Хотя изначально и традиционно Си ассоциировался только с операционной системой UNIX™, позже он был перенесен в среду многих операционных систем и теперь обособился и существует независимо от любой из них. Первоначальнаи реализация Турбо' Си была выполнена для операционной системы MS-DOS™, поэтому вопросы, связанные с программированием на «нижнем уровне», излагаются для указанной операционной системы. Предполагнется, что в будущем Турбо Си станет доступен в операционной системе OS/2 фирмы Microsoft и, возможно, в других ОС. В последние годы часто при вводе в строй новой рабочей станции или нового процессора первыми двумя реализованными трансляторами оказываются ассемблер для данного процессора и компилятор с языка Си. Другие трансляторы с языков высокого уровня обычно появляются позже. Это одновременно и следствие, н причина популярности языка Си. Си можно назвать изыком ассемблера высокого уровня. Это вполне правомерно, так как Си открывает программисту доступ к «внутренностям» компьютера - битам, байтам и регистрам, управляющим работой центрального процессора и внешних устройств. Но Си все-таки представляет собой нечто большее, чем изык ассемблера высокого уровня. Блочная структура программы на Си обеспечивает как защиту данных, так и высокий уровень контроля за областями действия и видимости переменных. Си поддерживает многие важные структуры данных высокого уровня и управляющие структуры, ставшие привычными в современных языках программировании. Как уже отмечалось, на Си можно создавать большие и сложные программные системы. Си - относительно мобильный язык. Программы на Си, написанные для данной операционной системы и для конкретного компьютера, зачастую можно перенести в другую операционную систему или на другой компьютер либо с минимальными изменениями, либо вовсе без них. Сказанное отиоситси и к библиотекам, которые сопутствуют большинству реализаций Си. Поскольку Си - довольно маленький по объему язык, большая часть его мощности заключена именно в библиотеках, поддерживаемых компилятором, и библиотеках, создаваемых программистами. Так, например, простейшие операции ввода и вывода на терминал не ивля-
12 Главе 1 ются частью языка, и они обычно выбираются из библиотеки stdio (стандартный ввод/вывод), поставляемой с большинством систем программирования Си. К счастью для программистов, язык- Си, развиваясь, вышел на такую ступень, что его удалось стандартизировать. Библиотеки Си тоже стандартизированы. К моменту написания этой кииги (середина 1987 г.) работа над стандартом Си, выполняемая Американским национальным институтом стандартизации (ANSI), близилась к завершению. К уже существующему де-факто стандарту Си, предложеииому Керииганом и Ричи (K&R), добавились некоторые новые и важные возможности. Эти же новые возможности стандарта ANSI были включены и в Турбо Си. Кроме того, в Турбо Си имеются специальные средства, поддерживающие программирование для процессоров семейства 80x86 под управлением операционной системы MS-DOS. Некоторые из этих новых средств описаны в гл. 11 и 12. Турбо Си был реализован фирмой Borland International весной 1987 г. Турбо Си - профессиональней система программирования на Си, включающая богатый набор библиотек, обеспечивающих поддержку как программировании на Си вообще, так и программировании для микропроцессора 8086 в операционной системе MS-DOS, автономный компилятор и загрузчик, интегрированный редактор-компилятор-загрузчик, утилиту make построении программ. Дли семейства процессоров 80x86 с ограниченным размером сегмента используемой памяти (64К) обеспечивается поддержка шести отдельных моделей памяти. Турбо Си явлиетси первым компилятором с Си для микропроцессоров семейства 80x86, который полностью реализует будущий стандарт ANSI дли Си. Ниже описываются отличия нового стандарта ANSI для Си от существовавшего ранее стандарта K&R- • Целые константы Добавлено новое ключевое слово signed, указывающее иа то, что соответствующий объект имеет знак. • Константы с плавающей точкой Введен новый тип коистант для арифметики с плавающей точкой - long double. В версии 1.0 это описание трактуетси как задание двойного (удвоенного)
Обзор и введение 13 размера отводимой под хранение объекта памяти. В последующих версиях для хранения объектов этого типа будет отводиться 10 байтов. • Командные строки препроцессора (директивы) Директивы *include и *line могут содержать макровызовы. Если макровызов присутствует в директиве, то он расширяется прежде, чем директива начнет выполниться. • Описания внешних объектов Описания внешних объектов, указываемые ключевым словом extern и расположенные в теле функции, подчиняются обычным правилам области действия описаний. Эти описании ие действуют вне блока, в котором они определены. • Прототипы описаний функций Это одно из наиболее существенных отличий от старого стандарта K&R для Си. Ключевые слова function prototype указывают спецификатор функции, содержащий имя функции и список формальных параметров функции, заключенный в круглые скобки. Прототипы функций используются для проверки допустимости типов и числа параметров в вызове соответствующей функции. Программисты, желающие отключить этот новый тип контроля, могут свободно пользоваться старым вариантом описания функций по стандарту K&R. • Макро Вложенные макровызовы, упомянутые в строке описания макро, расширяются только в момент расширения самого макро, а не при определении макро. • Препроцессор Препроцессор поддерживает некоторые новые директивы, макро и новые символы в макро. К новым возможностям относятся: макро _DATE_,_STDC_,_TIME_,_FILE_,_LINE_j директивы #еггог, #pragma; последовательность символов ## , которая предписывает конкатенацию двух элементов в макро.
14 Гмм 1 • Преобразования типов Добавление новых типов, отсутствующих в стандарте K&R, потребовало расширения правил умолчания для преобразования типов в выражениях со смешанными типами. • Тип значения, вырабатываемого функцией обработки прерывания Модификатор type interrupt для функции задает тип значения, возвращаемого функцией обработки прерывания. • Модификация типа Модификаторы const и volatile относятся к объектам адресного типа. Const используется для определения объектов данных, значения .которых не могут изменяться. Volatile определяет объекты, которые могут быть изменены функцией обработки прерывания или модифицированы при оптимизации структуры программы за счет создания оверлея. Некоторые возможности Турбо Си, не включенные в стандарт ANSI. • Комментарии При желании комментарии могут вкладываться друг в друга. Это может существенно помочь при отладке. • Специальные ключевые слова для микропроцессоров семейства 80x88 asm, __cs, _ds, _es, _ss, cdecl, far, huge, interrupt, near, pascal, _AX, _AH, _AL, _BX, _BH, _BL, _CX, _CH, _CL, _DX, _DH, _DL, _BP, _DI, _SI, _SP. • Препроцессор Препроцессор поддерживает некоторые новые директивы, макро и новые символы в макро. К новым возможностям относятся _СОМРАСТ_, __HUGE_, _LARGE_, _MEDIUM_, _MSDOS__, _SMALL_, _TINY_, _TURBOC_.
Обзор и введение 15 • Код ассемблера С использованием ключевого слова asm можно включать в текст программ на Турбо Си фрагменты ассемблерного кода. 1.2. Немного об этой книге (Турбо) Си - гибкий язык, позволяющий быстро писать эффективные программы. Вследствие гибкости (Турбо) Си программист может создавать компактный, быстроиспол- няемый, но труднопонимаемый код или же легкочитаемый, но неэффективный код. При определенном навыке и мастерстве программист может написать на (Турбо) Си весьма быстроисполняемую и компактную программу, сделав ее к тому же и легкочитаемой. Книга ориентирована как на начинающих, так и на более опытных программистов на Си, желающих изучить (Турбо) Си или углубить понимание языка Си и выучить Турбо Си. Предполагается, что читатель имеет навыки программирования на каком-либо языке высокого уровня и обучен компьютерной грамоте. Книга посвящена описанию языка Турбо Си и его использованию для построения программных систем. Отдельные разделы книги могут служить для изучении самого изыка Си. Новые возможности, включенные в неопубликованный пока стандарт ANSI для Си, а также специфические средства Турбо Си вводятся и рассматриваются по ходу изложения. Новейшие и специальные средства Турбо Си излагаются в конце книги. Множество программ и фрагментов программ демонстрируют как возможности языка Турбо Си, так и методику программирования. Автор убежден, что эффективность и универсальность Турбо Си можно лучше всего продемонстрировать на примерах. Начинающему программисту примеры помогут учиться языку. Программист с большим опытом работы на Си сможет совершенствовать свои знания, тщательно разбирая более сложные примеры. Для программ, представленных в этой книге, характерен логичный стиль программирования. Много сил пришлось потратить на то, чтобы тексты программ были наглядными и доступными для понимания. Более сложные программы, имеющиеся в книге, позволяют решать задачи из важных прикладных областей, инфор-
16 Глав* 1 матики. Цель книги - не только научить читатели языку Турбо Си, но и показать ему принципы разработки программ на (Турбо) Си. В некоторых примерах показано, как сложность программы может быть уменьшена за счет декомпозиции на интерфейсные модули и модули реализации. Эта книга не заменяет документация «Turbo С User's Guide», разработанной фирмой Borland, которая содержит полное описание системы. Поэтому и не рассматриваются вопросы использовании редактора Турбо Си, интегрированного компилятора-загрузчика, автономных компилятора и загрузчика, утилиты make. Соответствующие описания приведены в указанной документации. Читателю, которого интересуют более сложные приложения и способы построения на Турбо Си повторно используемых программ, можно рекомендовать обратиться к книге «Crafting Turbo С Software Components and Utilities» (Wiener, 1988). 1.3. Самая первая программа на Турбо Си, которая не печатает «Привет, Мир» Стало традицией, что в книгах, описывающих Си, (возможно, для демонстрации простоты этого языка) в качестве самой первой приводится программа, выводящая фразу: «Привет, Мир». В настоящей книге мы нарушим традицию. Как уже отмечалось в разд. 1.1, хотя (Турбо) Си - компактный язык, он не такой уж простой. И наша самая первая программа призвана это показать. Наша первая программа невелика по размеру, однако выгодно отличается от традиционной программы типа «Привет, Мир». Текст программы приведен на листинге 1.1. Несмотря на то, что первая программа короткая, в ней используются некоторые возможности языка, описываемые в гл. 5 и 6: строковые переменные, многомерные массивы и логические операции. Цель программы - показать начинающему программисту изящество (Турбо) Си. Листинг 1.1. first - первая программа иа Турбо Си /* Первая программа на Си first.с */ «include <stdio.h> «include <string.h>
Обзор и введение 17 main( int argc, char* argv[I ) ^ if ( ( argc == 2 ) && ( ( strcmpt argvl 1 I, "Кааи" ) == 0 ) II ( strcmp( argvl 1 I, "Филипп" ) == 0 ) ) ) printff "\n\n5Js снова сделал это на Турбо Си.\п", argvl 11); else printff"\n\nBorland снова сделал это на Турбо Си.\п"); > Ниже будут кратко пояснены конструкции языка, приведенные на листинге 1.1. После прочтения пояснений рекомендуется вернуться к листингу и подробно проанализировать текст программы. Текст /* Первая программа на Си first.с */ является комментарием и игнорируется компилятором с Турбо Сн. Комментарии, которые в Турбо Си (но не в Си) могут вкладываться друг в друга, начинаются с пары символов /* и заканчиваются парой символов */. Две директивы «include в начале программы являютси указаниями компилятору подставить на их место текст из файлов stdio.h (стандартный ввод/вывод) и string.h (обработка строк). Эти подставлиемые файлы содержат описания интерфейсов с функциями, включенными в две важные и широко используемые библиотеки. В библиотеку stdio.h включены функции и макро, обеспечивающие выполнение стандартных действий по вводу/выводу (I/O). Библиотека string.h содержит общеупот- ребимые функции манипулирования с символьными строками. Как видно из приведенной программы, функции управления вводом/вы водом и манипулирования со строками вынесены из языка (Турбо) Си. Для осуществления операций ввода/вывода или действий со строками программист может либо использовать библиотеки, поставляемые фирмами-изготовителями программного обеспечения, либо создавать такие библиотеки самостоятельно и пользоваться ими. Строка исходного текста mainf int argc, char* argvl 1 ) описывает формальные параметры (аргументы), передаваемые функции main. По требованиим (Турбо) Си описание
18 Глм« 1 функции main должно присутствовать в каждой программе, и оно всегда предваряет исполнительную часть первого блока текста программы. Перечисленные параметры (int argc, char* argv[]) обеспечивают доступ к одному или нескольким аргументам, разделяемым пробелами и задаваемым в командной строке вызова программы first, обеспечиваемого интерпретатором команд DOS. Например, пользователь может инициировать работу программы first, используя вызовы cfirst Кааи» или «first Филипп». В обоих случаях значение argc (счетчик аргументов) будет равно двум. При вызове программы ее. имя всегда задается первым аргументом командной строки. В версиях DOS 3.x на первый аргумент командной строки указывает переменная argv[0]. В более раиних версиях DOS имя самой программы недоступно, и в Турбо Си значение argvf_0] полагаетси в этом случае равным "с". Стандарт ANSI предписывает, что argv[0] должен указывать на пустую строку (""), если ими программы недоступно. Итак, для DOS 3.x строка, определяемая argv[0], - это "first". При первом обращении к программе в argv[l] будет содержаться строка "Каан", а при втором "Филипп". Следующий оператор исходного текста для улучшения внешиего вида программы разбит на части и размещен в нескольких строках. Он осуществляет логическую проверку того, равно ли значение argc двум и равно ли значение argv[l] строкам "Каан" или "Филипп". Если условие истинно, то выполняется оператор, стоящий за рассмотренным оператором проверки условия if: printf( "\n\n5Js снова сделал это на Турбо Си.\п", argvl 11); Оператор printf выводит строку, содержащуюся в argfvfl]. на месте шаблона %s. Последовательность управляющих символов \п\п переводит курсор на экране дисплея на две строки вниз перед выводом строки символов. Эффективная и универсальная функция вывода printf размещена в библиотечном файле stdio.c и части библиотек Турбо Си; она подробно описана в гл. 10. Если проверяемое условие ложно, то выполниется оператор вывода
Обзор и введение 19 printf( "\n\nBorland снова сделал это на Турбо Си.\п"); В условном операторе if ( ( argc == 2 ) S& ( ( strcmpt argvt 1 ], "Каан" ) == 0 ) 11 ( strcmp( argvl 1 1, "Филипп" ) == 0 ) ) ) используются логические операции И (обозначается символом &&) и ИЛИ (символ !!). а также функция сравнения строк strcmp из библиотеки string. Замечание Большинство программ, приведенных в этой книге, заканчивается выводом пустой строки (возврат каретки, перевод строки) непосредственно перед завершением исполнения программы. Это реализуется либо при помощи оператора вывода printf ("\n"); либо добавлением в конец последнего выполняемого в программе оператора вывода сямвола "\п\ Дело в том, что после завершения работы программы, выполняемой под управлением Интерпретатора команд Турбо Си (интегрированной среды), выводится системное сообщение «Hit any key to continue.» («Нажмите любую клавишу для продолжения работы.»). Если в программе отсутствует завершающий вывод пустой строки, то указанное сообщение затрет последнюю выведенную программой строку. 1.4. Программа на Турбо Си для возбуждения аппетита Хотя настоящая книга и организована преимущественно по принципу «от простого к сложному», будет полезно как можно раньше представить программисту, только ещй начинающему работу на (Турбо) Си, обзор возможностей этого языка. Материал настоящего раздела преследует две цели. Здесь рассматривается интерфейсный файл (util.h), обеспечивающий связь с программами из полезной вспомогательной библиотеки, и демонстрируется реализация этой библиотеки (файл util.c). В указанной вспомогательной
20 Глав* 1 библиотеке содержатся следующие функции: очистка экрана; позиционирование курсора на экране; получение текущей позиции курсора; удаление мерцающего курсора с экрана; восстановление мерцающего курсора на экране; получение текущего времени компьютера в часах, минутах, секундах и сотых долих секунды; выполнение хронометража работы фрагмента программы при помощи команд rpttiming ( begin ) - начать хронометраж и rpttiming ( end ) - завершить хронометраж; непосредственный ввод с клавиатуры (так называемый chot-key» ввод); ввод строки символов, завершенной нажатием клавиши ввод (ENTER); центрирование строки на экране терминала; проверка ответа пользователя yes/no (да/нет) с учетом того, что ответ можно сократить до у, Y, n или N; приостановка выполнения программы до тех пор, пока пользователь не нажмет клавишу пробела; эффективное генерирование случайных равномерно распределенных величин - вещественных нз диапазона от 0 до 1 и целых из диапазона от low до high. Некоторые из перечисленных функций будут использованы в последующих примерах. Исходный текст в файле util.c (листинг 1.3) демонстрирует новичку некоторые особенности языка. Показано, как выглидят в языке вызовы функций операционной системы высокого уровня (DOS) и присваивания значений переменным, связанным с регистрами процессора 8086. Читатель может вернуться к анализу листингов 1.2 и 1.3 после прочтения гл. 1-6. Листинг 1.2. Интерфейс с библиотекой общеупотребимых программ /* Интерфейс к библиотеке util.c Файл util.h */ er.um time type { begin, end >; void getxy( unsigned *h, unsigned *v ); void gotoxy( unsigned h, unsigned v ); void clrscreen( void ); void remove_cursor( void ); void restore_cursor( void ); void gtimef unsigned «hour, unsigned «minute, unsigned *sec, unsigned *hund ); void rpttiming! enum timetype p ); float rand_real( void );
Обзор и введение 21 int random! int low, int high ); void get_Jkey( .unsigned char *ch ); void readstringl unsigned char s[] ); void centermessage( unsigned char sN ); void spacebar! void ); int keypressedf void ); int yes( void ); Листинг 1.З. Реализация общеупотребимых программ /* Слукебный пакет общеупотребимых программ Файл lit 11. с Версия Турбо Си */ «include <stdio.h> «include <dos.h> «include "util.h" static const char null = '\0'; static const int max = 32767; static int ins = 0; static unsigned hi, h2, ml, m2, si, s2, hundl, hund2; static unsigned seedl, seed2; static int first = 1; void getxyf unsigned *h, unsigned *v ) ( union REGS regs; unsigned int result; regs.x.ax = 0x300; regs.x.bx = 0x0; int86( 0x10, &regs. &regs ); result = regs.x.dx; *v = result / 256; *h = result - *v * 256; ) void gotoxy( unsigned h, unsigned v ) < union REGS regs; if ( ( h >= 0 ) && ( h <= 79 ) && ( v >= 0 ) && ( v <= 24 ) ) if ( ( h != 79 ) II ( v != 24 ) )
22 Глава 1 { regs.x.ax = 0x200; regs.x.bx = 0x0; regs.x.dx = h + 256 * v; int86( 0x10, &regs, &regs ); } > void clrscreenl void ) /» Использует ANSI.SYS, в версиях DOS 2.1, 3.0, 3.1 и 3.2 очищает экран и позиционирует курсор в левый верхний угол. */ { unsigned esc = 27; printf( "У.с12Г/.сЮ;0{", esc, esc ); ) static void convert( unsigned char *ch ) /• Используется для преобразования клавиш, имеющих расширенные ASCII коды ( функциональные клавиши и клавиши, нажатые совместно с ALT ), в коды ASCII на верхнем регистре. Эти значения могут быть объявлены константами, соответствующими определенным клавишам. */ { switch ( *ch ) < case case case case case case case 59 60 61 62 63 64 65 *ch = break, *ch = break; *ch = break *ch = break; *ch = break; •ch = break- *ch = break: 128 129 130 131 132 133 134
m <n a 8 О ел 1ЛСОС-Г--«->тЧ,С'ЭООСМСОСПО С ПППСОСОСОСОСОСОСОСОСОГ- .« и II j« II ji« II i< II it II j< II i< II it II j« II it II i< II i< II it II ji« II II it cdcdcdcdcdcdcdcdcdcdcdcdcd cd £ai£ai£«£tl£ID£tlx:il£tl£l>£tl£ai£tl£lll£llltl Ot_Ot_Ot_Ot_Ot_Ot_G£-,Gt.Ot.Ot.Ot.Ot.Ot.OCt_ CO CO Ф en rd о e- со a> ca cd и 00 CO CO ca cd и t-t t~ ф Cfl cd и CM e- Ф СЛ cd о n f- co Cfl cd о CO e- Ф Cfl cd о e- e- Ф Cfl cd и СП t— си Cfl со и о 00 ф Cfl cd и *н 00 ф Cfl cd о n 00 ф Cfl cd о tn н ф Cfl cd о см 00 ф СП rd о э cd с» си ■о с cd J3 о •а си с 00 ■Г-* Cfl с э *»* >» си ■ч Ч-» си 00 ■о чЧ о > V .к Cfl оо си с СЛ о ы Р5 с о •■н с э л Cfl оо си и .. оЯ .. .. 00 t-t ~~ х - cd О Cfl . О 00 .С и си .м с сл и С о8 ОО cd со J3 ■ *- и о ли • • О II и -а *- M*J £ СО С О См Сн -Н » .*Н *^* *> го X о II JS cd f СП оо си с
24 Глам 1 intdos( &regs, &regs ); •ch = regs.h.al; convert( ch ); > else /• Переводит управляющие коды в значения символов ASCII на верхнем регистре, которые могут быть заданы в виде констант. */ switch ( *ch ) { case case case case case case case case case case case case case case case case 1 : 2 : 3 : 4 : 5 : 6 : 7 : 10: 11: 12: 14: 15: 16: 17: 18: 19: •ch = break •ch = break •ch = break *ch = break *ch = break- *ch = break; •ch = break; *ch = break; *ch = break; •ch = break; •ch = break; *ch = break; *ch = break; •ch = break; •ch = break; *ch = break; 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153
Обзор и введение 25 case 20: »ch = 154 break; case 21: *ch = 155 break; case 22: »ch = 156 break; case 23: »ch = 157 break; case 24: *ch = 158 break; case 25: *ch = 159 break; case 26: *ch = 160 break; case 30: »ch = 171 break; default : ; > void readstring( unsigned char s[1 ) { unsigned int index = 0; unsigned char ch; do { get„key( &ch I; if ( ch == 8 ) { if ( index ) ( index—; printfl "\b \b" ); > > else { printf( "Xc", ch ); si index++ 1 = ch; } } while ( ch != 13 ); si --index ] = null; >
26 Глава 1 int keypressedt void ) union REGS regs; regs.h.ah = OxOB; intdost &regs, &regs ); returnt regs.h.ah == 255 ); void centermessaget unsigned char sN ) unsigned int x, y; getxyt &x, &y ); gotoxy( 40 - strlent s ) / 2, у ); printft "Zs", s ); > void spacebart void ) unsigned char ch; gotoxy( 0, 24 ); centermessaget "Для продолжения нажмите пробел ->" ); do { get_key( &ch ); } while ( ch !='' ); } int yes( void ) /* Возвращает 1. если введено Y или у, и 0, если N или п */ { unsigned char ch; do < get_key( &ch ); ) while ( ( ch != 'y' ) && ( ch != 'Y' ) && ( ch != 'n' ) && ( ch != "N* ) ); printft ",Ac", ch ); returnt ( ch == 'Y' ) II ( ch == 'y' ) );
Обзор и недение 27 void gtimet unsigned «hour, unsigned «minute, unsigned *sec, unsigned *hund ) < union REGS inreg, outreg inreg.h.ah = 0x2C; intdos( &inreg, &outreg ); • hour = outreg. h.dh; •minute = outreg. h.dl; •sec = outreg.h.ch; •hund = outreg. h.cl; void rpttimingl enum timetype p ) { if ( p == begin ) gtime( &hl, &nl, &sl, &hundl ); if ( p == end ) { gtime( &h2, &m2, &s2, &hund2 ); if ( hund2 < hundl ) { hund2 += 100; s2 -= 1; > if ( s2 < si ) { s2 += 60; m2 -= 1; } if ( m2 < ml ) { m2 += 60; h2 -= 1; > if ( h2 < hi ) h2 += 24; printf ("\n\nXd часов, '/А минут, '/.А секунд, '/.А сотых\п", h2 - hi, s2 - si, m2 - ml, hund2 - hundl ); } > float rand_real( void )
28 Глада 1 unsigned h, m, s, hu; /• Используются для получения текущего времени */ unsigned с; float г; int index; /* Этот блок выполняется один раз */ if ( first ) -{ gtime( &h, &m, &s, &hu ); seedl = hu + m + 2 • s; seed2 = h + s*m + s; seedl •= 2; seed2 *= 2; if ( seedl > max ) seedl -= max; if ( seed2 > max ) seed2 -= max; /* Прокрутка генератора, г дальше не используется */ first = 0; for ( index = 1; index <= 30; index*+ ) r = rand_real(); У с = seedl + seed2; if ( с > max ) с -= max; с ♦= 2; if ( с > max ) с -= max; seedl = seed2; seed2 = c; return ( ( float ) с / 32767.0 ); int randomt int low, int high ) { float r, t; int c; г = ( float ) high - ( float ) low + 1.0; t = г * rand_real(); с = ( int ) t; return ( low + с ); void remove_cursor( void )
Обзор и введение 29 { union REGS regs; regs.x.ax = 0x100; regs.h.ch = 32; regs.h.cl = 0; int86( 0x10, &regs, &regs ); } void restore_cursor( void ) ( union REGS regs; regs.x.ax = 0x100; regs.h.ch = 12; regs.h.cl = 13; int86( 0x10, &regs, &regs ); > 1.5. Стиль написания программы Поскольку программы на (Турбо) Си можно писать в свободном формате, для обозначевия конца каждого оператора требуется ставить специальный разделитель - точку с запятой. Компилятор с Турбо Си не разбирается в сти- лих написания программ. Способы оформления программы, ее стиль служат для облегчения понимания программы человеком, поскольку программы в большей степени ориентируются на человека, чем на компилятор. Программа отражает способ решения даииой программистской задачи человеком. Вероятнее всего, программа станет со временем менятьси - будут исправляться ошибки, добавляться новые возможности. Такой процесс сопровождения программы зачастую охватывает большую часть времени и стоимости всего жизненного цикла программы. Хорошее оформление программы способствует ее легкому и надежному сопровождению. На листинге 1.4 представлена та же самая программа first, но при ее наборе не применялось форматирование. Компилятор с Турбо Си понимает эту программу без труда а как вы? Читатель должен заметить, что директивы «include обязательно располагаются каждая в отдельной строке.
30 Глам 1 Сколько существует программистов, столько существует и программных стилей. Невозможно переоценить важность использования логичного и ясного стиля написания программы. Выбор стиля - важный и интересный вопрос, возникающий перед каждым программистом, начинающим писать программы на (Турбо) Си. Если над большим программным проектом работает несколько программистов, то часто им всем полезно принять общий стиль. Это может помочь уменьшить количество ошибок и дает возможность всем разработчикам проекта лучше ориентироваться во всем проекте в целом. Для автоматизации форматирования созданы специальные средства, такие, как форматизаторы программ и средства получения аккуратно оформленных печатных документов. Программист должен чувствовать удовлетворение от вида хорошо оформленной программы и стремиться создавать именно такие программы. Листинг 1.4. Программа first на Турбо Си без форматирования (♦include <stdio.h> (♦include <string.h> main( int argc, char» argvl] ) < if ( ( argc == 2 ) && ( ( strcmp( argvl 1 1, "Каан" ) == 0 ) 11 ( strcmpl argvl 1 ], "Филипп" ) == 0 >>> printf( "\n\n5Ss снова сделал это на Турбо Си.\п", argvl 1 1 1; else printf( "\n\nBorland снова сделал это на Турбо Си.\п"); }
Глава 2 Турбо Си: обзор возможностей В настоящей главе вводятся многие базовые понятия (Турбо) Си. Конструкции языка, описываемые в этой главе, используются в иллюстративных программах с самого начала изложения материала. Детально функции не рассматриваются до гл. 8. Однако, чтобы читатель начал понимать программы на (Турбо) Си, функции будут введены в настоящей главе. В последующих главах проводится более подробное рассмотрение конструкций языка, упомянутых в этой главе, а также многих возможностей языка, не отмеченных здесь. Для того чтобы как можно быстрее приобщить читателя к программированию на Турбо Си, мы включили в эту главу простые, ио законченные программы, содержащие достаточно много основных конструкций языка. 2.1. Препроцессор, компилятор н загрузчик Для того чтобы исходная программа иа (Турбо) Си была оттранслирована и переведена в исполняемый машинный код (файл с расширением .ЕХЕ в операционной системе MS-DOS), она должна пройти через три процесса: препроцессирование, компиляцию и загрузку (сборку). В задачи препроцессора входит подключение при необходимости к данной программе на (Турбо) Сн внешних файлов, указываемых при помощи директивы #include (см. разд. 2.3), и расширение макро (см. разд. 2.4). Подробнее о препроцессоре можно узнать из гл. 11. Компилятор за несколько этапов транслирует то, что вырабатывает препроцессор, в объектный файл (файл с расширением .OBJ), содержащий оптимизированный машинный код, прн условии, что не встретились синтаксические н семантические ошибки. Если в исходном файле с программой на (Турбо) Си обнаруживаются ошибки, то программисту выдается их список, в котором ошибки привязываются к номеру строки, в которой они появились. Программист циклически выполняет действия по редактированию/компиляции до тех пор, пока не будут устранены все ошибки в исходном файле. Для ознакомления со специфическими командами цикла редактирования/компиляции советуем обратиться к документации cTurbo С User's Guide» фирмы
32 Глава 2 Borland. Загрузчик связывает между собой объектный файл, получаемый от компилятора, с программами из требуемых библиотек и, возможно, с другими файлами. В результате сборки получается файл с расширением ЕХЕ (ЕХЕ-файл), который может быть исполнен компьютером. ЕХЕ-файл может быть запущен на выполнение из командной строки DOS. Для ознакомления со специфическими командами вызова загрузчика обратитесь к документации «Turbo С User's Guide» фирмы Borland. 2.2. Комментарии Текст на (Турбо) Си, заключенный в скобки /* и */, компилятором игнорируется. Например, в программе, взятой из листинга 1.1, фрагмент кода, включающий оператор else, будет игнорирован компилятором. Если логическое выражение в операторе if ложно, то программа ничего ие напечатает. /* Программа first.с с фрагментом кода, отключенным комментарием */ «include <stdio.h> «include <string.h> main( int argc, char* argvl] ) < if ( ( argc == 2 ) && ( ( strcmp( argvl 1 1, "Каан" ) == 0 ) 11 ( strcmp( argvl 1 1, "Филипп" ) == 0 ) ) ) printf ( "\n\n4s снова сделал это на Турбо Си.\п", argvl 11); /* else printf ( "\n\nBorland снова сделал это на Турбо СиАп" ); ♦/ ) Комментарии служат двум целям: документировать код и облегчить отладку. Так, если в программе на (Турбо) Си используется некоторый специальный алгоритм, то бу-
Турбо Си: обзор возможностей 33 дет уместен комментарий, содержащий ссылку иа книгу или статью, в которой описывается этот алгоритм. Вообще говоря, в программу следует вносить любой текст, проясняющий код. Но комментариями не нужно и злоупотреблять. Сделать код иа (Турбо) Си - самодокументированным можно, используя разумные соглашения по именованию переменных, функций и т.д. Если, например, функция названа vector_sum (т.е. сумма_вектора), то нерационально включать в программу вслед за ее заголовком комментарий, о том, что /* Функция вычисляет сумму элементов вектора. */ Имя функции поясняет назначение функции. При использовании автономной версии компилятора Турбо Си ключ -С разрешает вложение комментариев. (См. «Turbo С User's Guide».) Вложенные комментарии не разрешены в стандарте Си ANSI, и поэтому предупреждаем читателя, чтобы он использовал эту возможность аккуратно. Вложенные комментарии особенно полезны при отладке программы. Если программа работает не так, как надо, то иногда оказывается полезным закомментировать часть кода (т.е. выиести ее в комментарий), заново скомпилировать программу и выполнить ее. Если теперь программа начнет работать правильно, то, значит, закомментированный код содержит ошибку и должен быть исправлен. Если программа ие заработает, отключаются новые фрагменты. Поскольку и сам исходный текст может содержать комментарии, то рассматриваемая возможность Турбо Си, позволяющая превращать в комментарий код, уже содержащий комментарии, позволяет быстро удалить фрагмент кода из текста программы. 2.3. Директивы Include Во многие программы иа (Турбо) Сн подставляются одни или несколько файлов, часто в самое начало кода главной программы main. Появление директив «include <файл_1> «include "файл_2" «Include <файлл> 2 Зак. 795
34 Глава 2 приводит к тому, что препроцессор подставляет на место этих директив тексты файлов файл_1, файл_2, .... файл_п соответственно. Если имя файла заключено в угловые скобки < >, то поиск файла производится в специальном разделе подстановочных файлов. Имя этого раздела задается пользователем (подробнее см. «Turbo С User's Guide»). Обычно в этот раздел помещаются все файлы с расширением . h . Если в разделе отсутствует искомый файл, то препроцессор выдает сообщение об ошибке и трехступенчатый процесс, описанный в разд. 2.1, прерывается. Если имя файла заключено в двойные, кавычки (как, например, "файл_2"), то поиск файла производится вначале в текущем разделе. Если здесь файл не обнаруживается, то система переходит к поиску файла в разделе подстановок. Если и здесь требуемый файл не будет найден, то препроцессор выдаст сообщение об ошибке н трехступенчатый процесс будет прерван. Предостережение Обычная ошибка - добавление после директивы «include точки с запятой. В отличие от многих других операторов (Турбо) Си директива include не должна оканчиваться точкой с запятой. 2.4. Макро С помощью директивы #define, вслед, за которой пишутся имя макро и значение макро, оказывается возможным указать препроцессору, чтобы ои при любом появлении в исходном файле на (Турбо) Си данного имени макро заменял это имя на соответствующее значение макро. Макро могут иметь параметры. Например, макро «define square( х ) ( ( х ) * ( х ) ) задает замену символа square(apryMeHT) на значение (аргумент) * (аргумент). Подробнее о макро будет сказано в гл. 11. Часто макро используют для того, чтобы увязать идентификатор и значение. Как только препроцессор встречает идентификатор, он заменяет его на соответствующее значение. Например, директива
Турбо Си: обзор возможностей 35 «define pi 3.U15926 связывает идентификатор pi со значением 3.1415962 . Предостережение Не оканчивайте значение макро (например, 3.1415926) точкой с запятой. Значение макро подставляется вместо имени макро полностью. Если точка с запятой присутствует, то оиа будет подставлена вместе с числом. 2.5. Оператор Printf: вывод на терминал Одной из основных задач программирования является вывод в файл илн на терминал. Без вывода информации программа не имеет возможности ничего сделать. В (Турбо) Си весь вывод реализуется через внешние функции и макро. Одной из наиболее универсальных и полезных функций вывода является printf. Она включена во внешнюю библиотеку stdio (стандартный ввод/вывод). Функцию printf можно использовать для вывода любой комбинации символов, целых и вещественных чисел, строк, беззнаковых целых, длинных целых и беззнаковых длинных целых. Типичный пример использования функции printf: prlntf( "ЧпВоэраст Эрика-Xd. Его доход $y..2f", age, Income ); I Предполагается, что целой переменной age (возраст) и вещественной переменной income (доход) присвоены какие-то значения. Последовательность символов "\п" переводит курсор на новую строку. Последовательность символов "Возраст Эрика-" будет выведена с начала новой строки. Символы %й - это спецификация для целой переменной. Вместо %d подставляется значение переменной age. Следующая литеральная строка "Его доход $". %2f - это спецификация (символ преобразования формата) для вещественного значения, а также указание формата для вывода только двух цифр после десятичной точки. Так выводится значение переменной income. Как видно из рассмотренного примера, спецификации помещаются внутри печатаемой строки. Вслед за этой 2*
36 Глава 2 строкой должен стоять нуль или более переменных, разделенных запятыми. Каждой спецификации в операторе printf должна соответствовать переменная соответствующего (адекватного) типа. Если используется несколько спецификаций, то всем им должны соответствовать переменные того типа, который задается спецификацией. Более формально спецификацию % можно определить следующим образом: %[флаг(и)1 I ширина1 I.точность1 11,L] символ_формата где I или L используются для указания целых длинных типов, а ширина - минимальный размер поля вывода. Значение флага Назначение Выравнивание по левому краю поля + Вывести знак значения - как плюс, так и минус Пусто Для неотрицательных значений вместо знака плюс вывести пробелы Тип значения Точность Целое Число цифр Вещественное Число цифр после десятичной точки Строка Число символов Символ формата Тип выводимого объекта %с %s %d %о %u %х %ld %Io %Iu %lx %f %e %g char строка int int (в восьмеричном виде) unsigned int ' int (в шестнадцатеричиом виде) long (в десятичном виде) long (в восьмеричном виде) unsigned long long (в шестнадцатеричиом виде) float/double (с фиксированной точкой) float/double (в экспоненциальной форме) float/double (в виде f или е в зависимости от значения)
Турбо Си: обзор •оэможиостей 37 %lf long float (с фиксированной точкой) %Ie long float (в экспоненциальной форме) %Ig long float (в виде f или е в зависимости от значения) Подробное описание функций семейства printf дается в разд. 10.1.24. На листинге 2.1 приведена программа, демонстрирующая некоторые возможности (Турбо) Си, рассмотренные в разд. 2.1-2.5. Листинг 2.1. Использование оператора printf iinclude <stdio.h> #define square(x) ( ( x ) * ( x ) ) «define pi 3.1415926 main() < float x = 2.5; int i = 11; int j = 119; printf( "ЧпЗначение квадрата 2.5 = %10.4f", square( x ) ); printf( "ЧпЧисло пи = %10.4f\ pi ); printf( "ЧпЗначение 2 ♦ пи = %10.4f\ 2.0 * pi ); printft "ЧпЧпШестнадцатеричный вид числа 11 = %x", i ); printff "ЧпЧпШестнадцатеричный вид числа 119 = их", J >; printf( "ЧпЧпВосьмеричный вид числа 119 = %оЧп". j ); ) Предлагаем читателю самому поэкспериментировать с оператором printf. Предостережение Частая ошибка - отсутствие соответствующей переменной для каждой спецификации в строке printf. Другая частая ошибка - несоответствие типа переменной спецификации. Обычно это происходит тогда, когда в строке мио-
38 Глава 2 го спецификаций и программист путает порядок требуемых переменных. 2.6. Оператор Scant: ввод с клавиатуры Оператор scanf является одной из многих функций ввода, имеющихся во внешних библиотеках. Подробно функции ввода и вывода будут рассмотрены в гл. 10. Каждой вводимой переменной в строке функции scanf должна соответствовать спецификация. Перед именами переменных следует поставить символ &. Этот символ означает «взять адрес». Подробнее о ссылках говорится в гл. 6 и 8. На листинге 2.2 приведен пример использования оператора scanf. Листинг 2.2. Пример использования оператора scanf «include <stdio.h> mainO < int weight, /* вес ♦/ height; /* рост */ printf ( "Введите ваш вес: " ); scanf ( "%d", «.weight ); printf ( "Введите ваш рост: " ); scanf ( "Xd", «.height ); printf ( "\n\nBec = /id, рост = Xd\n", weight, height ); ) 2.7. Функция Main Каждый исполняемый файл системы (Турбо) Си (программа) должен содержать функцию main. Эта функция может ие иметь параметров, и тогда ее описание - main(). Если функция main имеет параметры, как, например, на листинге 1.1, то эти параметры выбираются из строки вызова и их значения - строки символов. В этом случае заголовок main имеет вид main ( int argc, char* argvN ) .
Турбо Си: обзор возможностей 39 Дальнейшее рассмотрение вопросов, связанных с командной строкой, отложим до гл. 6. Код, задающий тело функции main, заключается в фигурные скобки { "и }. Общая структура функции main тако- аа. Bain О { /* Код, реализующий main */ ) О стиле Настоятельно рекомендуется сдвигать текст, заключенный в фигурные скобки, иа один или более пробелов вправо от уровня фигурных скобок. Фигурные скобки обрамляют блок. Отступ внутри блока облегчает чтение и сопровождение программы. Предостережение После описания функции main не следует ставить точку с запятой. 2.8. Типы данных В (Турбо) Си переменные должны быть описаны, а их тип специфицирован до того, как эти переменные будут использованы. При описании переменных применяется префиксная запись, при которой вначале указывается тип, а затем имя переменной. Сказанное можно продемонстрировать иа описаниях простых переменных int number_of_jnarbles, number_of_pebbles; float weight; int exam_score; char ch; В первом описании имеется список переменных, содержащих два имени (number_of_marb!es и number_of_pebb!es). Обе переменные описываются как целые (int). Переменная examscore целого типа и описана
46 Глава 2 отдельно, хотя ее можно и добавить к первому списку целых переменных. С типом данных связываются и набор предопределенных значений, и набор операций, которые можно выполнять над переменной данного типа. Детали такого механизма будут рассмотрены в гл. 4. Переменные можно инициализировать в месте их описания. Поясним сказанное на примерах int height =71; float income = 26034.12. /* к 1 Мая */ bank_balance = 4052.67, /♦ к 15 Апреля ♦/ time; Переменным height, income и bank_balance присваиваются начальные значения, а переменной time не присваивается. О стиле Читабельность программы повышается, если при задании списка переменных в каждой строке размешается по одному имени переменной и все имена выравниваются по первому символу. При использовании такого соглашения вслед за именем переменной остается место для комментария. Простейшими скалярными типами, предопределенными в (Турбо) Си, являются char Представляетси как одиобайтовое целое число int Двубайтовое целое long Четырехбайтовое целое float Четырехбайтовое вещественное double Восьмибайтовое вещественное В гл. 4 представляются и описываются еще некоторые скалярные типы, такие, как unsigned (беззнаковый) и unsigned char (беззнаковый символьный).
Турбо Си: обзор возможностей 41 2.9. Функции Процесс разработки программного обеспечения предполагает расчленение сложной задачи иа набор более простых задач и заданий. В (Турбо) Си поддерживаются функции как логические единицы (блоки текста программы), служащие для выполнения конкретного задания. Важным аспектом разработки программного обеспечения является функциональная декомпозиция, за последние годы получившая широкое распостраиеиие. Большинство современных языков программирования высокого уровня поддерживают функциональную декомпозицию. Функции имеют нуль или более формальных параметров и возвращают значение скалярного типа, типа void (пусто) или указатель. При вызове функции значения, задаваемые иа входе, должны соответствовать числу и типу формальных параметров в описании функции. Если функция ие возвращает значения (т.е. возвращает void), то оиа служит для того, чтобы изменять свои параметры (вызывать побочный эффект) или глобальные для функции переменные. Подробнее этот аппарат будет рассмотрен в гл. 6 и 8. Стандарт ANSI Си и Турбо Си поддерживают прототипы функций. Назначение прототипов функций можно проиллюстрировать на примере функции, возвращающей куб ее вещественного аргумента. double cube( double x ) | { return x • x * x ; > Аргумент х типа double специфицируется вслед за первой открывающей скобкой. В соответствии с более старым стандартом K&R Си описание этой функции выглядело бы так: double cube( x ) double x; < return x * х * х ; )
42 Глам 2 В Турбо Си для достижения совместимости прежних Программ на Си вверх разрешено использовать старый стандарт K.&R. Тем ие менее мы настоятельно рекомендуем читателям применять новейшие прототипы функций, поскольку при этом на этапе компиляции осуществляется более жесткий контроль типов. На листинге 2.3 показана короткая, но сложная программа на Турбо Си, использующая функции (в частности cube). Листинг 2.3. Программа иа Турбо Сн, вычисляющая кубы чисел «include <stdio.h> «define pi 3.1415926 malnO { extern double cube( double x ); printf ( "\n 2 e кубе = X.Of". cube( 2.0 ) ); priotf ( "\пПи в кубе = X.10f\n", cube( pi ) ); > double. Cube( double x ) { return x * x » x ; ) I Описание extern, помещенное в функцию main, является ссылкой вперед, позволяющей использовать функцию cube в функции main. Такое описание функции не должно противоречить спецификации функции при ее тел?. Ключевое слово extern можно опускать, ио сама ссылка вперед на описание функции является обязательной. Иначе говоря, оператор double cube! double x ); без ключевого слова extern является допустимым. Прн использовании старого стандарта K&R в описании функции не указывались типы формальных параметров. Описание extern для функции cube из листинга 2.3 должно
Турбо Си: обзор возможностей 43 выглядеть так extern double cubed; или double cubeO; При описании функций программист на Турбо Си имеет выбор. Описание в соответствии со стандартом K&R блокирует проверку типов параметров, и в этом слабость такого способа описания. Описание в соответствии с новым стандартом ANSI заставляет компилятор выполнять проверку типов параметров. Предположим, что первый вызов функции cube иа листинге 2.3 будет заменен на printf ( *\n 2 в кубе = %.0f\ cube( 2.0, 7.0 ) ); Эта ошибка при использовании старого внешнего описания по стандарту K.&R не будет обнаружена компилятором. Если же используется новый способ описания прототипов функций, то компилятор отметит эту строку кода как содержащую ошибку, связанную с несоответствием параметров. Вылавливать ошибки на этапе компиляции гораздо практичнее, чем на этапе выполнения. Таким образом, чем раньше обнаруживается ошибка, тем дешевле ее исправить. Предлагаем читателю вернуться к листингу 1.2, где содержатся прототипы для всех функций из модуля util. Программа, использующая какие-либо функции, описанные в файле util. h. должна включать оператор «include "util. h" для обеспечения ссылок вперед на все вспомогательные библиотечные функции. Эти функции задаются в файле util.с, приведенном на листинге 1.3. Альтернативным вариантом может быть описание только тех функций из модуля util, которые используются в данном приложении. Подробнее о функциях и передаче параметров можно узнать в гл. 6 и 8. Предостережение Частая ошибка - постановка точки с запятой вслед за правой скобкой в описании функции. За правой скобкой в описании функции должна следовать открывающая фигурная скобка.
44 Глам 2 2.10. Логическая организация простой программы иа Турбо Си (Турбо) Си предоставляет необычайно высокую гибкость для физической организации программы или программной системы. В табл. 2.1 показана типичная организация небольшой программы на Турбо Си. Заметим, что обычно (ио не обязательно) первой по порядку в тексте программы функцией является функция main. Таблица 2.1. Организация нескольких маленьких программ иа Турбо Си /• Заголовки и комментарии, описывающие программу */ /* Директивы include */ #include имя_файла_1 ♦include имя_файла_л /* Макро ♦/ ♦define макро_1 значение_1 ♦define макро_д значениеjn /• Описание глобальных переменных •/ тип_даниых глобальная_деременная_1; тип_данных глобальная_переменная_п; mainO { /* Описания extern, обеспечивающие ссылку вперед Иа функции и используемые в теле функции main •/ /• Описания локальных переменных •/ тип_данных локальная_переменная_1; тип_данных локальная_переменная_и; /* Тело функции main */ )
Турбо Сн: обзор возможностей 45 /• Функции, используемые в программе main •/ Тип_данных имя_функции_1( формальные параметры ) ( /• Описания extern, обеспечивающие ссылку вперед на функции и используемые в теле данной функции •/ /• Описания локальных переменных •/ тип_данных локальная_деременная_1; тип_данных локальная_переменная_и; /* Тело функции - 1 */ } Тип_данных имя „.функции _п( формальные параметры ) < /* Описания extern, обеспечивающие ссылку вперед на функции и используемые в теле данной функции п •/ /• Описания локальных переменных •/ тип ^данных локальная_переменная_1; тип_данных локальная_переменная_г; /* Тело функции п */ } Структура каждой функции совпадает со структурой главной программы (main). Поэтому функции иногда еще называют подпрограммами. Подпрограммы решают небольшую и специфическую часть общей задачи. 2.11. Локальные и глобальные переменные Читателя может удивить тот факт, что одни переменные описываются вне функции (глобальные переменные), а другие - внутри функции (локальные переменные). Классы памяти рассматриваются в гл. 8. Глобальные переменные являются видимыми (т.е. дос-
46 Глав* 2 тупнымн) во всем файле (модуле), в котором они описаны. Они также видимы для загрузчика и, таким образом, для внешних модулей. На листинге 2.4 приведена простая программная система иа Турбо Си, состоящая из двух файлов (модулей) - first.с (главный файл) и second.с. Две глобальные переменные а и b типа int описаны вне функции в файле second.с. Следовательно, они видимы для загрузчика Турбо Си. Поскольку в файле first.с присутствует описание extern для этих переменных типа int, то компилятор разрешает использовать их в функции main. Внешние ссылки уточняются загрузчиком. Программисту следует указать загрузчику, чтобы он при выполнении компиляции и сборки программы собрал вместе объектный код, помещенный в файлы first.obj и second.obj. Для этого в командной строке DOS нужно набрать ТСС first.с second.с Оператор extern int a,b можно поместить и вне функции main. В этом случае переменные а и b будут видимы во всем модуле first.с. Если оператор extern int a,b помещен внутри функции main, то переменные будут видимы только внутри этой функции. Листинг 2.4. Маленькая программная система на Турбо Си, использующая глобальные переменные /♦ Файл first.с */ mainO { extern int a, b; printft "\na = Xd b = Zd\n". a, b ); ) /* Файл second.с */ int a = 2, b = 5; Если перед описанием int a=2, b=5 в файле second.с поместить обозначение класса памяти static, то указанные переменные скрываются от загрузчика. Хотя программа в файле first.с будет успешно оттранслирована (компиля-
Турбо Си: обзор возможностей 47 тор учтет описание extern), загрузчик не сможет уточнить внешние ссылки и выдаст соответствующее сообщение об ошибке. 2.12. Операторы и операции Основу языка (Турбо) Си составляют операторы. Оператором-выражением называют выражение, вслед за который стоит точка с запятой. В (Турбо) Си точки с запятой используются для разделения операторов. Программа иа листинге 2.5 содержит оператор, который не делает ничего полезного. Листинг 2.5. Пример бесполезного оператора «include "util.h" main!) { int a, b, c; c/b; } Оператор c/b не возвращает значения и поэтому бесполезен. Компилятор Турбо Си предупреждает пользователя о таких семантических неточностях при помощи следующих предупреждающих сообщений: Warning tect.c 5: Code has no effect in function main (Предупреждение в test.с 5: Код функции main не вырабатывает значения ) Uarning test.с 5: Possible use of 'c' before definition in function main {Предупреждение в test.с 5: Возможно, что переменная 'с* используется до ее определения в функции main) Warning test.с 5: Possible use of 'b' before definition in function main (Предупреждение в test.с 5: Возможно, что переменная 'b' используется до ее определения в функции main) Программисту следует тщательно проанализировать все предупреждающие сообщения, выдаваемые компилятором Турбо Си. Если программа будет скомпилирована, собрана
48 Глава 2 И даже будет сформирован ЕХЕ-файл, не гарантируется, что программа будет работать корректно. Рекомендуем пользователю обратиться к документации фирмы Borland «Turbo С Users's Guide», чтобы узиать, как можно заставить компилятор и загрузчик выдавать все возможные предупреждающие сообщения. После этого попытайтесь понять причину возникновения всех таких сообщений. Принято группировать все операторы в следующие классы: присваивание, вызов функции, ветвление и цикл. В операторе присваивания используется операция присваивания = , например: с = а * Ь; Действие такого оператора можно описать следующими словами: «с присваивается значение а, умноженного иа Ь». Значение, присваиваемое переменной с, равняется произведению текущих значений переменных а и Ь. Операторы часто относятся более чем к одному из четырех классов. Например, оператор if ( ( с = cube( а ♦ Ь ) ) > d ) составлен из представителей следующих классов: присваивание, вызов функции и ветвление. Гибкость (Турбо) Си, позволяющая смешивать в одном операторе операторы разных классов, делает язык весьма выразительным и дает возможность экономить выражения. Однако если злоупотреблять предоставляемыми возможностями, то может получиться правильный, ио запутанный код. К понятию оператора вплотную примыкает понятие операции. Различают следующие группы операций (Турбо) Си: арифметические операции, операции отношения, логические операции, побитовые операции, операции присваивания, операция вычисления размера (sizeof) и операция следования (запятая). 2.12.1. Арифметические операции К арифметическим операциям относятся: сложение (+), вычитание (-), деление (/), умножение (*) и остаток (%). Все операции (за исключением остатка) определены для переменных типа int, char и float. Остаток не
Турбо Сн: обзор возможностей 49 определен для переменных типа float. Целочисленные сложение и вычитание выполняются без учета переполнения. В программе иа листинге 2.6 будут выведены следующие значения: 32767 и -32768. Листинг 2.6. Арифметическое переполнение «include "util.h" main!) { int a = 32767; printft "ЧпЗначение а = Xd. Значение а + 1 = '/.d\n", a, a + 1 ); ) Для беззнаковых целых добавление единицы к наибольшему целому дает в результате нуль. Сложение и вычитание с плавающей точкой выполняются с учетом возможного переполнения и потери точности. Все арифметические операции с плавающей точкой производятся над операндами двойной точности. Это приводит к некоторому замедлению вычислений. После того как получен результат с двойной точностью, он приводится к типу левой части выражения, если левая часть присутствует. Переполнение и потеря точности проявляются в момент выполнения программы и вызывают прерывание в программе. Более подробно арифметические операции рассматриваются в гл. 4. 2.12.2. Операции отношения В языке определены следующие операции отношения: проверка на равенство (==), проверка на неравенство (1=), меньше (<), меньше или равно (<=), больше (>), больше или равно (>=). Все перечисленные операции вырабатывают результат типа int. Если данное отношение между операндами истинно, то значение этого целого - единица, а если отношение ложно, то нуль. Все операции типа больше-меньше имеют равный приоритет, причем он выше, чем приоритет операций == и 1=. Приоритет операции присваивания ниже приоритета всех операций отношения. Для задания правильного порядка вычислений используются скобки.
50 Глава Рассмотрим следующий пример: if ( ( ch = getcharO ) > 'а' ) Функция getchar() возвращает символ из входной строки. Присваивание этого символа переменной ch выполняется до того, как переменная ch будет сравниваться с символом 'а'. Благодаря гибкости (Турбо) Си оператор ветвления в рассмотренном примере будет выполнен и в том случае, если убрать пару скобок вокруг ch=getchar(). Компилятор будет интерпретировать получившийся оператор следующим образом: Символ, получаемый от getcharO, сравнивается с 'а'. Если он больше 'а', то переменная ch принимает значение единица. В противном случае - нуль. Такая гибкость может приводить к непроизвольным программным ошибкам. Поскольку компилятор Турбо Си может обработать много вариантов оператора, программисту следует тщательно проверять сложные выражения в своей программе, задавая при этом вопрос: «Что должно означать это выражение иа самом деле?» Предостережение Обычной ошибкой для новичков, особенно для тех, кто переходит с программирования на Паскале к (Турбо) Си, является использование вместо операции сравнении на равенство == операции присваивания =. Традиционный компилятор с языка Си не оказывает программисту никакой помощи по отысканию такой логической ошибки. К счастью, компилятор Турбо Си выдает предупреждающее сообщение, на которое программисту нужно отреагировать. Следующая короткая программа иллюстрирует рассмотренную ошибку и диагностическое сообщение, выдаваемое компилятором Турбо Си: #include <stdio.h> mainO < int a = 5,
Турбо Сн: обзор возможностей 51 b = 6; if ( а = b ) printf ( "а равно b\n" ); else printf ( "а не равно b\n" ); ) Warning test.с 8: Possibly incorrect assignment in function main (Предупреждение test.с 8: Возможно некорректное присваивание в функции main) Программа выведет а равно b Будет вычислена неверная ветка в операторе if else, поскольку оператор If ( а = b ) истинен, если b ие равно нулю, независимо то того, равны ли а и Ь. 2.12.3. Логические операции В языке имеются три логические операции: && операция И (and) II операция ИЛИ (or) ! отрицание Аргументами логических операций могут быть любые числа, включая задаваемые аргументами типа char. Результат логической операции - единица, если истина, и нуль, если ложь. Вообще все значения, отличные от нуля, интерпретируются как истинные. Логические операции имеют низкий приоритет, и поэтому в выражениях с такими операциями скобки используются редко. Вычисление выражений, содержащих логические операции, производится слева направо и прекращается (усекается), как только удается определить результат. Если выражение составлено яз логических утверждений (т.е.
52 Глава 2 выражений, вырабатывающих значение типа int), соединенных между собой операцией И (&&), то вычисление выражения прекращается, как только хотя ' бы в одном логическом утверждении вырабатываетси значение нуль. Если выражение составлено из логических утверждений, соединенных между собой операцией ИЛИ (!!), то вычисление выражения прекращается, как только хотя бы в одном логическом утверждении вырабатывается ненулевое значение. Вот несколько примеров типичных выражений, в которых используются логические операции if( i > 50 && J == 24 ) if( valuel < value2 && (value3 > 50 11 value4 < 20) ) Дальнейшее рассмотрение побитовых операций будет продолжено в гл. 4. 2.12.4. Операции присваивания К операцним присваивании относится =, +=, -=, *= н /=, а также префиксные и постфиксные операции ++ и —. Все операции присваивания присваивают переменной результат вычисления выражения. Если тнп левой части присваивании отличается от типа правой части, то тнп правой части приводится к типу левой. В одном операторе операция присваивания может встречаться несколько раз. Вычисления производятся справа налево. Например: а = ( b = с ) * d; Вначале переменной b присванваетси значение с, затем выполняется операция умножения на d, н результат присваивается переменной а. Типичный пример использования многократного присваивания: a = b = c = d = e = f = 0; Операции +=, -=, *= и /= являются укороченной формой записи операции присваивания. Их применение про ил-
Турбо Си: обзор возможностей 53 люстрируем при помощи следующего описания: а += b означает а = а + Ь. а -= b означает а = а - Ь. а *= b означает а = а * Ь. а /= b означает а = а / Ь. Префиксные н постфиксные операции ++ и — используют для увеличения (инкремент) и уменьшения (декремент) на единицу значения переменной. Семантика указанных операций следующая: ++а увеличивает значение переменной а на единицу до использования этой переменной в выражении. а++ увеличивает значение переменной а на единицу после использования этой переменной в выражении. —а уменьшает значение переменной а на единицу до использовании этой переменной в выражении, а— уменьшает значение переменной а на единицу после использования этой переменной в выражении. Что выведет в результате своей работы следующая короткая программа? «include <stdio.h> mainO { int a, b, с = 3, d = 4, f = 3, g. h = 5. z = 6, i; a = z + (b = c»d»f+(g = h+(i = 3) ) ); printf( "%d\n\ a ); } Ответ : 50. Читатель заметит, что при компиляции рассмотренной программы система выдаст три предупреждающих сообщения, говорящих о том, что переменным b, g и i присваиваются
54 Глам 2 значения, которые впоследствии не используются. Хотя для данного случая такие предупреждения безобидны, программисту на Турбо Сн всегда следует выявить причину, породившую каждое из предупреждающих сообщений, и выполнить соответствующее корректирующее действие. 2.12.5. Другие операции Операцию sizeof (размер) можно применить к константе, типу нлн переменной. В результате будет получено число байтов, занимаемых операндом. Если операндом является тип, то такой операнд следует заключить в круглые скобки. Если операнд - переменная, то скобки можно опустить. На листинге 2.7 приведены примеры использования операции sizeof. Листинг 2.7. Использование операции sizeof /* Программа, иллюстрирующая использование операции sizeof */ /* Файл sizeof.с */ «include <stdio.h> mainO { float a; int b; char c; float df 500 J; printf ( "ЧпРазмер памяти под целое V.d", sizeof( int) ); printf ( "ЧпРазмер памяти под символ %d", sizeof( char ) ); printf ( "ЧпРазмер памяти под вещественное %d", sizeof( float ) ); printf ( "ЧпРазмер памяти под двойную точность %d", sizeof( double ) ); printf ( "ЧпРазмер памяти под переменную a %d", sizeof a ); printf ( "ЧпРазмер памяти под переменную b %d", sizeoft b ) ); printf ( "ЧпРазмер паиятн под переменную с 7А",- sizeof с );
Турбо Си: обзор возможностей 55 printf ( "ЧпРаэмер памяти под переменную d 5Jd\n", sizeof d ); } Программа выведет: Размер памяти под целое 2 Размер памяти под символ 1 Размер памяти под вещественное 4 Размер памяти под двойную точность 8 Размер памяти под переменную а 4 Размер памяти под переменную b 2 Размер памяти под переменную с 1 Размер памяти под переменную d 2000 Операция «запятая» применяется для связывания между собой выражений. Список выражений, разделенный запятыми, трактуется как единое выражение и вычисляется слева направо. Если, например, надо ввести символ и сравнить введенное с 'а', то для этого можно использовать следующий оператор ветвления: if ( с = getcharO, с > 'а' ) Поскольку выражения вычисляются слева направо, самая правая проверка с > 'а' определяет целое значение вычисленного результата проверки. 2.13. Управляющие структуры Управляющие структуры нлн операторы управления служат для управления последовательностью вычислений в программе. Операторы ветвления- и цикла позволяют переходить к выполнению другой части программы нлн выполнять какую-то часть программы многократно, пока удовлетворяется одно или более условий. Группа операторов, заключенная в фигурные скобки, является составным оператором. Составные операторы можно использовать везде, где допускается единичный (отдельный) оператор. Внутри составного оператора каждая строка должна завершаться символом «точка с запятой». Как уже отмечалось выше, логическое выражение в качестве результата вырабатывает значение типа int.
56 Глава 2 2.13.1. Операторы ветвления К операторам ветвления (Турбо) Си относятся if, if else, ?, switch н goto. Общий вид операторов ветвления следующий: if ( логическое выражение ) оператор; if ( логическое выражение ) оператор_1; else оператор_2; Логическое выражение> ? <выражение_1> : <выражение_2>; Если значение логического выражения истинно, то вычисляется выражение_1, в противном случае вычисляется выражение.?. switch ( выражение целого типа ) { case значение_1 : последсвательность_операторов_1; break; case значение_2 : последовательность_операторов_2; break; case значекие_л : последователькость_операторов_Л; break; default: последовательность _операторов_Л+1; ) Ветку default можно не описывать. Ветка default выполняется, если ни одно нз вышестоящих условий ие удоволетворено. Используя вложенные операторы if else, можно построить логический эививалент конструкции if then elsif else. Например:
Турбо Си: обзор возможностей 57 if ( а > b ) printf( "\nA больше" ); else if ( а == b ) printft "\пА и В равны" ); else if ( а > с && d < е ) printИ "ЧпУсловие 3 удовлетворено" ); else printf( "ЧпДействне по умолчании" ); 2.13.2. Оператор цикла В Турбо Си имеются следующие конструкции, позволяющие программировать циклы: while, do while и for. Их структуру можно описать следующим образом: while ( логическое выражение ) оператор; Цикл с проверкой условия наверху do оператор; While ( логическое выражение ); Цикл с проверкой условия внизу for ( инициализация; проверка; новое_значение) оператор; Подробно циклы рассматриваются в гл. 5. 2.14. Массивы Возможно, наиважнейшим структурным типом данных является массив. Массив - это набор объектов одинакового типа, доступ к которым осуществляется прямо по индексу в массиве. Обращение к массивам в (Турбо) Сн осуществляется с помощью указателей (pointers). В настоящей главе мы сознательно опускаем некоторые важные сведения об указателях и сообщим лишь немногое нз того, что следует знать о массивах. Полную информацию можно получить из гл. 6.
58 Глава 2 Массивы можно описывать следующим образом: тип_данных имя„массива [ раэмер_массива 1; Используя имя массива и индекс, можно адресоваться к элементам массива: имя_массива[ эначение_индекса 1 Значения индекса должны лежать в диапазоне от нуля до величины, на единицу меньшей, чем размер массива, указанный прн его описании. Вот несколько примеров описания массивов: char name! 20 1; int grades! 125 1; float income! 30 1; double measurements! 1500 1; Первый из массивов (name) содержит 20 символов. Обращением к элементам массива может быть пате[ 0 ], пате[ 1 ], ..., пате[ 19 ]. Второй массив (grades) содержит 125 целых чисел. Обращением к элементам массива может быть grades[ 0 ], grades[ 1 ], .... gradesf 124 ]. Третий массив (income) содержит 30 вещественных чисел. Обращением к элементам массива может быть incomef 0 ], incomef I ], .... income[ 29 ]. Четвертый массив (measurements) содержит 1500 вещественных чисел с двойной точностью. Обращением к элементам массива может быть measurements^ 0 ], measurements! 1 ], .... measurements! 1499 ]. О массивах, естественно, можно сказать еще много. В гл. 6, в частности, будут рассмотрены проблемы захвата и освобождения памяти, строки, арифметические действия над указателями, многомерные массивы. В программе на листинге 2.8 заводится массив на 1000 целых чисел. Прн помощи функции average подсчнты- вается сумма элементов этого массива. Первым формальным параметром функции average является массив. Подробнее о передаче параметров будет сказано в главах 6 и 8, где собственно и рассматриваются функции. В качестве второго параметра функции передается число суммируемых значений в массиве а.
Турбо Си: обзор возможностей 59 Листинг 2.8. Программа, иллюстрирующая использование массивов /* Программа, иллюстрирующая использование массивов */ /* Файл array.с */ «include <stdio.h> «define size 1000 int data! size 1; mainO { extern float average( int all, int s ); Int i; for ( i = 0; i < size; i++ ) data! i 1 = i; printf ( "ЧпСреднее значение массива data = %f\n", average( data, size ) ); ) float average( int all, int s ) ( float sum = 0.0; int i; for ( i = 0; i < s; i++ ) sum += at i 1; return sum / s; ) 2.15. Несколько примеров программ на Турбо Си Ниже будет представлено несколько прикладных программ, использующих основные конструкции Турбо Си, рассматриваемые в настоящей главе. Вслед за каждой из программ дается описание ее действия. Начиная со следующей главы, каждая из тем, затронутых в настоящей главе, будет рассмотрена более подробно. Кроме того, в последующих главах будет раскрыто еще много тем, о которых в настоящей главе даже не упоминалось. Но прежде всего давайте что-нибудь запрограммируем на Турбо Си!
вО Глава 2 2.15.1. Определение максимального и минимального значений На листинге 2.9 приведена программа, решающая задачу вычисления максимального и минимального значений с использованием функций max и min. В качестве входных значений для указанных функций задаются массив чисел с плавающей точкой н размер такого массива; в результате получаются соответственно максимум и минимум. Листинг 2.9. Вычисление максимальных и минимальных значений элементов массива «include <stdio.h> «include <util.h> «define size 5000 float scores! size 1; main() < extern float max( float data!], int s ); extern float min( float datatl, int s ); int i; /* Индекс массива scores */ /* Заносим в массив scores 5000 случайных значений */ for ( i = 0; i < size; i++ ) scores! i 1 = 100.0 * rand_real(); printf ( "ЧпМакснмальное значение = %f", max( scores, size ) ); printf ( "ЧпМинимальное значение = %f\n", min( scores, size ) ); float max( float datatl, int s ); < float maximum = data! 0 1; int i; for ( i = 1; i < s; i++ ) if ( data! i 1 > maximum ) maximum = data! i 1;
Турбо Си: обзор возможностей в! return maximum; ) float mini float datatl, int в ) { float minimum = data! 0 1; int i; for ( i = 1; i < s; i++ ) if ( data! i ] < minimum ) minimum = data! i ]; return minimum; ) В теле функции main 5000 элементам массива scores присваиваются значения случайных чисел, получаемых при помощи функции rand_real из библиотеки util. В функциях шах н min локальные переменные maximum и minimum инициализируются значением data[ 0 ], т.е. первым элементом массива. В обеих функциях выполняется цикл, пробегающий по всем элементам со второго до последнего, в котором значения каждого элемента сравниваются с текущим- значением максимума или минимума. Текущие значения при необходимости обновляются. Обращение к функциям min и max в main выполняется внутри оператора вывода. Обратите внимание на использование константы size (размер). Если изменяется размерность массива, задаваемая этой константой, то это не приводит к необходимости менять что-лнбо в самом коде программы. 2.15.2. Утилита копировании файлов, использующая переназначение Рассмотрим задачу копирования входного текстового файла в новый выводимый текстовый файл. Задача может показаться на первый взгляд достаточно сложной, особенно потому, что функции чтения нз файла и записи в файл нами еще не рассматривались. Тем не менее задачу можно решить, напнсав всего несколько строк кода. В программе на листинге 2.10 используется функция поточного ввода getchar, а сам алгоритм основывается на переназначении ввода/вывода, поддерживаемого операционной системой MS-DOS.
62 Глам 2 Листинг 2.10. Утилита копирования файлов /* Утилита копирования файлов. Файл еру.с Использование: еру <вводной файл >выводной файл ♦/ «include <stdio.h> main!) < int ch; while ( ( ch = getcharO ) != EOF ) putcharl ch ); > Переменная ch описана как int, а ие как char потому, что константа EOF (End Of File - конец файла) из библиотеки stdio имеет значение -1. Тип int гарантирует поддержку как положительных, так и отрицательных значений, а тип char в некоторых версиях Си может быть усечен до 1 байта и, стало быть, поддерживает только неотрицательные целые. Желательно, чтобы наши программы на Турбо Си и впредь были как можно более мобильными. Функция putcharQ из библиотеки stdio выводит символьный аргумент в выводной поток. Используя переназначение вывода для выводного файла и переиазиачеиие ввода для вводного файла, программа успешно считывает символ за символом из входного потока и выводит их в выводной поток. 2.15.3. Нумерование строк программы на языке Си Пусть требуется напнеать на языке Си программу, которая добавляет номера строк к тексту произвольной программы иа Си. Мы вновь воспользуемся переназначением ввода/вывода, поскольку так можно осуществить чтение из входного потока и запись в выводной поток. Соответствующая программа приведена на листинге 2.11. Как и в программе на листинге 2.10, здесь для определения того, что символ ch является символом конца файла, используется цикл while. Как только введенный символ совпадет с символом перехода на новую строку (\п), перед строкой печатается ее номер и сам номер
Турбо Си: обзор ■озможиостай 63 увеличивается на едниицу. Здесь будет видно удобство использования постфиксной операции ++ для увеличения счетчика строк иа единицу. Листинг 2.11. Нумерование строк программы на Си #include <stdio.h> main!) { int line_number = 1, ch; printf ( "\n%5d ", line_number ); while ( ( ch = getcharO ) != EOF ) < if ( ch == '\n' ) printf ( "\n%5d ", ♦+line_number ); else putchart ch ); > ) Программа на листинге 2.11 напечатает: I «include <stdio.h> 2 3 main!) 4 < 5 int line_number = 1. 6 ch; 7 8 printf ( "\n7.5d ". line_number ); 9 while ( ( ch = getcharO ) != EOF ) 10 < II if ( ch == '\n' ) 12 printf ( "\n%5d ", t+linejiumber ); 13 else 14 putchart ch ); 15 ) 16 )
64 Глава 2 2.15.4. Сортировка методом пузырька Задача сортировки значений массива встречается часто и имеет важное значение. На листинге 2.12 представлен алгоритм сортировки значений в массиве вещественных чисел по возрастанию методом пузырька. Перес-4 тановка значений осуществляется в простом операторе присваивания, где используется операция «запятая:». В главной программе (функции main) вначале в массив data заносится size вещественных чисел, затем вызывается функция сортировки bubble-sort, после чего отсортированный массив распечатывается. На первом проходе алгоритма сортировки методом пузырька все элементы массива, начиная с последнего, сравниваются попарно, и если элемент а[ j ] оказывается меньше чем элемент а[ j-1 ] , то элементы меняются местами. После завершения первого прохода можно гарантировать, что наименьший элемент массива будет помещен в самый левый край массива - в элемент с индексом 0. На втором проходе выполняются те же действия, однако проход заканчивается перед элементом а[ 0 ]. После второго прохода второй по значению элемент будет гарантированно размещен в позиции а[ 1 ]. При каждом новом проходе самый маленький элемент из всех оставшихся попадает в самую левую позицию массива. Название «метод пузырька» дано алгоритму потому, что при каждом просмотре наибольшие элементы как бы медленно всплывают «наверх» к левому краю массива, что похоже на то, как к поверхности жидкости всплывают пузыри. Листинг 2.12. Сортировка методом пузырька /* Программа сортировки массива вещественных значений методом пузырька */ finclude <stdio.h> «include <util.h> «define size 100 float datat size 1; main () (
Турбо Си: обзор возможностей 65 extern void bubble_sort( float at I, int s ); int index; /* Заполнить массив data случайными вещественными значениями */ for ( index = 0; index < size; index++ ) data! index 1 = rand_real(); bubble_sort( data, size ); /• Вывод отсортированного массива */ for ( index = 0; index < size; index++ ) printft "\ndataf %d ] = %f", index, datat index П; printf( "\пя ); void bubble_sort( float ad, int s ) < int i, J; float temp; for ( i = 0, i < s - 1; i++ ) for ( j = s - 1; J > i; J—) if ( a[ J J < at J - 1 I > /* Поменять местами at J I и at j - 1 I */ temp = at J I. at J I = at j - 1 I, at J - 1 I = temp; 2.15.5. Определение фальшивой монеты Пусть дано 12 монет. Известно, что одна из монет фальшивая, причем она либо немного тяжелее, либо немного легче любой из оставшихся 11 монет. В известной старинной задаче вам предлагается разработать алгоритм, следуя которому н используя только рычажные весы, не более чем за три взвешивания можно было бы выявить фальшивую монету. Прежде чем читать дальше, попробуйте сами решить такую задачу. Решение ее нетривиально! Алгоритм решения заимствован нами с разрешения авторов из табл. 2.10 книги «A First Course in Computer Science with Modula-2» (Pinson, Sincovec, and Weiner, 1987). Эта таблица воспроизведена в виде табл. 2.2. 3 Зак. 795
66 Глава 2 Таблица 2.2. Алгоритм решения задачи поиска фальшивой монеты Взвесьте на рычажных весах две группы, по четыре монеты в каждой. Если весы уравновешены, то фальшивая монета среди четырех оставшихся монет. Сравните любые две из четырех оставшихся монет с двумя заведомо хорошими монетами из восьми проверенных. Если весы уравновешены, то Сравните одну нз оставшихся монет с хорошей монетой. Результат взвешивания определит фальшивую монету. Иначе, фальшивая монета - одна из двух потенциально фальшивых монет на весах. Сравните одну нз двух монет с хорошей монетой. Результат взвешивания определит фальшивую монету. Иначе, четыре оставшиеся монеты хорошие. Разделите восемь потенциально фальшивых монет на группу легких и группу тяжелых в зависимости от того, что показали весы при их взвешивании. Поместите две тякелые. и одну легкую монету на каждую чашку весов. При этом должны остаться две легкие монеты. Выполните взвешивание. Если весы уравновешены, фальшивая монета находится среди двух оставшихся легких монет. Сравните одну нз двух оставшихся монет с хорошей монетой. Результат взвешивания определит фальшивую монету. Иначе, выбор сужается до трех монет - одна из двух тяжелых монет на нижней чашке весов (на тялелой стороне) или легкая монета на верхней чашке весов (на легкой стороне). Сравните две тяжелые монета друг с другом. Если весы уравновешены, то фальшивая монета - это оставшаяся легкая монета. Иначе, монета на никней чашке весов (тяжелая сторона) - фальшивая, причем она тяжелая! —i
Турбо Си: обзор возможностей 67 Алгоритм, описанный в табл. 2.2, реализован в виде программы на Турбо Сн, приведенной на листинге 2.13. В функцнн main последовательно выполняется представленный алгоритм. В программе используются некоторые конструкции языка, которые мы еще не вводили, например инициализация локальных массивов left и right в функции main. Новым будет н использование командной строки в функции main. И, наконец, в присваивании значения weight элементу argv[ 2X0]= 'h' приходится использовать двумерный массив. В остальной части программы, также полезной для анализа, используются вложенные блоки if, позволяющие следовать логике алгоритма. Функция scale возвращает -1 (левая чашка весов внизу), если тяжелая фальшивая монета находится на левой чашке весов или легкая фальшивая монета находится на правой чашке весов. Функция возвращает 1 (правая чашка весов внизу), если тяжелая фальшивая монета находится на правой чашке весов или легкая фальшивая монета на левой чашке весов. Функция возвращает 0 (весы уравновешены), если на весах нет фальшивой монеты. Попробуйте проследить логику длинной, но линейной программы на Турбо Си. Листинг 2.13. Решение проблемы определении фальшивой монеты /* Компьютер'определяет фальшивую монету без жульничества •/ «include <stdio.h> •include <util.h> int weight; /* 1 если фальшивая монета тяжелая, иначе 0, если легкая */ int bad_coin; /* Номер фальшивой монеты */ main( int argc, char* argvM ) { extern int scale( int lefttl, int right!I, int c, int w ); extern void report_outcome( int result ); extern void delayO; char key;
68 Глам 2 int leftt ПН 1,2, 3,4), rightt 4 1 = <„5, 6, 7, 8 ); int outcome; clrscreenO; /* Ввод начальных данных */ if ( argc 1=3) < printf( "Вызов: (номер фальшивой монеты) (h или 1)" ); return; } bad_coin = atoi( argvt 1 ] ); if ( bad_coin < 1 11 bad_coin > 12 ) < printf( "ЧпЧпНомер монеты должен быть от 1 до 12" ); return; ) weight = ( argvt 2 If 0 1 == 'h' ); printf ( "ЧпВзвешивание 1" ); printf ( "Чп " ); printf("Чпмонеты 1..4 слева <-> Монеты 5..8 справа"); outcome = scalef left, right, bad.coin, weight ); report_outcome( outcome ); printf("ЧпДля продолжения нажмите любую клавишу.Чп" ); getjcey( &key ); if ( outcone == -1 ) < /* Две тяжелые и одна легкая монета на каждой чашке весов */ leftt 0 1=1, leftt 11=2, leftt 21=5, leftt 3 1=0; rightt 0 1=3, right! 11=4, rightt 21=6. right! 3 1=0; printf ( "ЧпВзвешивание 2" ); printf ( "4nr " ); printf("Монеты 1,2,5 сжева<->Монеты 3,4,6 справа"); outcome = scale( left, right, bad_coin, weight ); report_outcome( outcome ); printf("ЧпДля продолжения нажмите любую клавишу.Чп" ); getjkeyt &key ); if ( outcome == -1 ) < /* Сравните две тяжелые монеты друг с другом •/ left! 0 1=1, left' 11=0, leftt 21=0,
Турбо Си: обзор возможностей 69 leftt 3 1=0; right! 0 1=2, rightt 11=0, right! 2 ] = 0, right! 3 1=0; printf ( "ЧпВзвешиванне З" ); printf ( "Чп " ); printf ( "Монета 1 слева <-> нонета 2 справа* ); outcome = scale! left, right, bad_coin, weight ); report_outcome( outcome ); if ( outcome == -1 ) printf ( "\п\пМонета 1 фальшивая и тяжелая." ); else if ( outcome == 1 ) printf ( "ЧпЧпМонета 2 фальшивая и тяжелая." ); else printf ( "ЧпЧпМонета 6 фальшивая и легкая." ); > else if ( outcome == 1 ) < /* Сравните две тяжелые монеты друг с другом. */ left! 0 1=3. left! 11=0, left! 21=0, left! 3 1=0; right! 0 1=4, right! 11=0, right! 21=0, right! 3 1=0; printf ( "ЧпВзвешиванне 3* ); printf ( "\n " ); printf ( "Монета З слева <-> Монета 4 справа" ); outcome = scale! left, right, bad_coin, weight ); report_outcornel outcome ); if ( outcome == -1 ) printf ( "ЧпЧпМонета З фальшивая и тяжелая." ); else if ( outcome == 1 ) printf ( "ЧпЧпМонета 4 фальшивая и тяжелая." ); else printf ( "ЧпЧпМонета 5 фальшивая и легкая." ); > else { /* Фальшивая монета нажоднтся среди двух легкиж */ left! 0 1=7, left! 11=0, left! 21=0, left! 3 1=0; right! 0 1=9. right! 11=0, right! 21=0. right! 3 1=0; printf ( "ЧпВзвешиванне 3" ); printf ( "4n " ); printf ( "Монета 7 слева <-> Нонета 9 справа" );
70 Глава 2 outcome = scale( left, right, bad_coin, weight ); report_outcome( outcome ); if ( outcome != 0 ) printf ( "\п\пМонета 7 фальшивая н легкая." ); else printf ( "\п\пМонета 8 фальшивая и легкая." ); > else if ( outcome == 1 ) < /• Две тяжелые и одна легкая монеты на каждой чашке весов */ leftt 0 1=5, leftt 11=6, left! 21=1, leftt 3 1=0; rightf 0 1=7, rightt 11=8, rightt 21=2, right[ 3 1=0; printf ( "ХпВзвешивание 2" ); printf ( "\n " ); printf("Монеты 5,6,1 слева<->Монеты 7,8,2 справа"); outcome = scale( left, right, bad_coin, weight ); report_outcome( outcome ); printf("ЧпДля продолжения налмите любую клавишу. \n" ); getjcey( &key ); if ( outcome == -1 ) < /* Сравните две тяжелые монеты друг с другом */ leftt 0 1=5, leftt 11=0, leftt 21=0, leftt 3 1=0; rightt 0 1=6, rightt 11=0, rightt 21=0, rightt 3 1=0; printf ( "ХпВзвешивание 3" ); printf ( "\n " ); printf("Монета 5 слева<->Монета 6 справа"); outcome = scale( left, right, bad_coin, weight ); report_outcome( outcome ); if ( outcome == -1 ) printf ( "\п\пМонета 5 фальшивая и тяжелая." ); else if ( outcome == 1 ) printf ( "\п\пМонета 6 фальшивая и тяжелая." ); else printf ( "\п\пМонета 2 фальшивая и легкая." ); } else if ( outcome == 1 ) { /* Сравните две тяжелые нонеты друг с другом */
Турбо Си: обзор возможностей 71 leftt 0 1=7. leftt 11=0. left! 21=0. leftt 3 1=0; rightt 0 1=8, rightf 11=0, rightt 21=0, right[ 3 1=0; printf ( "ЧпВэвешивание 3" ); printf ( "\n " ); printf("Нонета 7 слева<->Монета 8 справа"); outcome = scale! left, right, bad_coin, weight ); report_outcoae( outcome. ); if ( outcome == -1 ) printf ( "\п\пМонета 7 фальшивая и тяжелая." ); else if ( outcome == 1 ) printf ( м\п\пМонета 8 фальшивая и тяжелая." ); else printf ( "ЧпЧпМонета 1 фальшивая и легкая." ); > else < /* Фальшивая монета находится среди двух легких монет */ leftt 0 1=3. leftt 1 J = 0, leftt 21=0. leftt 3-] =0; rightt 0 1=9, rightt 1 J = 0, rightt 21=0, rightt 3 1=0; printf ( "ЧпВэвешивание 3" ); printf ( "\n " ); printf("монета З сжева<->Монета 9 справа"); outcome = scale( left, right, bad_coin, weight ); report.outcome! outcome ); if ( outcome .'= 0 ) printf ( "ЧпЧпМонета 3 фальшивая и жегкая." ); else printf ( "ЧпЧпМонета 4 фажьпшвая и легкая." ); ) > else ( /* Фальшивая монета находится среди оставшихся четырех мбнет •/ leftt 0 1=9. leftt 1 I = 10, leftt 21=0. leftt 3 1=0; rightt 0 1=1. right! 11=2. right! 21=0, right! 3 J = 0; printf ( "ЧпВэвешивание 2" );
72 Глава 2 printf ( "\n " ); printf("Монеты 9,10 слева<->Монеты 1,2 справа"); outcome = scalef left, right, bad_coin, weight ); report_outcome( outcome ); printf С \п Для продолжения нажмите любую кжавишуАп" ); get_Jcey( 8*ey >; if ( outcome != О ) { /• Фальшивая монета нажодится слева •/ left! 0 1=9, left! 11=0, left! 21=0, left! 3 1=0; right! 0 1=1, right! 11=0, right! 21=0, right! 3 1=0; printf ( "ЧпВзвешивание 3" ); printf ( "\n " ); printf("Монета 9 слева<->Монета 1 справа"); outcome = scale( left, right, bad_coin, weight ); report_outcome( outcome ); if ( outcome != 0 ) printf ( "\п\пМонета 9 фалывитая." ); else printf ( "\п\пМонета 10 фальшивая." ); ) else < /• Фальшивая монета либо 11, лтбо 12 •/ left! О 1 = 11, left! 11=0, left! 21=0, left! 3 1=0; right! 0 1=1, right! 11=0, right! 21=0, right! 3 1=0; printf ( "ЧпВзвешивание 3" ); printf ( "\n " ); printf ("Монета 11 слева <-> Монета 1 справа" ); outcome = scale( left, right, bad_coin, weight ); report_outcome( outcome ); if ( outcome != 0 ) printf ( "ЧпЧпМонета 11 фальшивая." ); else printf ( "ЧпЧпМонета 12 фальшивая." ); ) ) printf ( "Чп" );
Турбо Си: обзор возможностей 73 int scale( int leftM, int right! 1, int c, int w ); { /• Возвращает О, если весы уравновешены, -1, если левая чаша тяжелее, и 1, если правая чашка легче •/ if ( lefU 0 ] == с II left! 1 1 == с 11 lefU 2 ] == с II left! 3 ] == с ) /• Фальшивая монета на левой чашке весов •/ return ( ( w ) ? ( -1 ) : ( 1 ) ); else if ( right! 0 1 == с II right! 1 1 == с 11 right! 2 1 == с II right! 3 ] == с ) /• Фальшивая монета на правой чашке весов •/ return ( ( w ) ? ( 1 ) : ( -1 ) ); else /• На весах нет фальшивой монеты •/ return 0; ) void report_outcome( int result ); i if ( result == -1 ) printf ( "\n\n Левая чашка весов опустилась." ); else if ( result == 1 ) printf ( "\n\n Правая чашка весов' опустилась." ); else printf ( "\n\n Весы уравновешены." ); printf ( " Ваш следующий ход ????" ); }
Глава 3 Лексические структуры языка В настоящей главе описываются лексические элементы языка (Турбо) Си. Из этих элементов строятся программы. 3.1. Элементы Программа на (Турбо) Сн представляет, собой строку символов, состоящую из лексических элементов пяти типов: зарезервированные (ключевые) слова, константы, операции, ограничители и идентификаторы. Смежные элементы отделяются друг от друга разделителями или комментариями. Разделители состоят из пробелов, символов табуляции, возврата каретки, перевода строки. 3.2. Комментарии Комментарии служат для документирования программы и ограничиваются символами /» и •/. Комментарии могут содержать любое количество символов или строк символов н трактуются компилятором как разделители. Если при вызове компилятора ' Турбо Cm указан ключ -С, то допускаются вложенные комментарии. Хотя вложенные комментарии весьма удобно использовать при отладке, пользоваться имн следует с осмотрительностью, поскольку стандарт Си не допускает вложения комментариев. Мобильность программы будет нарушена, если по завершении работы вложенные комментарии ие будут удалены из программы. Важно, чтобы, программируя на Турбо Сн, программист придерживался логичного стиля комментирования, обеспечивающего легкость восприятия программы. Не следует писать комментарии для пояснения вполне очевидного кода. Всегда лучше написать самодокументнрованную программу, содержащую наглядные имена (идентификаторы), чем посредством комментариев прояснять запутанный н зашифрованный код. При помощи комментариев можно прояснить назначение программы, подпрограммы (функции), фрагмента исходного текста, описания переменной или дать ссылку на используемый алгоритм или структуру данных. Комментарии могут быть
Лексические структуры языка 75 использованы для указания номера версии, даты и времени создания программы. Вообще комментарии должны прояснять процесс решения задачи в тех случаях, когда по самому тексту программы сделать это сложно. Не нужно надеяться на то, что другой программист или даже автор программы поймет назначение кода посредством «обратного конструирования» (т.е. определит смысл программы по ее коду). 3.3. Ограничители Символы-ограничители, используемые в (Турбо) Си, приведены в табл. 3.1. Их назначение объясняется в последующих разделах. Таблица 3.1. Снмволы-ограннчнтели ( ) [ ] { } . 3.4. Операции Перечень операций (Турбо) Си приведен в табл.3.2. Смысл и назначение операпий объясняется в последующих разделах. Таблица 3.2. Операции Односимвольные операции I & * - : / ? + % Двух- и трехе имвольиые операции != && :: -> ++ — << >> <= >= += -= *= /= %= < <= > >= &= ~= ! = Компилятор Турбо Сн не допускает яспользования разделителей внутри многосимвольных операций. Следующая программа не будет скомпилирована: mainl) { int a = 4; а + = 5; > Ошибка находится в тексте а + = 5. Текст следует
76 Глава 3 заменить на следующий: а+=5, который будет корректным. 3.5. Идентификаторы В (Турбо) Си идентификаторы служат для именования сущностей, таких, как типы, переменные, константы и функции. Идентификаторы состоят из букв и цифр и могут содержать символ «подчеркивание». В Турбо Си значащими являются первые 32 символа идентификатора, а остальные игнорируются. В других реализациях Си значащими могут считаться не 32 первых символа идентификатора, а меньше. Идентификатор не может начинаться с цифры и совпадать с зарезервированным словом. Формальное определение идентификатора в (Турбо) Си может быть записано следующим образом: <идентификатор> ::= I <буква> I <подчеркивание> ] { <буква> I <подчеркивание> I <цифра> У* Эта запись означает, что идентификатор начинается с буквы или подчеркивания, за которыми может стоять сколько угодно букв, подчеркиваний или цифр. В Турбо Си разрешено вслед за первым символом идентификатора ставить знак доллара $. В этом случае мобильность программы может быть нарушена. Символы подчеркивания в идентификаторах могут помочь сделать имена более наглядными. Например, по-видимому, легче понять смысл идентификатора vector_sum, чем идентификатора vectorsum. Тем более проще понять назначение переменной с именем right.jadjacentjiode_in_tree чем с именем rlghadjacentnodeintree. В идентификаторах (Турбо) Си прописные и строчные буквы различаются. Так, например, идентификатор Bread отличается от bread, который, в свою очередь, отличается от BrEaD. Многие программисты условились
Лексические структуры языка 77 использовать в идентификаторах по возможности только строчные буквы, за исключением случаев, когда идентификатор именует константу. 3.6. Зарезервированные слова В табл. 3.3 приведены все зарезервированные в Турбо Си слова. Их нельзя использовать в качестве идентификаторов. Поскольку компилятор (Турбо) Си различает строчные и прописные буквы, зарезервированные в языке слова в программе должны задаваться в точности так, как указано в табл. 3.3. Таблица 3.3. Слова, зарезервированные в Турбо Си asm* auto break case cdecl* char comit continue default do double else enum extern far* float for goto huge* if int interrupt* long near* pascal* regiHter return short signed sizeof static struct switch typedef union unsigned void volatile while _cs* _ds* _es* _SS* JM* AL* Jtf* _PH* _PL* _0X* _CH* _CL* Tcx* J)H* J)L* _px* JBP* JJI* J3I* _SP* *Звездочкой отмечены ключевые слова, включенные в Турбо Си, но отсутствующие в стандарте Си. 3.7. Констамты В языке (Турбо) Си имеются четыре типа констаит: целые, вещественные (с плавающей точкой), символьные и строковые. 3.7.1. Константы целого типа Константы целого типа могут задаваться в десятичной, восьмеричной или шестиадцатеричиой системах
78 Глава 3 счисления. Десятичные целые константы образуются из цифр. Первой цифрой не должен быть яуль. Восьмеричные константы всегда начинаются с цифры нуль, вслед за которой либо ие стоит ни одной цифры, либо стоят несколько пифр от нуля до семерки. Шестнадцатиричные константы всегда начинаются с цяфры нуль и символа х или X, вслед за которыми может стоять Одна или более шестнадцатеричных цифр. Шестнадцатеричиые цифры - это десятичные цифры от 0 до 9 н латинские буквы «a», cb», «о, «d», «e», <f», или «А», сВ», «С», cD», «E», cF». Программа, приведенная на лнстянге 3.1, выведет: а=3478 Ь=3478 с=3478 поскольку константы 3478, 06626 и 0xD96 задают одно и то же число. Листинг 3.1. Десятичные, восьмеричные к шестнадцатеричиые константы «include <stdio.h> main() < int a = 3478, b = 06626, с = 0xD96; printf( "a=%d b=Xd c=Xd\n". a, b, c); > К любой целой константе можно справа приписать символ 1 или L, и это будет означать, что константа - длинная целая (long integer). Символ и нлн U, приписанный к константе справа, указывает на то, что константа - целая без знака (unsigned integer). Если значение .константы превышает 65535, то она будет интерпретироваться как длинная целая без знака (unsigned long). Считается, что значение любой целой константы всегда неотрицательно. Если константе нредшествует знак минус, то он трактуется как унарная (одноместная) Операция смены знака, а не как часть константы.
Лексические структуры языка 79 3.7.2. Константы веществемиого типа Константы с плавающей точкой (называемые вещественными) состоят из цифр, деситичной точки и знаков десятичного порядка е или Е. Ниже приведены все возможные варианты записи коистаит вещественного типа: 1. 2е1 .1234 . 1еЗ .1 2Е1 1.234 0.0035е-6 1.0 2е-1 2. le-12 .234 Считается, что значение вещественной константы всегда неотрицательно. Если константе предшествует знак минус, то он трактуется как одноместная операция смены знака, а не как часть самой константы. 3.7.3. Символьные константы Символьные константы заключаются в апострофы (кавычки). Все символьные константы имеют а Турбо Си значение типа int (целое), совпадающее с кодом символа в кодировке ASCII. Нельзя рассчитывать иа то, что другие компиляторы с Си обязательно используют кодировку ASCII для задания соответствия между символом и целым числом. Для повышения читабельности и мобильности программы следует, где это только возможно, пользоваться для задания символьных констант символьными литералами. Так, например, фрагмент кода if (ch >= 'а' && ch <= 'z* ) будет более наглядным и мобильным, чем эквивалентный ему фрагмент if (ch >= 97 && ch <= 122 ) Один символьные константы соответствуют символам, которые можно вывести на печать, другие - управляющим символам, задаваемым с помощью esc-последовательности, третьи - форматирующим символам, также задаваемым с помощью esc-последовательностн.
80 Глава J Например, символ «апостроф» задается как 'V, переход на новую строку - как 'V, а обратный слэш - как *\\\ Управляющие коды В табл. 3.4 приведены управляющие коды (esc-последовательности), используемые в (Турбо) Си. Каждая esc-последовательность должна быть заключена в кавычки. Таблица 3.4. Управляющие коды \п \t W \b \r \f \\ V V \a \? \ddd \xhhh Новая строка Горизонтальная табуляция Вертикальная табуляция Возврат на символ Возврат в начало строки (возврат каретки) Прогон бумаги до конца страницы Обратный слэш Одинарная кавычка Двойная кавычка Звуковой сигнал Знак вопроса Код символа в ASCII - от одной до трех восьмеричных цифр Код символа в ASCII - от одной до трех шестиадцатеричных цифр На листинге 3.2 приведены примеры использования символьных констант. Листинг 3.2. Использование символьных констант «include <stdio.h> oalnO { char a b с d e f К = = = = = = = 'A' ••' Mt •v •\\ •\n •\a
Лексические структуры языка 81 h = '\?\ i = '\07\ j = '\х07\ printf( "a = Y.c Ь = %с с = '/.с d = Кс", а, Ь, с, d ); printfl "e = У.с f = Хс\ е, f ); printf( "g = Хс h = %с i = %с j = Xc\n", g, h, i, j ); } В качестве примера работы с символьными константами рассмотрим программу, подсчитывающую количество строк в текстовом файле и определяющую количество гласных букв (а, е, i, о, и и А, Е, (, О, U) в нем же. Текст программы приведен на листинге 3.3. Было бы правильнее описать переменную ch не как символьную (char), а как целую (int), поскольку значение константы EOF (End Of File) равно -1. Хотя в Турбо Си программа, в которой переменная ch описана как char (точнее, как символьная со знаком, т.е. signed char), будет работать, этого нельзя утверждать при использовании других компиляторов с Си. Мобильность может быть обеспечена, если используется Описание int. Функция getchar() выдает следующий символ из входного файла. Используя переопределение источника ввода, реализованное в DOS, можно подсчитать число строк в файле inputfile, вызвав программу следующим образом: count < inputfile Листинг 3.3. Подсчет числа строк и числа гласных букв в текстовом файле /• Файл count.с •/ «include <stdio.h> «include <ctype.h> ■ain() < char ch = getcharf); int number_lines = 1; int number_vowels = 0; ch = tolowert ch ); if ( ch == *a' II ch == 'e' II ch == 'i' II
82 Глам 3 ch == 'о' II ch == 'u' ) number_vowels++; while ( ch != EOF ) < ch = getcharO; if ( ch == '\n' ) number_lines++; ch = tolower( ch ); if ( ch == 'a' II ch == 'e' II ch == 'i' II ch == V :: ch == V ) number_vowels++; ) printf( "ЧпКоличество строк в тексте = %d", number_lines ); printfl "ЧпКоличество гласных = 4d\n°, nuiber_vowels ); ) 3.7.4. Строковые константы Строковые константы состоят из нуля нлн более символов, заключенных в двойные кавычки. В строковых константах управляющие коды задаются с помощью esc-последовательиости. Обратный слэш используется как символ переноса текста на новую строку. В Турбо Си в соответствии с проектом стандарта ANSI разрешается применять строковые константы, располагаемые в нескольких строках. Указанную новую возможность следует использовать с осторожностью, поскольку это затрудняет перенос программ в среду старых версий компиляторов с С». Примеры строковых констант приведены на листинге 3.4. Правила описания строковых констант рассматриваются в гл. 6 в разделе о массивах. Листинг 3.4. Использование строковых констант «include <stdio.h> maint) i char «strl, »str2, «str3; strl = "Данный текст не будет перенесен на\ следующую строку. ЧпЧп";
Лексические структуры языка 83 str2 = "Вот пример длинной строки, разбитой яа \п" "более короткие фрагменты. Следует помнить, \п" "что возможность разбивать строки на\п" "фрагменты является нестандартной и\п" "включена только в Турбо Си. Для\п" "обеспечения мобильности программыЧп" "указанной возможностью следует \п" "пользоваться с осторожностью. \п\п"; str3 = "\073вонок!"; printf( strl ); printf( str2 ); printf( str3 ); printf( "\n" ); } Программа выведет следующий текст: Данный текст не будет перенесен на следующую строку. Вот пример длинной строки, разбитой на более короткие фрагменты. Сжедует помнить, что возможность разбивать строки на фрагменты является нестандартной и включена только в Турбо Си. Для обеспечения мобильности программы указанной•возможностью следует пользоваться с осторожностью. Звонок! I
Глава 4 Скалярные типы данных, операции, преобразования типов В главе рассматриваются скалярные типы данных (Турбо) Сн (int, char, float) и нх вариации. Описываются н операции (Турбо) Сн. Вводится иовейшнй тнп данных Сн - enumeration (перечнслнмый). Обсуждаются вопросы приведения н преобразования типов. 4.1. Типы данных и элементы памяти Тнп задается набором допустимых значений и набором действий, которые можно совершать над каждой переменной рассматриваемого типа. Считается, что переменная нлн выражение принадлежит к данному типу, если его значение принадлежит области допустимых значений этого типа. Переменные типизируются посредством нх описаний. Выражения типизируются посредством содержащихся в них операций. В (Турбо) Сн имеется множество предопределенных типов данных, включая несколько видов целых, вещественных, указателей, переменных, массивов, функций, объединений, структур и тнп void (отсутствие типа). Тнп void не имеет ни значений, ни действий. Скалярные типы Тип void Функции Агрегатые типы Указатель Арифметический Перечисдииый Массив Структура Объединение Вещественный Целый Рис. 4.1. Типы в (Турбо) Сн. Целые типы включают несколько разновидностей целых и символьных данных. Арифметические типы объединяют це-
Скалярные типы денных, операции, преобразования 85 лые н вещественные. Скалярные типы включают арифметические типы, указатели н перечнслнмые типы. Агрегаты, нлн структурные типы, включают массивы, структуры и объединения. Функции представляют особый класс, рассматриваемый в гл. 8. Типы (Турбо) Сн представлены на рис. 4.1. В каждой реализации Си всякий тип занимает определенное число единиц памяти. За такую единицу принимается память, требуемая для хранения одного символа; и обычно она составляет один байт. Сказанное верно и для Турбо Сн. Все объекты одного типа занимают одно и то же количество единиц памяти (байтов). Число еднннц памяти, требуемое для размещения элемента данного типа, может быть вычислено по операции sizeof (размер). В табл. 4.1 представлены типы данных (Турбо) Си, указаны их размеры в байтах и заданы диапазоны значений (наборы значений). Таблица 4.1. Типы данных в (Турбо) Си, нх размеры и диапазоны значений Тип char unsigned char int unsigned short unsigned short long unsigned long float double long double pointer pointer Размер в байтах 1 1 2 2 Аналогично int Аналогично unsigned 4 4 4 8 Аналогично double В перспективе будет занимать 10 байтов 2 4 Диапазон значений -128...127 0...255 -32768...32767 0...65535 -2147483648... ...2147483647 0...4294967295 3.4Е-38...3.4Е38 1.7Е-308... ...1.7Е308 (near, _cs, _ds, _es, _5s) (far, huge) Пользователи могут определять новые типы, используя спецификатор typedef. Например,
86 Глава 4 /* Определяем новый тип - натуральные числа */ typedef unsigned int CARDINAL; CARDINAL ту_лш»Ьег; Новый тип CARDINAL определяется как целый беззнаковый. 4.2. Использование объектов различных типов Всем объектам соответствуют области памяти, нз которых можно прочитать и в которые можно записать значения. Будем называть леводопустимым такое выражение, которое позволяет как считывать определяемое нм значение объекта, так н изменять его. Название отражает тот факт, что леводопустимое выражение может использоваться в левой части оператора присваивания. Соответственно, праводопустимое выражение может использоваться только в правой части оператора присваивания. Ниже приведены возможные леводопустнмые выражения • Имена переменных, описанных как арифметические, указатели, перечислнмые или объединения. • Выражение в круглых скобках, причем тогда н только тогда, когда само выражение является леводопустимым. • Выражение, определяющее поле (ехрг. field), причем тогда н только тогда, когда само выражение (ехрг) является леводопустимым. • Выражение, задающее косвенный выбор поля (ехрг -> field). • Косвенное выражение *ехрг. Следующие объекты никогда не могут встречаться в левой части оператора присваивания, т.е. не являются леводопустнмыми • Имя функции. • Имя массива (ссылка на элемент массива является леводопустимым выражением).
Скалярные типы денных, операции, преобразования 87 • Константа перечнслнмого типа. • Оператор присваивания. • Вызов функции. Многие нз перечисленных конструкций будут расмат- рнваться в последующих разделах этой книги. 4.3. Переменные целого типа Как уже указывалось в гл. 2, переменные, прежде чем онн будут использованы, должны быть описаны при помощи спецификатора типа. Прн описании переменной ей может быть присвоено начальное значение. Ниже приведен типичный пример использования переменных целого типа. Начальное значение переменной может задаваться, например, выражением int age =15, /• возраст •/ height =70; /• рост •/ unsigned weight = 2 • height; /• вес •/ long index; /• индекс •/ Если используются спецификаторы unsigned, long и short, то спецификатор int можно опускать. Способы записи целых констант рассматривались в разд. 3.7.1. Допустимые операции над целыми операндами указаны в табл. 4.2. Таблица 4.2. Операции для типа int Арифметические операции + Унарный плюс, сложение Унарный минус, вычитание • Умножение / Деление X Остаток от деления х- Изменить и заменить, где х может быть +,-,*,/ или % ++ Инкремент (увеличение на 1) — Декремент (уменьшение на 1) Логические операции && И II ИЛИ
88 Глава 4 ! НЕ == Равно != Не равно > Больше >= Больше или равно < Меньше <= Меньше или равно Битовые операций & И (and) I ИЛИ (or) " ИСКЛЮЧАЮЩЕЕ ИЛИ Отрицание >> Сдвиг вправо << Сдвиг влево х= Изменить и заменить, где х иожет быть &, I ,/ч.» или « 4.4. Символьные переменные Ниже приведен типичный пример использования символьных переменных: char ch = 'f', ans = 'n', response = '2', ascii_value = 65, /• Символ 'А' в кодировке ASCII - фрагмент программы может оказаться немебквышм •/ answer; Переменные типа char имеют размер 1 байт н являются самыми маленькими по размеру объектами, которые можно описать. По умолчанию в Турбо Си (если прн вызове системы ие задан ключ -К) предполагается, что символьные переменные являются знаковыми-. Беззнаковые символьные переменные либо описьжметежчжво при помощи -- лнецифккатора unsigned, либо определяются по умолчанию прп установке ключа системы -К.. Диапазон значеинй типа signed char от -128 до 127. Беззнаковые символьные переменные имеют значении От 0 до 255.
Скалярные типы данных, операции, преобразования 89 Способы записи символьных коистаит рассматривались в разд. 3.7.3. В выражениях переменные типа char могут смешиваться с переменными типа int, поскольку н те и другие принадлежат к целому типу. Вполне корректно сложить 3 с символьным значением 'а'. В программе на листинге 4.1 показано смешение целых н символьных значений. Программа выводит следующие строки: Значение ch + 3 = d Значение ans = 1 Листинг 4.1. Смешение целых и символьных переменных «include <stdio.h> ша1п() < char ch = 'а'; char ans; printf("Значение ch + 3 = %c", ch + 3 ); ans = ch % 3; printf ("\п\пЗначение ans = Xd\n'\ ans ); } Поскольку char - это целый тип, для него применимы все операции, операнды которых могут иметь тнп int. 4.5. Переменные вещественного типа Ниже приведен типичный пример описания переменных вещественного типа: float force = 12.78, /• сила •/ acceleration = 1.234; /* ускорение •/ double height; /• высота •/ Так как для размещения чисел типа float требуется иамятн в два или четыре разе (для двойной точности double) больше, чем для целых, и поскольку арифметические операция с плавающей точкой в несколько раз медленнее, чет операции целочисленной арифметяки, то вещественные переменные должны применяться только там, где требуются дробные величины. Из-за того что для
90 Глава 4 представления всех чисел с плавающей точкой используется конечное число цифр, при выполнении арифметических действий появляются ошибки вычислений. Способы записи вещественных констант рассматривались в разд. 3.7.2. Операции над вещественными операндами указаны в табл. 4.3. Таблица 4.3. Операции для типа float Арифметические операции + Унарный плюс, сложение Унарный минус, вычитание * Умножение / Деление х= Изменить и заменить, где х может быть +,-,* или % ++ Инкремент (увеличение на 1) — Декремент (уменьшение на 1) Логические операции && И II ИЛИ ! НЕ == Равно != Не равно > Больше >= Больше или равно < Меньше <= Меньше или равно 4.6. Приоритет и порядок выполнения операций Если в выражении не используются круглые скобки, задающие порядок выполнения операций, то -группировка операндов для операций производится с учетом приоритета операций. Например, в выражении first •= second <= third операнд second группируется с операцией <=, поскольку операция <= имеет более высокий приоритет, чем операция *= Приведенное выражение эквивалентно следующему: first *= (second <= third).
Скалярные типы данных, операции, преобразования 91 Заметьте, что выражение (second <= third) может принимать значение нуль илн единица. В следующем примере first = second -= third операции = и -= имеют одинаковый приоритет, однако операнд second группируется с операцией справа от него, поскольку операции = н -= являются правоассошшруемымн (выполняются справа налево). Все двухместяые и трехместные операции являются левоассоцннруемымн (выполняются слева направо), за исключением условных операций и операций присваивания, являющихся правоассоцннруемымн. В табл. 4.4 приведены операции (Турбо) Си в порядке убывания приоритета. Эту таблицу стоит внимательно изучить. Многие операции из табл. 4.4 описываются в последующих главах. По мере изучения новых операций рекомендуется вновь н вновь обращаться к табл. 4.4. Таблица 4.4. Операции (Турбо) Си в порядке убывания приоритета Операция Назначение [) Задание элемента массива () Вызов функции Выбор поля структуры -> Выделение поля структуры с помощью указателя ++,— Постфиксное/префиксное увеличение и уменьшение на 1 - если и то и другое встречается в одном выражении, то постфиксное имеет более высокий приоритет sizeof Определение размера переменной в байтах (тип) Приведение к типу Побитовое отрицание ! Логическое НЕ Унарный минус & Определение адреса • Обращение по адресу *,/,% Умножение, деление и остаток - одинаковый приоритет +,- Сложение, вычитание - одинаковый приоритет <<,>> Сдвиг влево, сдвиг вправо - одинаковый приоритет
92 Глам 4 <S1 > = N —, •■ — & ,Л 1 && It ?: = ,+=,-=, •=./=. «=,»=, Сравнение - одинаковый приоритет Равенство, неравенство - одинаковый приоритет Побитовое И Побитовое исключающее ИЛИ Побитовое ИЛИ Логическое И Логическое ИЛИ Условный оператор Присваивание и замещение - одинаковый приоритет &=.л=.|= Операция запятая, которая предписывает последовательное вычисление выражений 4.7. Арифметические операции Операции целочисленной арифметики выполняются без учета переполнения. Так, для целых со знаком 32767 + 1 будем иметь -32768, а для беззнаковых целых 65535 + 1 будет 0. Результатом целочисленного деления будет целая часть частного. Так, 9/2 = 4 и -9/2 = -4. В турбо Си при целочисленном делении отбрасываются левые незнача- щиенули. Для других реализаций Си это, вообще говоря, может быть ие так. По стандарту Си операции с плавающей точкой вы- полннютсн над операндами двойной точности типа long. Результат вычисления приводится к типу левой части выражения. Операция выделения остатка от делении (%) выдает в качестве результата остаток от деления первого целого числа на второе. В Турбо Си, если один из операндов отрицателен, то и результат отрицателен (имеет знак минус), а если оба операнда отрицательны, то результат положителен. Если хотя бы один из операндов отрицателен, то результаты выполнения операции выделении остатка от деления могут быть разными в различных реализациях Си. Для обеспечения мобильности кода не следует использовать в операции % отрицательные операнды.
Скалярные типы данных, операции, преобразования 93 Результатом работы программы с листинга 4.2 будет: Остаток от деления 13 на 3 равен 1 Остаток от деления -13 на 3 равен -1 Остаток от деления 13 на -3 равен -1 Остаток от деления -13 на -3 равен 1 Листинг 4.2. Пример выполнения операции выделения остатка ttlnclude <stdio.h> ша1п() { printf( "ЧпОетаток от деления 13 на 3 равен %d'\ 13 % 3 ); printf( "ЧпОстаток от деления -13 на 3 равен %d", -13 % 3 ); printf( "ЧпОстаток от деления 13 иа -3 равен %d", 13 % -3 ); printf( "ЧпОстаток от деления -13 на -3 равен %d4n", -13 % -3 ); > Знак минус, предшествующий выражению, обозначает операцию. Поскольку все коистаиты в Турбо Си являются беззнаковыми, то уиариый минус фактически означает умножение константы на -1. Предостережение Не следует присваивать отрицательные значения целым беззнаковым переменным. Что, по вашему мнению, должна вывести программа, представленная на листинге 4.3? Листинг 4.3. Присваивание отрицательного значения целой беззнаковой переменной ttlnclude <stdio.h> ш1п() < unsigned i = -1; printf( "Беззнаковое i = %u", 1 ); printf( "ЧпЗнаковое i = %d4n", i ); >
94 Глав* 4 4.8. Операции отношения Логические выражения строятся из операций отношения и вырабатывают в качестве результата значение типа int. Если результат равен нулю, то. считается, что логическое выражение ложно, в противном случае - истинно. Если в логическом выражении не используются скобки, то оно вычисляется слева направо. Вычисление прекращается, как только результат становится определен- нным. Такой способ вычисления логических выражений называется усечением. Вычисление выражения if(a>bllc>dlle>f) прерывается, как только выясняется, что либо а > Ь, либо с > d , либо е > f. Допускается, чтобы вычисление с > d или е > f не производилось, если уже ясно, что а > Ь. Усечение с успехом может быть использовано для задания корректного порядка вычислений в логических выражениях. Например, логическое выражение if ( b != 0.0 && а / b > 12.4 ) имеет больший смысл, чем логическое выражение: if ( а / b > 12.4 && b != 0.0 ) Для первого выражения если значение переменной b равно нулю, то вторая часть выражения (а/Ь) вычисляться не будет, и тем самым предотвратится возможная ошибка при делении на нуль. Для второго выражения это не так. На листинге 4.4 приведены примеры использования операций отношения при вычислении всех простых чисел, не превышающих заданного целого числа, которое, в свою очередь, должно быть больше либо равно семи. Определяется функции generatejrimes (т.е. генера- цня_простых_чисел). Она не возвращает результат (void) и использует один параметр типа int - upper_bound (т.е. верхний_предел).
Скалярные типы данных, операции, преобразования 95 Первая логическая проверка онределяет: значение upper_bound больше или равно семи? Только в этом случае функция будет печатать простые числа. В противном случае функция не выполнит никаких действий. Если первая проверка оказывается успешной (upper_bound>=7), то проверяется, является ли зиаченне upper_bound четным. Если оно четное, то из значения вычитается единица, в противном случае значение не изменяется. Далее следует цикл for. В цикле значением три инициализируется целая переменная candidate, используемая в качестве счетчика цикла. Дальнейшее выполнение тела цикла будет происходить, если истинным будет выражение candidate <= upper_bound При каждом выполнении цикла значение переменной candidate будет увеличиваться иа два. Внутри цикла for значением три инициализируется переменная trialdi visor (т.е. пробиый_делитель). В цикле while вычисляется другое логическое выражение while ( trial_divisor <= sqrt( candidate ) && ( г = candidate % trial_divisor) != 0 ) Если это логическое выражение истииио, то значение trialdivisor увеличивается на два. Отношение candidate % trialdivisor запоминается в целой переменной г с тем, чтобы сэкономить одну операцию- деления после цикла. После выхода из цикла while с помощью логического выражения г 1=0 выполняется еще одна проверка. При этом выясняется причина окончания цикла - либо значение trialdivisor слишком велико, либо переменная candidate действительно содержит простое число. Алгоритм, используемый для проверки каждого числа в переменной candidate на то, является ли оно простым числом, может быть назван алгоритмом «грубой силы». Этот эпитет особеиио уместен, если выбрать значение upperbound близким к максимально допустимому целому числу. Вычисления будут очень долгими, поскольку значение candidate будет много раз делиться на значения пробных делителей.
96 Глава 4 Для систем с дисплеем на 80 символов в ширину оператор printf("%8d",candidate) позволит вывести простые числа в виде таблицы по десять чисел в строке, выровненные по правому краю столбца. Листинг 4.4. Вычисление простых чисел и примеры использования операций отношения /• Программа вывода простых чисел */ «include <stdio.h> ^include <match.h> main О { extern void generate_primes( int upperJbound ); int maximum; printft "ЧпГенеряровать простые числа до ? " ); scanf( "'/A', &maxlmum ); generatejprimes( maximum ); printfl "\n" ); > void generate_primes( int upper_bound ) i int trialjdivisor; int candidate; int r = 1; if ( upper_bound >= 7 ) { if ( upperjbound / 2 == 0 ) /• Верхняя граница. - четное число •/ upperJbound —; for ( candidate = 3; candidate <* upper_bound; candidate += 2 ) { trial_divisor = 3; while ( trial_divisor <= sqrt( candidate ) && ( r = candidate % trial_divisor) != 0 ) trial_divisor += 2; if ( r != 0 > /* Простое число получено •/
Скалярные типы данных, операции, преобразования 97 printf( "%8d", candidate ); } } } 4.9. Побитовые операции Операнд или операнды побитовых операций должны быть целого тяпа. Побитовые операции обычно используются в приложениях, требующих для доступа к аппаратуре на нижнем уровне манипуляций с битами. Получаемый таким образом код сильно зависит от машины и используемой операционной системы и поэтому должен создаваться с осторожностью. Операции &, ! и Л обозначают соответственно побитовые операции И, ИЛИ и ИСКЛЮЧАЮЩЕЕ ИЛИ. Операции определяются следующим образом: ИЛИ 0 1 0 0 1 1 1 1 искл. или 0 1 0 0 1 1 1 0 и 0 1 0 0 0 1 0 1 ! : Если хотя бы один из битов равен единице или оба равны единице, то результат - единица; в противном случае результат - нуль. & : Если оба бита равны единице, то результат - единица; в противном случае результат - нуль. " : Если оба бита одновременно либо нуль, либо единица, то результат - нуль; в противном случае результат - единица. Рассмотрим следующий пример. Битовое представление целых чисел 123 и 321 выглядит следующим образом: 123 = 0000000001111011 321 = 0000000101000001 Если используется побитоваи операция ИЛИ (!), то получится следующий результат: 123 I 321 = 0000000101111011 = 379 4 Зак 795
9в Глам 4 Если используется побитовая операция И (&), то получится следующий результат: 123 & 321 = 0000000001000001 = 65 Если используется побитовая операция ИСКЛЮЧАЮЩЕЕ ИЛИ С), то получится следующий результат: 123 л 321 = 0000000100111010 = 314 Операции << и >> служат для сдвига последовательности битов, соответственно, влево и вправо. Например, 123 « 5 = 0000111101100000 = 3936 123 » 1 = 0000000000111101 = 61 Приведенные операции могут применяться для деления или умножения на число, равное степени числа два, в соответствии со следующими правилами: х >> п эквивалентно делению х на 2, в степени п. х « п эквивалентно умножению х на 2 в степени п. Предположим, что нам нужно вывести значения всех битов беззнаковой целой переменной. Функция display_bits (вывести_бнты) на листинге 4.5 делает именно это. Здесь для каждой битовой позиции выполняется операция specimen » bit„position & 1 и печатается ее результат. Поскольку операция сдвига вправо имеет более высокий приоритет, чем побитовая операция И, в выражении ие требуется ставить скобки. Листинг 4.5. Вывод битового представления беззнакового целого числа /* Программа вывода битового представления беззнакового целого числа •/ «include <stdio.h> nainO
Скалярные типы данных, операции, преобразования 99 extern void display_bits( unsigned jpecinen ); unsigned value; displayJbits ( 0 ); display_bits ( 5 ); display_bits ( 13 ); display_bits ( 123 ); display_bits ( 32 ); display_bits ( 117 ); printf( "\п\пВведите число: " ); scanf( "Xu", Rvalue ); if ( value >= 0 ) printf( "\nn ); display Jbits( value ); > void display_bits( unsigned specimen ) { const int max_position = 15; int bit_position; printff "\п\пБитовое представление для %d\n", specimen ); for ( bit_position = max_position; bit_position >= 0; bit_position — ) printf( " %d ", specimen » bit_position & 1 ); } Предостережение: знаковые целые числа Хотим предупредить читателя, что при использовании в операции сдвига вправо знакового целого операнда некоторые компиляторы с Си ие «вдвинут» нули слева, если самый левый бит операнда ие был равен нулю. Поэтому соглашении о мобильности предписывают преобразовывать операнд операции сдвига вправо в беззнаковый тип до выполнения самого сдвига. Аналогичные проблемы совместимости возникают и при применении побитовых операций &, Л ж ! к знаковый целым значениям. Выход есть - преобразовывать перед выполнением побитовых операций знаковые целые в беззнаковые. В качестве еще одного примера использования побитовых операций рассмотрим задачу построения абстрактного множества. Пусть мы хотим разместить множество из 4*
100 Глава 4 1600 беззнаковых целых чисел в 200 байтах. Это означает, что под каждое целое беззнаковое число может быть отведен только одни бит в множестве. На листинге 4.6 приведен текст программы на Турбо Си, включающей две основные операции - insert (включение) и in (принадлежность к множеству) - для множества, которое может содержать до 1600 беззнаковых целых чисел. В функции insert сначала вычисляется индекс index: element / 16. Далее используются побитовая операция ИЛИ и присваивание (!=), добавляющие бит 1 в- позицию, определяемую по формуле (element % 16). В соответствии с приоритетом операций (см. табл. 4.4) выражение element % 16 вычисляется до выполнения сдвяга влево. Операция ИЛИ и присваивание имеют самый низкий приоритет и поэтому выполняются в последнюю очередь. Рассмотрим пример. Представьте, что мы желаем добавить к множеству число 16. Позиция, определяемая значением нуль переменной index, содержит биты (числа) от 0 до 15. Для значения 16 переменной element вычисляется индекс index = element/16. Переменная index получит правильное значение 1. Номер бита в соответствующем элементе массива будет вычислен как element%16 и станет равен 0. В гл. в мы расскажем о том, почему всем элементам глобального массива set перед началом выполнения программы будет присвоено значение нуль. Процесс, обратный рассмотренному выше, происходит при вызове функции in, определяющей принадлежность элемента множеству. После вычисления соответствующей позиции в массиве проверяется бит, положение которого определяется по формуле element%16. Если этот бит - единица, то элемент принадлежит множеству, в противном случае не принадлежит. Листинг 4.6. Реализация абстрактного множества /* Работа со множествами в Турбо Си */ «include <stdio.h> unsigned set! 100 1;
Скалярные типы данных, операции, преобразования 101 maln() { extern void Insert( unsigned element ); extern int in( unsigned element ); prlntf( "ЧпРаэмер множества = %d", sizeof( set > >; insert( 23 ); insert( 27 ); insert( 1245 ); insert( 1111 ); if ( in( 28 ) ) prlntf( "\n28 содержится в мноиестве" ); else printf( "\n28 не содержится в множестве" ); if ( in( 27 ) ) printf("\n27 содержится в мнокестве" ); else printfC "\п27 не содержится в мнонестве" ); if ( in( 1112 ) ) printf( "\nlll2 содержится в множестве" ); else printf( "\nlll2 не содержится в мноиестве" ); if ( in( 1111 ) ) printf( "\nllll содержится в множестве" ); else printf( "\nl111 не содержится в множестве" ); if ( in( 1245 ) > printf( "\nl245 содеркится в мнокестве" ); else printf( "\nl245 не содеркится в множестве" ); void insert( unsigned element ) { /* Положение элемента определяется по формуне element/16, а положение бита - по формуле element % 16 */ int index = element / 16; set! index 1 1= 1 << element 'A 16; int in( unsigned element )
102 Глава 4 < int index = element / 16; return ( set! index 1 >> element % 16 ) & 1; } 4.10. Прочие операции Читателю, интересующемуся правилами исиользоваиия операций присваивания, sizeof, следования (запятая), постфиксных и префиксных операций, рекомендуем обратиться к материалу разд. 2.12.4 и 2.12.5. 4.11. Перечислимые типы Перечислимые типы включены в язык Си сравнительно недавно и в Турбо Си реализованы. Они служат для улучшения читабельности программы и позволяют естественным образом отображать понятие пространства задачи на понятие пространства в программной реализации задачи. Например, представьте переменную, которая должна описывать светофор. Многие программисты на Си опишут переменную trafficlight (светофор) как int. Значение нуль будет обозначать красный цвет, единица - желтый, а двойка - зеленый. Если же воспользоваться перечислимыми типами, то можно дать следующее описание светофора: enum {red, amber, green) traffic_Jight; /* {красный, желтый, зеленый) светофор »/ или enum light_type { red, amber, green ); /* тип_свет {красный, желтый, зеленый) */ enum light_type traffic_light; После того как определен тег (признак) перечислимого типа light_type, можно ссылаться на этот тип, что весьма удобно. Рекомендуется применять теги перечислимых типов везде, где используются перечислимые типы. Рассмотрим программу на листинге 4.7. Она напечатает: Зеленый свет
Скалярные типы данных, операции, преобразования 103 Листинг 4.7. Пример использования перечислимого типа /* Пример перечислимого типа */ ^include <stdio.h> maint) < enum light_type < red, amber, green ); enum light_type traffic_light = green; switch ( traffic_light ) { case red : printf( "ЧпКрасный свет" ); break; case amber : printf( н\п1елтый свет" ); break; case green : printf( "ЧпЗеленый свет" ); > priotft "\n" ); ) Перечислимый тип задается перечислением набора констант (например, red, green, amber). Каждой константе ставится в соответствие целое число. Если к программе на листинге 4.7 добавить оператор printf ("ЧпЗначение переменной traffic_light = %d", traffic_light); то будет напечатано значение 2. Первой по порядку константе - red - ставится в соответствие 0, следующей - amber - целое значение 1, а последней - green - целое значение 2. Программист может и сам приписать целочисленные значения каждой из перечисленных констант, например enum light_type < red = 5, amber = 2, green = 7 >; Ясно, что использование перечнслимых типов определяет более естественное соответствие между множеством
104 Глава 4 перечисленных констант и множеством целых чисел. В некоторых новейших компиляторах с Си, таких, как Турбо Сн, объекты перечислимого типа могут смешиваться с объектами других целых типов. В Турбо Си можно написать int 1 = 14 + trafflc_llght; В некоторых реализациях Си ие разрешается смешивать перечислимые типы и целые типы. Для обеспечения мобильности следует преобразовывать значения перечислимого типа в целый тип перед их совместным использованием. Обсуждение вопросов, связанных с областями действия констант перечислимого типа, отложим до гл. 8, в которой подробно рассмотрим понятия «область действия» и «видимость». 4.12. Приведение н преобразование типоа Для выполнения однозначного преобразования (cast) объектов одного типа в другой в языке имеется специальная конструкция вида (имя_типа) выражение Например, float г = 3.5; int i; 1 = (int) r; Преобразование одного типа данных в другой тип данных выполняется в соответствии со следующими условиями: • Преобразование используется для однозначного перевода данного значения в другой тип. • Операнд автоматически приводится к другому типу перед выполнением соответствующей арифметической или логической операции. • Если операнд одного типа присваивается леводопус- тимому объекту другого типа, то приведение типов выполняется автоматически.
Скалярные типы данных, операции, преобразования 105 • Аргумент функции может автоматически приводиться к требуемому типу прямо в вызове функции. • Результат функции может быть автоматически приведен к требуемому типу в момент возврата из функции. Следует отличать преобразование, которое изменяет битовую запись значения (представление данных), от преобразования, изменяющего интерпретацию данных. В первом случае выполняется действие более существенное, чем во втором. Например, когда беззнаковый тип переводится в целый тип, не требуется менять представление данных. Когда же тип float приводится к типу int. то из-за разницы в размерах используемой для данных типов памяти должно быть произведено изменение представления. В различных реализациях Си приведение типов осуществляется по-разному. Например, если знаковое целое преобразуется в беззнаковое целое того же размера, то компилятор с Си, использующий для представления знаковых целых дополнительный код, не будет изменять представление. Если же в реализации знакового целого используется отдельный знаковый разряд, то изменения представления не избежать. Читателю должно быть ясно, что преобразование типов может сделать код иемобильным, и, поэтому, если уж преобразований типов не удается избежать, соответствующие места в программе должны быть аккуратно выделены (прокомментированы). Если значение вещественного типа (float) преобразуется в целое и если оно выйдет нз диапазона допустимых значений для целых, то произойдет ошибка преобразования и полученный результат преобразования будет немобильным (и непредсказуемым). 4.12.1. Преобразование при присваивании В операторе присваивания типы выражений в левой и правой частях оператора должны быть одинаковыми. Если это не так, компилятор делает попытку привести тип значения правой части к типу левой части. Это бывает возможным прн следующих условиях: • Тнп левой части является любым арифметическим типом, и тип правой части - арифметический тип.
106 Глам 4 • Тип левой части - это любой тип указателя, а в правой части стоит целая константа 0. • Тип левой части - это указатель на тип Т. а в правой части стоит массив типа Т. • Тнп левой части - это указатель на функцию, а в правой части стоит функция. Например, float г = 5; Здесь целое значение 5 переводится в значение с плавающей точкой 5.0. 4.12.2. Преобразования для унарных операций Автоматическое преобразование типов выполняется для операндов унарных операций !, -, ~ и * и, кроме того, для операций « и », которые фактически являются двухместными операциями. Дополнительно в Турбо Си используется унарная операция +. Арифметические значения более узких типов преобразуются в арифметические выражения более широких типов. Короткие целые переводятся а тип int. а числа с плавающей точкой переводятся в тип double. Массивы и функции переводятся в соответствующие указатели. Более строго: • Типы char и short приводятся к типу int. • Типы unsigned char и unsigned short приводятся к типу unsigned. • Тнп float приводится к типу double. • Массив типа Т приводится к типу указателя на Т. • Функция, возвращающая значение типа Т, приводится к указателю на функцию, возвращающую тип Т. 4.12.3. Преобразования для двухместных операций Если операция выполняется над двумя операндами, то оба оии сначала переводятся к простому общему типу, н
Скалярные типы данных, операции, преобразования 107 результат, вырабатываемый операцией, обычно оказывается того же общего типа. Короткие арифметические типы расширяются (как показывалось выше), а массивы и функции приводятся к указателям. Более строго: Если один из операндов типа double, то другой операнд приводится к типу double. Если один из операндов типа unsigned long, то другой операнд приводится к типу unsigned long. Если один из операндов типа long int, а другой типа unsigned int, то каждый из операндов приводится к типу unsigned long int. Если одни из операндов типа unsigned int, то другой операнд приводится к типу unsigned int. Если один из операндов типа char или unsigned char, то он приводится к типу int. Если один из операндов типа enum, то ои приводится к типу int. Рассмотрим пример: int i = 5; double г = 7.0; if ( i < г) Значение i = 5 приводится к 5.0 перед выполнением сравнения со значением 7.0.
Глава 5 Управляющие структуры В этой главе рассматриваются конструкции языка (Турбо) Си, позволяющие управлять логикой выполнения программы. Управляющие структуры можно разделить иа две группы: структуры выбора и циклы. Каждая из указанных групп рассматривается в настоящей главе. 5.1. Блоки и составные операторы Любая последовательность операторов, заключенная в фигурные скобки, является составным оператором (блоком). Возможности локализации данных с помощью блоков расссматриваются в гл. 8. Составной оператор ие должен оканчиваться точкой с запятой, поскольку ограничителем блока служит сама закрывающая скобка. Внутри блока каждый оператор должен оканчиваться точкой с запятой. Составной оператор может использоваться везде, где синтаксис языка допускает обычный оператор. На листинге 5.1 приведен пример использования составного оператора или блока сразу вслед за оператором if. Листинг 5.1. Пример использования составного оператора /* Фрагмент программы, иллюстрирующий использование составного оператора */ if(x+y-z>w) < prlntf( "\пНаконец-то удача"); х += 3; У "= 4; z *= 6; > 5.2. Пустой оператор Пустой оператор (null statement) представляется символом «точка с запятой», перед которым нет выражения. Пустой оператор используют там, где синтаксис языка требует присутствия в данном месте программы оператора,
Управляющее структуры 109 однако по логике программы оператор должен отсутствовать. Необходимость в использовании пустого оператора часто возникает при программировании циклов, когда действия, которые могут быть выполнены в теле цикла, целиком помещаются в заголовок цикла. Стиль программирования Рекомендуется точку с запятой, относящуюся к пустому оператору, помещать на отдельной строке - иначе при чтении и анализе программы можно не обратить внимания на пустой оператор. Пример оператора цикла, вслед за которым помещен пустой оператор, приведен иа листинге 5.2 при описании функции length (длина). Листинг 5.2. Пример пустого оператора int lenghUchar »str) < int index = 0; while ( strl jndex++ 1) t return —index; ) Обратим внимание на то, что цикл while иа листинге 5.2 можно заменить на другой его вариант: while ( strl index++ 1) continue; Возможно, этот вариант демонстрирует несколько лучший стнль программирования. 5.3. Конструкция аыбора Все выражения, реализующие условия для каждой конструкции выбора, должны заключаться в круглые скобки. 5.3.1. Оператор IF Синтаксис оператора IF: if ( выражение ) оператор
110 Глам S Как уже отмечалось выше, там, где синтаксис языка предписывает использовать оператор, может стоять и составной, и пустой оператор. Если выражение в заголовке условного оператора вырабатывает ненулевое значение, то оператор в условном онераторе выполняется, в противном случае управление передается оператору, следующему за условным. На листинге 5.3 приведена функция, сравнивающая задаваемые в обрашеиин к ней три целых числа и возвращающая наименьшее из этих трех. Листинг 5.3. Использование оператора IF ^include <stdlo.h> main() { extern int smallest ( int valuel, Int value2, int value3 ); printf( "Наименьшее значение из -12, -15, -10 = %d\n", smallest( -12, -15, -10 ) ); > int smallest ( int valuel, int value2, int value3 ) { if ( valuel <= value2 && valuel <= value3 ) return valuel; if ( value2 <= valuel && value2 <= value3 ) return value2; return value3; > 5.3.2. Оператор IF ELSE Синтаксис оператора IF ELSE таков: If ( выражение ) оператор1 else оператор2 Если значение выражения ие равно нулю, то выполняется оператор!, в противном случае - оператор2.
Управляющие структуры 111 Предупреждение: обособленный ELSE Может показаться, что приведенная иа листинге 5.4 программа ничего не напечатает. Создается впечатление, что ELSE относится к первому оператору IP. Но ие верьте глазам своим! Синтаксис (Турбо) Си предписывает, что ELSE всегда относится к ближайшему оператору IF, т.е. в нашем случае - ко второму. Поскольку выражение в первом IF истинно, а во втором - ложно, то программа на листинге 5.4 напечатает "оператор 2". Листинг 5.4. Обособленный ELSE ttinclude <stdio.h> mainO { int i = 4, J = 6, k = 8; if Ci < k ) if (i > J) printf( "оператор l\n" ); else printf( "оператор 2\n"); > Программа иа листинге 5.5 так же, как и программа иа листинге 5.3, вычисляет наименьшее из трех чисел, однако с точки зрения логики оиа выглядит проще. Здесь использована конструкция IP ELSE. Листинг 5.5. Применение конструкции IF ELSE ^include <stdio.h> mainO { extern int smallest ( int value1, int value2, int value3 ); printn "Наименьшее значение из -12, -15, -10 = %d\n", smallest! -12, -15. -10 ) ); }
112 Глам 5 int smallest ( int valuel, Int value2, Int value3 ) < if ( value2 < valuel ) { if ( value3 < value2 ) return value3; else return value2; > else if ( value3 < valuel ) return value3; else return valuel; 5.3.3. Условная операция ? Условная операция ? может с успехом использоваться вместо конструкции IF ELSE там, где входящие в нее операторы являютсн простыми выражениями. Синтаксис условной операции таков: результат = выражение ? выражение1 : выраженнеЗ Для примера рассмотрим программу на листинге 5.6. Перемеииоб result при ее инициализации будет присвоено значение i, если выражение ( i < j ) истинно, и j, если выражение ( i < j ) ложно. В нашем случае result инициализируется значением j. Листинг 5.6. Пример использования операции ? «include <stdio.h> mainO { int i = 6, j = 4; int result = ( i < j ) ? i : j; printf ( "%d\n", result ); }
Управляющие структуры 113 На листинге 5.7 дан еще один пример использования операции ? В, этом примере никакого ивного результата не получается. Поскольку выражение ( i < j ) в данном случае ложно, то вычисляется второе выражение printf ( "i >= j" ) Листинг 5.7. Еще один пример использования операции ? ^include <stdio.h> main() { int i = 6, j = 4; (i < j) ? printft "i < j\n" ) : printf( "i >= j\n" ); > И, наконец, на листинге 5.8 приведен последний пример использования операции ? В качестве результата при помощи функции printf выводится следующий текст: Минимум из i и j - это 3. Листинг 5.8. Последний пример использования операции ? «include <stdio.h> main() < int i = 3, j = 5; printf ( "Минимум из i и j - это 5id\n.", ( i < j ) ? i : j ); > 5.3.4. Оператор SWITCH Конструкция SWITCH заменяет разветвленный многократный оператор IF ELSE. Из-за некоторой особенности (нечетности) конструкции IF ELSE большинство пользователей при программировании вложенных условий на языке Си больше усилий тратят на преодоление трудностей язы-
114 Глава 5 ка, а не на выражение логики программы) Синтаксис оператора SWITCH таков: switch ( выражение ) { case константное_выражение_1 : оператор(ы) case константное_выражение_2 : оператор(ы) case константное_выражение_п : оператор(ы) default: оператор(ы) } После вычисления выражения в заголовке оператора его результат последовательно сравнивается с константными выражениями, начиная с самого верхнего, пока не будет установлено их соответствие. Тогда выполняются операторы внутри соответствующего case, управление переходит на следующее константное выражение и проверки продолжаются. Именно поэтому в конце каждой последовательности операторов должен присутствовать оператор break. После выполнения последовательности операторов внутри одной ветки case, завершающейся оператором break, происходит выход из оператора SWITCH. Обычно оператор SWITCH используется тогда, когда . программист хочет, чтобы была выполнена только одна последовательность операторов из нескольких возможных. Каждая последовательность операторов может содержать нуль или более отдельных операторов. Фигурные скобки в этом случае ие требуются. Ветка, называемая default (умолчание), может отсутствовать. Если она есть, то последовательность операторов, стоящая непосредственно за словом default и двоеточием, выполняется только тогда, когда сравнение ии с одним из стоящих выше константных выражений не нети вно. Листинг 5.9 иллюстрирует применение конструкции SWITCH. Функция error_message (сообщеияе_об_ошибке) выводят одно из пяти сообщений в зависимости от значения параметра errorcode (кодошибки), лежащего в диапазоне
Управляющие структуры 115 от единицы до пяти. Вели значение параметра не принадлежит указанному диапазону, то выводится сообщение «Неверный код ошибки>. Листинг 5.9. Пример использования конструкции SWITCH •include <stdio.h> void error jaessage( int error_code ) < switch ( error_code ) { case 1 : printf( "ЧпСообщение 1" >; break; case 2 : printf( "ЧпСообщение 2" ); break; case 3 : printf( "ЧпСообщение 3" ); break; case 4 : printf( "ЧпСообщение 4" ); break; case 5 : printf( "ЧпСообщение 5" ); break; default : printf( "ЧпНеверный код ошибки" ); > > 5.3.5. Оператор GOTO Оператор GOTO используется дли передачи управлении внутри функции от одного оператора к другому. Синтаксис оператора СОТО такой: goto идентификатор; Управление передается безусловно на оператор в теле той же функции, помеченный указанным идентификатором.
116 Главе 5 Например, goto backend; backend: x += 3; Хоти в (Турбо) Си и разрешена передача управления иа любой оператор в теле функции, опыт показывает, что следует пользоваться этой возможностью как можно реже или вовсе от иее отказаться. Бессистемное использование оператора GOTO может затруднить последующее сопровождение кода программы и свести иа иет усилия компилятора по оптимизации программы. Вели все-таки применяется оператор GOTO, то целесообразно придерживаться следующих рекомендаций: • Не входите внутрь блока извне. • Не входите внутрь оператора IF или ELSE конструкции IF ELSE или оператора SWITCH. • Не входите внутрь итерационной структуры (оператора цикла) извне этой структуры. В некоторых современных структурных языках программирования, таких, как Модула-2, оператор GOTO вообще не разрешен из-за возможных ошибок, которые порождает его использование. 5.4. Циклы Циклы, или итерационные структуры, позволяют повторять выполнение отдельных операторов или групп операторов. Число повторений в некоторых случаях фиксировано, а в других определяется в процессе счета на основе одной или нескольких проверок условий. Бывают циклы с проверкой условия перед началом выполнения тела цикла (top-testing), no окончании выполнения тела (bottom-testing) или внутри тела (middle-testing). Ниже будут рассмотрены все указанные типы циклов.
Управляющие структуры 117 Б.4.1. Цикл WHILE Синтаксис цикла WHILE (пока) таков: while ( условиое_выражеиие ) оператор Ясно, что в цикле типа WHILE проверка условия производится перед выполнением тела цикла. Если результат вычисления условного выражения ие равен нулю, то выполняется оператор (или группа операторов). Перед входом в цикл WHILE в первый раз обычно инициализируют одну или несколько переменных для того, чтобы условное выражение имело какое-либо значение. Оператор или группа операторов, составляющих тело цикла, должны, как правило, изменять значения одной или нескольких переменных, входящих в условное выражение, с тем чтобы в конце концов выражение обратилось в нуль и цикл завершился. Предостережение Потенциальной ошибкой при программировании цикла WHILE, как, впрочем, и цикла любого другого типа, является запись такого условного выражения, которое никогда не прекратит выполнение цикла. Такой цикл называется бесконечным. На листинге 5.10 приведен пример программы, содержащей бесконечный цикл. Прекратить выполнение такой программы на Турбо Си можно тремя и только тремя способами: набрать на клавиатуре комбинацию клавиш Ctrl+C, перезагрузить компьютер или выключить электропитание. Не существует более изящных способов прервать такую зациклившуюся программу! Листинг 5.10. Пример зацикливания aain О int i = 4; while ( i > 0 ) printff "ЧпТурбо Си быстр и эффективен\п" ); >
118 Глава 5 Цикл WHILE завершается в следующих случаях: • Обратилось в нуль условное выражение в заголовке цикла. • В теле цикла встретился оператор break. • В теле цикла выполнился оператор return. В первых двух случаях управление передается иа оператор, располагающийся непосредственно за циклом. В третьем случае происходит возврат из функции. Программист, поработавший ранее на языке Паскаль и переходящий на (Турбо) Си, может непреднамеренно допустить ошибку, представленную на листинге 5.11. Эта ошибка порождает бесконечный цикл. Ошибка состоит в том, что вместо оператора сравнения иа равенство (==) использован оператор присваивания. Компилятор Турбо Сн выдает предупреждение о возможной ошибке в следующем виде: Warning test.с 5: Possibly incorrect assignment in function main (Предупреждение в test.с 5: Возможно некорректное присваивание в функции main) Следует обращать внимание на все предупреждающие сообщении, выдаваемые компилятором. В конце концов вы обнаружите причину появления такого сообщения и, возможно, исправите что-то в программе. Листинг 5.11. Распространенная ошибка, приводящая к бесконечному циклу main () int j = 5; while ( j = 5 ) { printf( "\nj = 5\n" ); } }
Управляющие структуры 119 Часто цикл WHILE используется для проверки ответов пользователя на вопрос из программы. В программе иа листинге 5.12 тело цикла начнет выполняться, если пользователь введет любой символ, отличный от у или п. Цикл будет выполняться до тех пор, пока пользователь ие введет или у, или п. i Листинг 5.12. Пример использования цикла WHILE printf( "ХпОтвечайте yes или по (у/п) : ") ; scanf( "%c\ &ch ); while ( ch != 'у' && ch !=. 'п' > { printf( "ЧпОтвечайте yes или по (у/п) : ") ; scanf( "%c", &ch ); ) Функция search (поиск), приведенная иа листинге 5.13, возвращает неотрицательный индекс, определяющий положение целой переменной key (ключ) в массиве matchup, и -1, если в массиве такого значения нет. Передача массивов как параметров функции рассматривается в гл. 6. Строки программы, непосредственно выполняющие поиск значения key: while ( index < size && data! index 1 != key ) index++; return (data! index ] == key ) ? index : -1; Здесь используется условный оператор ?. Листинг 5.13. Поиск элемента в массиве «include <stdio.h> int scores! 100 ]; main () { extern int searcM int data U, int size, int key ); int i;
120 Глава 5 for ( i = 0; i < 100; i++ ) scoresl i ] = i; printf( "ХпИндекс числа 96 в массиве = %d", search( scores, 100, 96 ) ); printft "ХпИндекс числа 107 в массиве = %d\n", searcht scores, 100, 107 ) ); } int search( int data!!, int size, int key ) { int index = 0; while ( index < size 8.8. data[ index 1 != key ) index++; return ( datat index ] == key ) ? index : -1; > Предостережение: класс ошибок типа «лишний шаг» Человеку свойственно ошибаться. Циклы предоставляют нам обширные возможности проявить нашу человечность. На листинге 5.14 приведен пример программы, содержащей ошибку типа «лишний шаг». Ошибка, допущенная в функции sum, является, вероятно, ошибкой наиболее серьезного типа. Программа будет скомпилирована, начнет выполняться н чаще всего будет выдавать корректный результат. Цикл WHILE будет выполнен ровно size+1 раз. К сожалению, это приведет к выходу за границу массива data, так как, согласно описанию, массив имеет размер 100, а не 101. Попытка обратиться к элементу data[100] может обойтясь без последствий, но может и привести.к серьезной ошибке. Результат выполнения программы будет некорректным, если значение второго параметра вызова функции меньше, чем размер массива. Пусть обращение имеет вид sum(data,2). Результат будет 3 вместо правильного 1. Листинг 5.14. Пример проявления ошибки «лишний шаг» ^include <stdio.h> int scoresl 100 ]; main () < extern int sum( int data [], int size );
Управляющие структуры 121 Int i; for ( i = 0; i < 100; i++ ) scoresi 1 J = i; printf( "ХпСумма значений в массиве scores = %d\n", sum( scores, 100 ) ); > int sum(int data!], int size ) < int cumulative_sum = 0; int index = 0; while ( index <= size ) cumulative_sum += data! index++ 1; return cumulative_sum; > 5.4.2. Цикл DO WHILE - В цикле DO WHILE проверка условия осуществляется после выполнения тела цикла. Синтаксис цикла do оператор while ( усжовное_выражение ); Как уже указывалось, в (Турбо) Си вместо одиночного оператора (например, в теле рассматриваемого цикла) может быть подставлена группа операторов (блок). Цикл DO WHILE прекращает выполняться, когда условное выражение обращается в нуль (становится ложным). Как и для цикла WHILE, для цикла DO WHILE можно описать ситуации, приводящие к выходу из цикла: • Условное выражение обратилось в 0. • Внутри цикла встретился оператор break. • Внутри цикла выполнен оператор return. На листинге 5.15 показаны некоторые практические приемы использования циклов DO WHILE н других управляющих структур, рассмотренных в настоящей глане. Представьте, что требуется лексикографически сравнить две строки (два массива символов). Функция compare (срав-
122 Глава 5 ннть), приведенная иа листинге 5.15, возвращает значение -1, если первая строка (strl) меньше второй строки (str2); возвращает значение 0, если строки совпадают, и возвращает 1, если первая строка больше второй строки. Формальное описание строк как структур данных и стандартной библиотеки работы со строками дано в разд. 6 гл. 6. В этой стандартной библиотеке имеются функции strlen и strcmp, вычисляющие длину я сравнивающие строки; их реализация приведена на листинге 5.15. Главная идея алгоритма функции сравнения - это просмотр каждой нз строк до тех пор, пока либо ие будет достигнут конец самой короткой нз двух строк, либо не проявится несоответствие строк. Цикл DO WHILE, выполняющий указанные действия, выглядят следующим образом: minlength = ( lenl < 1еп2 ) ? lenl : 1еп2; do index++; while ( Index < ainlength && strlt index 1 == str2l index 1 ); И опять оказалось удобным использовать условный оператор ? По завершении цикла для определения того, что же возвращать функции compare в качестве результата, используется конструкция IF ELSE удивительно большой вложенности. Листинг 5.15. Пример программировании цикла DO WHILE: сравнение двух строк ^include <stdio.h> main () { extern int coapare( char strlU, char str2I1 ); char strll 80 1, str2[ 80 ]; printf( "ЧпВведнте первую строку: "); scanf( "Xs", strl); printf( "ХпВведите вторую строку: "); scanfl "7.s", str2); if ( compare( strl, str2 ) < 0 )
Управляющие структуры 123 printft "\п\пПервая строка меньше второй." ); else if ( comparet strl, str2 ) == 0 ) printft "\п\пПервая строка равна второй." ); else printft "\п\пПервая строка больше второй." ); printft "\п" ); int lengtht char strtl ) { int index = 0; while ( strl index++ 1 != 0 ) * return —index; int comparet char strlfl, char str2N ) i extern int lengtht char str N ); int lenl = lengtht strl ); int len2 = lengtht str2 ); int index = -1; int minlength; minlength = ( lenl < len2 > ? lenl : len2; do index++; while ( index < minlength && strlt index 1 == str2t index 1 ); if ( lenl == len2 && strll index ] == str2t index 1 ) return 0; else if ( strlt index ] == str2t index ] && lenl < len2 ) return -1; else if ( strll index 1 == str2t index ] && len2 > lenl ) return 1; else if ( strlt index ] < str2[ index 1 ) return -1; else return 1;
124 Глава 5 5.4.3. Цикл FOR Наиболее общей формой цикла в (Турбо) Си является цикл FOR. Цикл FOR - это более общая и более мощная форма, чем аналогичный цикл в языках Паскаль и Модула-2. Конструкция FOR выглядит следующим образом: for ( [ необязательное выражеиие1 ]; [ необязательное выражениё2 ]; [ необязательное выракеииеЗ 1 ) оператор Каждое нз трех выражений можно опускать. Хотя в принципе каждое нз этнх выражений может быть использовано программистом как угодно, обычно- первое выражение служит для иннцналнзацнн индекса, второе - для выполнения проверки на окончание цикла, а третье выражение - для изменения значения индекса. Формально это правило можно опнсать так: 1. Если первое выражение присутствует, то оно вычисляется. 2. Вычисляется второе выражение (если- оно присутствует). Если вырабатывается значение 0, то цикл прекращается, в противном случае цикл будет продолжен. 3. Исполняется тело цикла. 4. Вычисляется третье выражение (еслн оно присутствует). 5. Переход к пункту 2. Появление в любом месте тела цикла оператора continue приводит к немедленному переходу к шагу 4. Цнкл FOR можно свести к циклу WHILE следующим образом: Цикл FOR: for ( выражеиие1; выражение2; выражениеЗ ) оператор; переводится в
Управляющие структуры 125 выражение1; while ( выражение2 ) { оператор; выражениеЗ; ) Цикл for ( ; ; ) также имеющий право на существование, будет выполниться весьма долго! Это один из способов записать бесконечный цикл. На листинге 5.16 приведена короткая программа, выводящая четные числа от 1000000 до 0 в порядке убывания. Листинг 5.16. Пример цикла FOR: четные числа от 1 000 000 до 0 «include <stdio.h> aainO i long i; for ( i = 1000000; i >= 0; i -= 2 ) printfl "\nXld", i ); printfl "\n". ); ) Чтобы продемонстрировать гибкость цикла FOR в (Турбо) Сн, перепишем цикл из рассмотренной программы следующим образом: for ( i = 1000000; i >= 0; printfl "\nXld\ i ), i -= 2 ) Еще одно решение той же самой задачи вывода четных чисел от 1 000 000 до 0 в порядке убывания дано на листинге 5.17. Здесь же показано использование оператора continue.
126 Глава 5 Листинг 5.17. Оператор continue: четные числа от 1 000 000 до 0 ttinclude <stdio.h> main() { long i; for ( 1 = 1000000; i >= 0; 1 —) if ( i % 2 == 1 ) continue; else printf< "VnXld". i ); printfl "\n" ); > 5.5. Несколько примеров В этом разделе вы найдете несколько прикладных программ, в которых используются управляющие структуры, рассмотренные выше. 5.5.1. Суммирование подвекторов: даа подхода Предположим, нам дан массив целых чисел. Определим подвектор как один или более последовательных элементов массива. Каждому подвектору поставим в соответствие сумму его элементов. Требуется написать функцию, вычисляющую максимум из сумм подвекторов в массиве. На листинге 5.18 использован алгоритм, решающий поставленную задачу грубой силой. Три вложенные цикла FOR гарантируют, что каждая возможная комбинация элементов будет суммирована и ее сумма будет сравнена с текущим значением максимума. Недостатком такого алгоритма является то, что объем вычислений увеличивается как куб от числа элементов массива (из-за трех вложенных циклов). Так, если мы удвоим размер массива, то Объем вычислений увеличится в восемь раз.
Управляющие структуры 127 Листинг 5.18. Мансимум сумм подвекторов: алгоритм грубой силы /* Суммирование подвекторов с использованием алгоритма грубой силы */ ttinclude <stdio.h> const int mlnint = -32768; /• Это минимальное целое •/ int dataM = { -11, 13, 14, -30, 18, 20, -3 ); main() extern int sumofsubf Int vectorE), int size ); printf( "Чпмаксимум simofsub для data = 'And", sumofsubt data, 7 ) ); > int sumofsub( int vector!], int size ) { int max = minint; int total, first_index, second_index, third_index; for ( first_index = 0; first_index < size; first_index++ ) for ( second_index = first_index; second_index < size; second_index++ ) { total = 0; for ( third_index = firstjndex; third_index <= second_index; third_index++ ) total += vectorE third_index J; max = ( total > max ) ? total : мах; } return max; } Гораздо более эффективное решение приведено на Листинге 5.19. Предлагаем читателю самому «пройтись» по алгоритму, заданному новой функцией sumofsub, и убедиться, что эта функция действительно вычисляет мансимум сумм подвекторов. В этом более эффективном решении Объем вычислений возрастает линейно с увеличением размера массива.
128 Глава 5 Листинг 5.19. Максимум сумм подвекторов: эффективное решение /• Суммирование подвекторов с использованием эффективного алгоритма •/ ♦include <stdio.h> const int minint = -32768; /• Это минимальное целое •/ int dataN = { -11, 13, 14, -30, 18, 20, -3 ); main() { extern int sumofsubl int vector!J, int size ); printfl "ЧпМаксимум sumofsub для data = X\nd", sumofsub( data, 7 ) ); ) int sumofsubl int vectorN, int size ) { int maxsofar = minint; int maxendinghere = 0; int index; for ( index = 0; index < size; index++ ) { maxendinghere = ( maxendinghere + vectorl index 1 > 0 ) ? maxendinghere + vector! index J : 0; maxsofar = ( maxendinghere > aaxsofar ) ? maxendinghere : maxsofar; ) return maxsofar; ) Программа на листинге 5.20 сравнивает времена работы двух алгоритмов. Поскольку алгоритм грубой силы (в этой программе он назван sumofsubl) слишком медленный, то его хронометрирование производится только дли. массивов длиной 200 и 400. Для гораздо более быстрого алгоритма (он назван sumofsub2) оказывается возможным для тестирования брать массивы размером 2000, 4000, .... 28000 н 30000. Обратите внимание на то, что компилятор Турбо Сн
Управляющие структуры 129 при трансляции программы выдаст предупреждающее сообщение о том, что переменной max_sum_of_sub присваивается значение, которое в дальнейшем не используется. Эта некорректность объясняется тем, что функции вызываются лишь для замера времени их работы. Листинг 5.20. Программа сравнения двух функций суммирования подвекторов /* Сравнение времени работы двух программ суммирования подвекторов */ «include <stdio.h> «include <util.h> «define max_size 30000 const int minint = -32768; /• Минимальное целое •/ int datalmax_size]; mainO extern int sumofsubK int vector!], int size ); extern int sumofsub2( int vector!], int size ); int i, iteration; int max_sum_of_sub; for ( i = 0; i < max_size; i++ ) data! i ] = randoml -50, 100); printf( "ЧпВремя работы алгоритма грубой силы для 200\п" ); rpttimingt begin ); max_sum_of_sub = sumofsubK data, 200 ); rpttimingt end ); printf( "ЧпВремя работы алгоритма грубой силы для 400\п" ); rpttimingt begin ); max_sum_of_sub = sumofsubK data, 400 ); rpttimingt end ); for ( iteration = 1; iteration <= 15; iteration** ) { 6 Зек. 795
130 Глава 5 printf("ЧпБыстрый алгоритм для размера Xd", iteration • 2000 ); rpttilling! begin ); max_sum_of_sub = sumofsub2( data, iteration • 2000 ); rpttimingt end ); ) printf( "\n" ); ) int sumofsubK int vector!], int size ) i int max ■= minint; int total, first_index, secondjndex, third_index; for ( first_index = 0; first_index < size; first_index++ ) for ( secondjndex = first_index; secondjndex < size; secondjndex++ ) { total = 0; for ( thirdjndex = firstjndex; thirdjndex <= secondjndex; thirdjndex++ ) total += vectorl thirdjndex J; max = ( total > max ) ? total : max; ) return max; ) int sumofsub2( int vector!1, int size ) < int maxsofar = minint; int maxendinghere = 0; int index; for ( index = 0; index < size; index++ ) { maxendinghere = ( maxendinghere + vector! index ] > 0 ) ? maxendinghere + vector! index 1 : 0; maxsofar = ( maxendinghere > maxsofar ) ? maxendinghere : maxsofar;
Управляющие структуры 131 ) return aaxsofar; ) Ниже приведены результаты хронометрирования, полученные для компилятора Турбо Си версии 1.0 на компьютере COMPAQ 386: Алгоритм грубой силы размер 200 0 часов, 0 минут, 4 секунды, 23 сотых размер 400 0 часов, 0 минут, 33 секунды, 3 сотых Примечание: при удвоений размера массива времн вычислений увеличилось приблизительно в восемь раз. Быстрый алгоритм размер часы минуты секунды сотые 2000 4000 6000 8000 10000 12000 14000 16000 18000 20000 22000 24000 26000 28000 30000 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 6 6 16 16 16 22 22 2В S.S.2. Вычисление размера фиксироааииых аыплат при простом процентном займе Хотя и существует формула вычисления размера фиксированной ежемесячной выплаты при простом процентном займе, мы будем вычислять соответствующую сумму с точ- 5*
132 Глава 5 ностью до полпенни. Для этого воспользуемся следующим алгоритмом: Шаг 1. В качестве начального приближения месячного платежа (тр) взять величину р/п, где р - размер займа, а п - число месяцев, за которые следует рассчитаться за заем. Эта величина будет меньше требуемой, так как она не включает процентные начисления. Если процентная ставка равна 0, то это начальное приближение будет верным решением. Шаг 2. Подсчитать баланс после п выплат bal(n) на основе величины тр, вычисленной на шаге 1. Разностное уравнение, позволяющее вычислить сумму, имеющуюся к месяцу t+1, в зависимости от суммы, имеющейся к месяу t, выглядит следующим образом: bal(t+l) = bal(t) + mr * bal(t) - mp, где bal(t+l) - сумма, имеющаяся в наличии к месяцу t+1; bal(t) - сумма, имеющаяся в наличии к месяцу t; mr - месячная процентная ставка (годовой процент, деленный на 12); тр - месячная выплата. Шаг 3. Пока bal(n) > 0, выполнять в цикле Увеличить тр на 20.0 (двадцать долл.). Вычислить новое значение bal(n). Конец цикла. Шаг 4. Пока bal(n) < 0, выполнять в цикле Уменьшить тр на 5.0 (пять долл.). Вычислить новое значение bal(n). Конец цикла. Шаг 5. Пока bal(n) > 0, выполнять в цикле Увеличить тр на 1.0 (один долл.). Вычислить новое значение bal(n). Конец цикла. Шаг 6. Пока bal(n) < 0, выполнять в цикле Уменьшить тр на 0.01 (один цент). Вычислить новое значение bal(n). Конец цикла. Шаг 7. Вывести значение тр.
Управляющие структуры 133 n, mp, principal, тг ) > 0 ) Программа на Турбо Сн, реализующая представленный алгоритм, приведена на листинге 5.21. Приближенное значение размера месячного платежа (тр) определяется с помощью следующей последовательности циклов WHILE в функции monthly_payment (месячный платеж): while ( bal( n, mp, principal, тг ) > 0 ) тр += 20.0; while ( bal( n, mp, principal, mr ) < 0 ) тр -= 5.0 while ( bal( тр += 1.0 while ( bal( n, mp, principal, mr ) < 0 ) mp -= 0.01; return mp; Листинг 5.21. Вычисление фиксированного месячного платежа при простом процентном займе /* Программа вычисления фиксированного месячного платежа при простом процентном займе */ •include <stdio.h> mainO { extern float monthly_payment( int n, float principal, float mr ); float loanjamount; float monthly_xate; int term_of_loan; printf( "ЧпВведите размер займа: t " ); scanf( '"/.f", &loan_amount ); printf( "ЧпВведите месячную процентную ставку: "); scanff "Xf", &monthly_rate ); printf( "ЧпВведите число месяцев: " ); scanf( "%d", &term_of_loan ); printf("ЧпЧпФиксированный месячный платеж = Stf.2f\n", monthly_payment( term_of_loan, loan_a*ount, monthly_rate ) ); ) float bal( int n, float mp, float principal,
134 Глава S float mr ) float current_balance, previousJbalance = principal; int index; for ( index = 1; index <= n; index++ ) < current_balance = ( 1.0 + mr ) * previous_balance - mp; previous_balance = current_balance; > return current_balance; float monthly_payment( int n, float principal, float mr ) { extern float bal( int n, float mp, float principal, float mr ); float mp = principal / n; while ( bal( n, mp, principal, mr ) > 0 ) mp += 20.0; n, mp, principal, mr ) < 0 ) n, mp, principal, mr ) > 0 ) while ( ball mp -= 5.0; while ( bal( mp += 1.0; while ( bal( n, mp, principal, mr ) < 0 ) mp -= 0.01; return mp; ) Вот так выглядит вывод программы прн типичном расчете (займ 5000$ под 8.5% на 10 лет): Введите размер займа: t 5000 Введите месячную процентную ставку: 0.00708333 Введите число месяцев: 120 Фиксированный месячный платеж = $61.99
Глава 6 Указатели и массивы На первый взгляд читателю может показаться ошибкой то, что глава, посвященная основным для (Турбо) Си понятиям - указателям и массивам,- попала в середину книги. Указатели обеспечивают доступ к адресам памяти и манипуляции с ними. С помощью указателей можно строить - одномерные, двумерные и многомерные массивы, а также строки символов. Используя указатели, можно создавать динамические структуры. Наконец, указатели помогают передавать функции в качестве параметров других функций. Другими словами, для того чтобы стать знатоком Си, нужно прежде всего стать знатоком указателей. Цель настоящей главы состоит в том, чтобы помочь вам стать специалистом по указателям в (Турбо) Си. Кроме того, здесь рассматриваются массивы, которые являются, пожалуй, наиболее важным структурным типом в (Турбо) Сн, и приводятся примеры решения задач с использованием одномерных и многомернных массивов. Особое внимание уделяется важному классу одномерных массивов, именуемому строкой. Хотя формально функции ие будут рассматриваться до гл. 8, в настоящей главе мы изучим важный вопрос, связанный с использованием указателей н массивов в (Турбо) Си как параметров функций. Точнее говоря, мы рассмотрим, как указатели и массивы передаютси в качестве входных и выходных параметров функции. Отложив рассмотрение указанных вопросов до гл. 8, мы тем самым существенно сузили бы рамки обсуждения указателей и массивов. 6.1. Указатели и модели памяти Указатель - это адрес памяти. Значение указателя сообщает о том, где размещен объект, но не говорит о самом объекте. Символ операции * используется для задания «указателя на» объект. Например, рассмотрим следующее описание'. int *х;
136 Глава 6 его следует понимать как «х является указателем на целое». Указатель на тип void совместим с любым указателем. Например, если задано void «x; lnt *y; то допустимо следующее присваивание: У = х; В операционной системе MS-DOS для микропроцессора семейства 8086 и Турбо Си размер указателя (число байтов, требуемых для размещения адреса памяти под указатель) зависит от модели памяти, задаваемой при компиляции программы. В Турбо Си программы можно компилировать в расчете на шесть моделей памяти: крошечная (tini), маленькая (small) (модель по умолчанию), средняя (medium), компактная (compact), большая (large) и огромная (huge). Программа, написанная на Турбо Си для машин семейства IBM PC в расчете на жонкретную модель памяти, может оказаться немобильной. Все синтаксические конструкции Турбо Сн, поддерживающие шесть перечисленных моделей памяти, являются расширением стандарта Си. Прежде чем продолжить материал, связанный с указателями, рассмотрим бегло все шесть моделей памяти. Предполагается, что читатель имеет доступ к дополнительной информации или представляет себе архитектуру процессора 8086. Наиболее ясное описание внутренней структуры микропроцессора 8086 приведено в книге «Programmer's Guide to the IBM PC» (P.Norton, 1986). Память для программы на (Турбо) Си требуется для четырех целей: для размещения ее программного кода, для размещения данных, для динамического использования и для резервирования компилятором на время выполнения программы. Концептуальная модель этих областей приведена на рис. 6.1.
Указатели и массивы 137 Старшие адреса памяти Младшие адреса памяти Буфера видеопамять ПЗУ пользуемая о. п. Стек —> Свободная память Куча < — Статические данные Код программы Векторы прерываний DOS Рис. 6.1. Распределение памяти в Турбо Си. Область памяти под код программы в процессе работы остается неизменной. Неизменной остается и память, отводимая под статические данные. Объем памяти для кучи зависит от того, сколько памяти запрашивает программист с помощью функций alloc н malloc (эти функции подробно рассматриваются в настоящей главе ниже). Размер использованной памяти стека изменяется при активизации автоматических (локальных) переменных в функциях, а также за счет того, что при вызовах функций в стек заносятся Параметры функций. Подробнее о передаче параметров будет сказано в гл. 8. Модели памяти, поддерживаемые в Турбо Сн Крошечная: Во все четыре регистра сегментов (CS, DS, SS, ES) засылается одни и тот же адрес. Под код программы, статические данные, динамически размещаемые данные и стек отводится 64К памяти. Такая модель налагает на задачу серьезные ограничения и используется только в тех случаях, когда особенно ощущается дефицит памяти. Переменные типа указатель в такой модели памяти занимают только два байта (близкие указатели). Следовательно, переменные типа указатель содержат смещение внутри фиксированного сегмента памяти. Маленькая: Под код программы отводится сегмент размером 64К- Стек, куча и статические данные размещаются в одном сегменте размером 64К- Такая модель памяти принимается по умолчанию и вполне подходит для многих маленьких и средних задач. Переменные типа указатель в такой модели
138 Глава 6 памяти занимают только два' байта (близкие указатели). Следовательно, переменные типа указатель содержат смещение внутри фиксированного сегмента памяти. Средняя: Размер памяти под код программы ограничен 1 Мбайтом. Это означает, что в коде программы используются далекие указатели. Стек, куча и статические данные, как и в случае маленькой модели памяти, размещаются вместе в сегменте памяти размером 64К. Такую модель памяти рекомендуется применять прн программировании очень больших программ, не использующих большого объема данных. Для адресации (указания) в коде программы служат далекие указатели (сегмент и смещение), занимающие четыре байта. Таким образом, все вызовы функций выполняются как далекие вызовы, и все возвраты из функций считаются далекими. Для адресации данных используются близкие указатели, занимающие два байта. Компактнаи: Под код программы отводится 64К- Под данные отводится 1 Мбайт. Объем статических данных ограничивается 64К, а размер стека, как и для всех моделей, не может превысить 64К. Такая модель памяти должна применяться при создании малых и средних по размеру программ, требующих большого объема статических данных. Адресация внутри программы выполняется с помощью близких указателей (их размер - два байта). Для адресации данных используются четырехбайтовые далекие указатели. Большая: Размер памяти под код программы ограничен 1 Мбайтом. Под статические данные отводится 64К, Куча может занимать до 1 Мбайта памяти. Такую модель приходится использовать во многих больших задачах. Как программа, так и данные адресуются далекими указателями, занимающими четыре
Указатели и массивы 139 байта В большой модели памяти ни одна отдельная единица данных ие может превышать 64К. Огромная: Аналогична большой модели, но суммарный объем статических данных может превышать 64К- Огромная модель памяти ие предусматривает огромных указателей. В гл. 12 рассматриваются некоторые расширения Турбо Сн и вопросы использования нестандартных описаний близких и дальних указателей. В общем случае переменная типа указатель описывается так: тип *переменная_указатель Такая запись означает, что переменная_указатель является указателем на тип. Двумя наиболее важными операциями, связанными с указателями, являются операция обращения по адресу * (иногда называемая операцией снятия ссылки) н операция определения адреса &. Операция обращения по адресу * служит для присваивания или считывания значения переменной, размещенной по адресу переменная_указатель, прн помощи лево-опреде- леиного выражения *переменная_указатель. Например, *ptr_var.= value; Операция определения адреса & возвращает адрес памяти своего операнда. Операндом должна быть переменная. Операция определения адреса выполняется следующим образом: адрес = &переменная; где адрес - это соответствующее левоопределенное выражение, куда помещается адрес, а переменная - нмя переменной, определенной выше в программе. В Турбо Си размер возвращаемого адреса зависит от применяемой модели памяти.
148 Глам 6 Всем указателям в (Турбо) Си можно присвоить безопасный адрес памяти - нуль целого типа. Гарантируется, что этот адрес не совпадет ни с одним адресом, уже использованным в системе. Такой адрес, нередко называемый нулевым адресом, часто применяют как ограничитель в динамических структурах. Подробно динамические структуры будут рассмотрены в следующей главе. Рассмотрим программу, приведенную на листинге 6.1. При ее компиляции использовалась маленькая модель памяти, где указатели занимают два байта памяти. В программе содержится серьезная ошибка. Мы специально включили серьезную ошибку в самую первую программу, в которой применяются указатели, потому что такая ошибка является распространенной и потенциально опасной для программы и на нее следует обратить внимание незамедлительно. Указатели х и w заданы так, что они указывают на тип int. Переменные у и г описаны как int. Предложение *х = 16 означает, что по адресу, задаваемому в х, помещается значение 16. Здесь операция * используется как операция обращения по адресу (снятия ссылки). Переменной у присваивается значение -15. Указателю w присваивается значение адреса переменной у. И это предложение правильно. Указатель w указывает на тип int. Иначе говоря, w содержит адрес переменной целого типа. По этому адресу уже записано значение -15. Таким образом, значение указателя со снятой ссылкой *w равно -15. Программа сначала напечатает размер указателя х Поскольку используется маленькая модель памяти, то размер равен двум байтам. Далее выводится значение смещения внутри сегмента памяти 64К как целое без знака После этого выводится целое значение 16, помещенное по этому адресу памяти. Потом выводятся адреса переменных у и z. И наконец, печатается значение указателя со снятой ссылкой *w, рааное -15. Где же ошибка? Рассмотренная программа может серьезно повредить работе компьютера, так что может потребоваться его холодная перезагрузка (наиболее серьезный тип системного сбоя!). Почему? Ошибка, воспроизведенная на листинге 6.1, одна из наиболее частых. По отдельности каждая строка программы правильна. Проблема заключается в неинициализированном
Указатели и массивы 141 указателе. Описание int «x; приводит к тому, что компилятор резервирует память, необходимую для хранения адреса (два байта для маленькой модели). Компилятор ие отводит двух байтов для размещения целого числа по этому адресу. Если программист желает по такому адресу поместить целое значение, то ои должен сам позаботиться о выделении требуемой для этого памяти. Начальное значение адреса х может не соответствовать допустимому адресу или адресу, по которому желательно разместить целое число. Более того, начальный адрес, содержащийся в х, может, к несчастью, совпасть с адресами таблицы описаний файлов или другой важной частью операционной системы DOS. Попытка записать целое по такому адресу может привести к тому, что будет затерта часть операционной системы. К сожалению, компилятор с Турбо Си нлн любой другой компилятор с Си не смогут «отловить» ошибку такого рода. Ответственность за инициализацию указателей полностью лежит на самом программисте. Исправить ситуацию, представленную на листинге 6.1, можно, использовав одну из функций распределения памяти - malloc, с помощью которой можно запросить намять из кучи, для целого - два байта. Оператор, выполняющий указанные действия: х = ( int * ) malloc( slzeof( int ) ); должен быть подставлен в программу на листинге 6.1 непосредственно перед оператором: *х = 16; В программу также следует подставить с помощью оператора include файл alloc.h, содержащий описание функций динамического распределения памяти. В качестве аргумента функции динамического распределения памяти malloc задается число байтов, которые следует зарезервировать в куче. Функция всегда возвращает указатель на тип void, поэтому такой указатель
142 Глма 6 совместим с любым указателем, который может встретиться в левой части оператора присваивания. Поскольку в нашем случае мы хотим, чтобы функция malloc выдала нам адрес целого (вернула в качестве результата указатель на целое), воспользуемся преобразованием ( int « ) для перевода значения, выдаваемого функцией malloc, в адрес, действительно указывающий яа целое. Использование функция malloc преследует две цели. Во-первых, переменной-указателю х присваивается значение адреса, которое гарантированно не совпадает с адресами, используемыми системой. Во-вторых, в памяти, отведенной дли кучи, выделяются два байта, в которых можно разместить целое, и полученный адрес заносится в х. Если в памяти под кучу иет свободного места для размещения двух байтов, то в качестве результата функции malloc будет выдай адрес 0. Во многих ситуациях пользователь должен проверять, не выдала ли функция в качестве результата 0. Поэтому удобно переписать оператор malloc в следующей форме: if ( ( х = ( int • ) malloc( sizeof( int > ) > != 0 ) /• фрагмент программы •/ > Большинство программ, приведенных в настоящей книге, невелекя по размеру, и для того, чтобы сделать код как можно более простым, а большинстве листингов, где используется функция malloc, отсутствует какая бы то ни было проверка выхода за пределы кучи. Рис. 6.2 иллюстрирует ситуацию, возникающую при использовании операции malloc. ч х - адрес памяти 16 Рис. в. 2. Адрес памяти.
Указатели и массивы 143 Листинг 6.1. Переменные-указатели и адреса памяти: пример серьезной ошибки «include <stdio.h> aain() { int »x; int **; int y; int z; •x = 16; У = -15; w = &y; printf( "ЧпРазмер x = %d". sizeof( x ) ); printft "ЧпЗяачение указателя х = %u", x ); printf( "ЧпЗяачение по такому адресу^ = Xd", »x ); printf( "ЧпАдрес у = %u", &y ); printf( "ЧпАдрес z = %u", &z ); printf( "ЧпЗяачение »w = %d4n", »w ); > После включения оператора malloc в программу яа листинге 6.1 вывод программы станет следующим: Размер х = 2 Значение указателя х = 1486 Значение по такому адресу = 16 Адрес у = 65224 Адрес z = 65226 Значение »w = -15 Значение указателя х равно 1486 - это относительный адрес указателя х. Адреса переменных у н г отличаются на два байта. Конкретные числовые значения указателей могут отличаться для разных реализаций компилятора и зависят от других факторов, определяемых выбором конкретного компьютера.
144 Глава 6 6.2. Проблемы, связанные с указателями, и- их разрешение Проблема, связанная с ошибками инициализации переменных-указателей, рассмотрена в разд. 6.1 и проиллюстрирована программой на листинге 6.1. Мы и дальше уделим виимаиие проблеме инициализации, поскольку ошибки такого рода являются обычными для новичков, программирующих на Си, и приводят к серьезным последствиям. Прочитав раздел 6.1, читатель должен усвоить, что описание переменной-указателя в теле такой функции, как, например, main, не приводит ни к инициализации указателя осмысленным значением, ии к резервированию в куче памяти для значения. Существует четыре способа задать переменной-указателю осмысленное начальное значение: • Описать указатель вне любой функции или снабдить его предписанием static. Начальным значением является нулевой адрес памяти - 0. Перед тем как начать пользоваться указателем, следует зарезервировать память под значение. • Присвоить указателю адрес переменной. • Присвоить указателю значение другого указателя, к этому моменту уже правильно инициализированного. • Использовать функции распределения памяти, такие, как alloc я malloc. Первый способ основывается иа важном свойстве статических переменных. Все статические переменные (переменные, описанные в теле функции и снабженные предписанием static, или переменные, описанные вне функции) инициализируются значением 0. Как отмечалось в разд 6.1, адрес памяти 0 - это специальный адрес, часто используемый как признак конца динамических структур. Поскольку компилятор отводит память под переменную в момент ее описания, то присваивание указателю адреса переменной гарантирует, что нужная память отведена После снятия ссылки значение указателя совпадает со значением переменной, адрес которой был присвоен указателю.
Указатели и массивы 145 Когда указателю присваивается значение другого указателя, то это означает, что одному и тому же адресу памяти присваиваются разные имена (идентификаторы обеих переменных-указателей). Если значение, на которое ссылаются два указателя, будет изменено в операторе присваивания с использованием одного из указателей, то значение, получаемое с помощью другого указателя, также будет изменено. Рассмотренная ситуация называется двойным указанием и может привести к серьезным проблемам. Рассмотрим программу иа листинге 6.2. Три указателя х, у и z инициализируются в месте их описания. Поскольку унарные операции являются правоассоциативными, адрес памяти, возвращаемый функцией malloc, ассоциируется с адресом х, а не со снятой ссылкой по адресу х. Иначе говоря, описание int *х = ( lnt * ) malloc( sizeof( int ) ); означает: «величина х описана как указатель иа целое, начальное значение которого равно адресу, возвращаемому функцией malloc». Результатом работы программы на листинге 6.2 будет х = 14 у = 15 z = 17 х = 14 у = 15 z = 14 х = 14 у = 14 z = 14 х = -15 у = -15 z = -17 Серьезная ошибка двойного указания заключена в операторе у = х; После выполнения такого оператора у начинает самостоятельное существование. Действительно, хну являются двумя различными именами одной и той же области памяти. Когда по адресу, содержащемуся в у, заносится значение -15 (для этого используется косвенный оператор присваивания *у = -15), значение, иа которое ссылается х, также будет равно -15. Вот почему в последней строке вывода будет напечатано, что х=-15 и у=-15.
146 Глава 6 Еще одна неприятность состоит в том, что два байта памяти, которые были в самом начале отведены для переменной-указателя, оказываются уже недоступными. Значение 15, присвоенное ранее по ставшему теперь недоступным адресу памяти, навсегда теряется. Пря этом размер кучи уменьшается иа два байта В приведенном случае двойственные ссылки привели к завешиванию (потере) памяти. Программист должен быть весьма осторожен, чтобы избежать потеря памяти. На рис. 6.3 показано состояние памяти в системе перед каждым из первых трех операторов print value (ne- чатьзиачений). 4х, , Чу, , Ч2 1 4f ч[ 14 | «1 „| чу, , ч2 15 и ^ г—л ч2г—I | 15 I | 14 I Рис. 6.3. Двойственные ссылки. Листинг 6.2. Иллюстрация двойственных ссылок «include <stdio.h> «include <alloc.h> main!) К extern void print_values< int one, int two, int three ); int »x = < int • ) malloc( sizeof( int ) ), •y = ( int • ) ша11ос( sizeofl int ) ), *z = ( int * ) malloc( sizeofl int ) ), •x = 14; •y = 15;
Указатели и массивы 147 •z = 17; print_values( »x, "у, *г ) •z = »х; print_values( »x, "у, *г ) у = х; /* Серьезная ошибка */ print„values( *х, "у, *г ) •х = -14 •у = -15 •z = -17 print_values( »х, "у, »z ); printf( "\n" ); > void print_values( int one, int two, int three ) < printf( "\n\nx = %d у = %d z = %d". one, two, three ); > Указанные ошибки, связанные с неинициализированными указателими и двойственными ссылками, не исчерпывают весь перечень ошибок, связанных с переменными-указателями. Серьезная опасность возникает, если функция возвращает указатель, являющийся адресом автоматической (локальной) переменной. Автоматическая переменная описывается внутри функции. Память под нее отводится в момент активизации (вызова) функции. При выходе из функции память для всех автоматических переменных освобождается. Поэтому возвращенный адрес может быть позже использован системой, и информация, содержащаяся по этому адресу, может оказаться замененной новой информацией. Выход из описанного положения - никогда не возвращать адреса автоматических переменных. Еще один источник ошибок - неосвобождение памяти, запрошенной ранее с помощью функций alloc яли malloc, когда указатель уже больше ие нужен. Система неспособна автоматически освобождать память в куче. Возврат (освобождение) памяти в куче выполняет функции free. В качестве аргумента функции free задается указатель, ссылающийся иа освобождаемую память. Использование функции free показано в следующей главе. И еще одна ошибка - присваивание переменной-указателю адресного значения непосредственно.
148 Глава 6 Можно ли выполнить следующее действие? int »x = 12345; К сожалению, ответом будет «да», и компилятор не запретит нам использовать такой оператор. Компилятор Турбо Си предупреждает пользователя о том, что оператор нарушает мобильность программы. В некотором смысле это оператор низкого уровня. Возможно, что данный олератор не окажет никакого действия даже в Турбо Си. Его интерпретация зависит от того, какая модель памяти используется. В нестандартную библиотеку Турбо Си (интерфейс с ее программами описай в dos.h) включены две важные функции изменения и чтения значений по произвольным адресам памяти - poke и peek. Способ, которым компилятор с Си отображает адреса памяти в целые числа, зависит От реализации. Даже в системе Турбо Си это отображение зависит от используемой модели памяти. Если с системой ие поставляется библиотека, содержащая функции, аналогичные poke и peek, то программист должен написать их самостоятельно, ориентируясь на конкретную реализацию Си. 6.3. Массивы Массивы описываются следующим образом: тип имя_массива [ раэмер_массива 1; Компилятор отводит под массив память размером (sizeof(THn) * размермассива) байтов. Все массивы индексируются, начиная с нуля. Например, если массив data описай как float data [ 245 ]; то 245 элементов типа float будут: data[0], data[l], ..., data[244]. Имя массива фактически является константой-указателем, ссылающейся на начальный адрес данных, которые обычно занимают множество последовательных ячеек памяти. Важно заметить, что речь идет именно о константе.
Указатели и массивы 149 Начальный адрес массива определяется компилятором в момент описания массива, и такой адрес никогда ие может быть переопределен. Таким образом, нельзя написать float data! 245 1; float »г = ( float * ) malloct sizeoft float ) ); *r = 1.234; data = r; /* Оператор содержит ошибку •/ Компилятор Турбо Си предупредит вас о том, что в операторе data=r требуется леводопустнмое значение. Такое сообщение выдается из-за того, что значение адреса data не может быть заменено на другое значение адреса г. Как можно присвоить значения одного массива другому? В таких языках, как Паскаль или Модула-2, если два массива first и second описаны как VAR first, second : ARRAY! 0 .. 9 1 OF REAL; то сложный оператор присваивания second := first; перепишет все значения из массива first в массив second. Каждый из массивов располагается в своей памяти. Попытаемся выполнить то же самое иа Турбо Си: int first! 10 J, second! 10 1; second = first; Такое действие, конечно, неверно, поскольку указатель на массив second - константа. Как же тогда выполнять сложные присваивания? Непосредственно сделать это в Турбо Си нельзя. Рассмотрим следующий код, имитирующий сложное присваивание: int *first = ( int * ) aallocl 100 * sizeofl int ) ), •second = ( int • ) malloct 100 * sizeoft int ) ); second = first;
150 Глава 6 Приведенный оператор присваивания вереи и присваивает адрес, содержащийся в first, переменной second. Два массива неразличимы в памяти. Изменение значений в одном из массивов приводит к изменениям и в другом. Присваивать переменной second значение адреса first опасно, поскольку адрес second оказывается уже недоступным. Было бы правильно, прежде чем выполнять присваивание, написать free( second ); Для того чтобы получились два различных массива, значения из массива first могут быть переданы в массив second следующим образом: int i; int «first = ( int * ) mallocl 100 * sizeofl int ) ), •second = ( int * ) malloc( 100 * sizeofl int ) ); for ( i = 0; i < 100; i++ ) second! i 1 = first! i 1; Рассмотрим описание char *str; На первый взгляд такое описание задает переменную-указатель иа тип char. Действительно, такое описание означает именно это. Однако, к счастью, такое описание представляет массив с начальным адресом str и пока неопределенным числом элементов. Указанное описание массива, как и любое описание указателя, страдает от возможной ошибки неинициализированного указателя. Как уже говорилось в разд. 6.2, указатель str можно инициализировать, описав его статично, присвоив str адрес другой статической переменной, присвоив str значение другого указателя, уже корректно инициализированного, или, как это чаще всего делают для указателей на массивы, получив адрес памяти с помощью функции malloc или alloc. Сравним два массива first и second, описанные ранее.
Указатели и массивы 151 int "first = ( int • ) malloc( 200 • sizeof( int ) ); int second! 200 1; Оба массива занимают по 400 байтов памяти (200*sizeof(int)). Поскольку при создании массива first используется функция динамического распределения памяти malloc, то он будет размещен в куче. Если оба описания помещены в теле функции main, то массив second размещается в стеке наряду с остальными локальными для main данными. Основным различием между массивами будет то, что second представляет собой константный указатель, а first - таким не является. Оператор присваивания first = second; является ошибочным, но оператор присваивания second = first; . является верным. Читателя может удивить, что доступ к данным из массивов first и second выполняется одинаково. Как мы уже знаем, для того чтобы добраться до четвертого значения в массиве second, нужно указать second[ 3 ]. А как же получить доступ к четвертому элементу массива first? Да точно так же, указав first[ 3 ]. Тот факт, что массив описывается с помощью переменной-указателя (например, массив first), ие изменяет способа доступа к данным в массиве на считывание или запись. Рассмотрим теперь программу на листинге 6.3. В результате своей работы программа выведет Адрес data = 64252 Адрес data 10 1= 64252 Адрес data I 1 1 = 64256 Замечание для читателей Значения, полученные на вашем компьютере, могут отличаться от приведенных. Значение указателя data (имени массива) то же самое, что н адрес элемента &data[ 0 ]. Заметим, что ад-
152 Глава 6 pec второго элемента массива &data[ 1 ] отстоит от адреса первого элемента на четыре байта. Листинг 6.3. Массивы и указатели /* Программа, иллюстрирующая связь между именами массивов и указателями */ ttinclude <stdio.h> main() { float data! 245 1; printff "ЧпАдрес data = %u\ data ); printff "ЧпАдрес data [01= %u", &data[ 0 ] ); printff "ЧпАдрес data [11= %u\n", &datal 11); } 6.4. Арифметические действия над указателями Над указателями могут быть выполнены действия целочисленной арифметики. Рассмотрим следующее описание: int data! 100 1; Адрес еотого элемента массива можно получить при помощи выражения &data[ 99 ]. Но этот же адрес можно записать и как data +99 Компилятор Турбо Си переведет оператор data[ 24 ] в оператор *(data + 24). Он также переведет оператор data! 9 ] = -66; в оператор ♦(data + 9) = -66; Сложение или вычитание указателей всегда выполняется в единицах того типа, к которому относится указатель. Адрес data отличается от адреса data+1 на число байтов памяти, требуемых для хранения одного целого (в нашем случае - на два байта).
Уна^лтели и массивы 153 Разрешено вычитать из указателя указатель. Разность определяет число элементов массива между двумя указателями. Указатели можно сравнивать между собой. Список допустимых действий над указателями приведен в табл. 6.1. Таблица 6.1. Операции над указателями Операция Пояснение ptrl == ptr2 Сравнение на равенство ptrl != ptr2 Сравнение на неравенство ptrl < ptr2 Сравнение на меньше и ptrl <= ptr2 меньше или равно ptrl > ptr2 Сравнение на больше и ptrl >= ptr2 больше или равно ptr2 - ptr2 Вычисление числа элементов между ptr2 и ptrl ptrl+int_val Вычисление указателя, отстоящего от ptrl вверх ptrl-int_val или вниз на int_val элементов Примеры выполнения арифметических действий над указателями приведены на листинге 6.4, где представлена программа, обеспечивающая просмотр всех элементов массива и печать их значений. Вывод результатов программы выглядит следующим образом: 12 42 72 102 132 162 192 222 252 282 312 342 372 402 15 45 75 105 135 165 195 225 255 285 315 345 375 405 18 48 78 108 138 168 198 228 258 288 318 348 378 408 21 S1 81 111 141 171 201 231 261 291 321 351 381 411 24 54 84 114 144 174 204 234 264 294 324 354 384 414 27 57 87 117 147 177 207 237 267 297 327 357 387 417 30 60 90 120 150 180 210 240 270 300 330 360 390 420 33 63 93 123 153 183 213 243 273 303 333 363 393 423 36 66 96 126 156 186 216 246 276 306 336 366 396 426 39 69 99 129 159 189 219 249 279 309 339 369 399 429
154 Глава 6 432 462 492 522 552 582 435 465 495 525 555 585 438 468 498 528 558 588 441 471 501 531 561 591 444 474 504 534 564 594 447 477 507 537 567 597 450 480 510 540 570 600 453 483 513 543 573 60Э 456 486 516 546 576 606 459 489 519 549 579 Цикл, в котором собственно и выполняется печать значений, записывается так: while ( data != end_ptr ) printff "'/.8.Of", *data++ ); Логическое выражение data != end_ptr становится ложным, когда значение data, наконец, будет увеличено до значения, совпадающего с end_ptr, т.е. являющегося адресом последнего элемента массива. Довольно странное выражение *data++ является одной из причин того, почему некоторые программисты ие любят пользоваться языком программирования Си. Такое выражение - идиома. Кому-то оио покажется запутанным, но на самом деле оно вполне понятное. Как отмечалось в гл. 4, унарная операция ++ является правоассоциативной. Операция обращения по адресу * выдает значение по адресу data. Далее адрес data ассоциируется с операцией ++ и увеличивается на 1 (иа самом деле - на sizeof(float) байтов). Теперь понятно? .Ну конечно же! Представьте, что цикл, в котором выполняется печать, был запрограммирован несколько по-другому: while ( data++ != end_ptr ) printft ",/C8.0f,,( «data ); Здесь . возникнет уже знакомая иам по гл. 5 ошибка недобора единицы. Первое значение, которое напечатает программа, будет 15. Правильное первое значение 12 будет потеряно при выводе, поскольку адрес data увеличится на единицу до того, как будет напечатано соответствующее значение *data, а ие после этого. Советуем быть внимательными к ошибкам такого рода, возникающим при использовании указателей и арифметических действий над указателями.
Указатели и массивы 155 Листинг 6.4. Пример использования арифметических действий над указателями ^include <stdio.h> ttinclude <alloc.h> main() < «define size 200 float «data = ( float » ) mallocl size * sizeoff float ) ); float «endjptr = data + size - 1; int i; for ( i = 0; i < size; i++ ) data! i 1 = 3 ♦ i + 12; /* Для доступа к элементам массива используются арифметические операции над указателями */ while ( data != endjptr ) printff "%8.0f\ *data++ ); printff "\n" ); ) До того как' вы почувствуете себя слишком уж уверенно от того, что вы поняли цикл while на листинге 6.4, разберите короткую программу на листинге 6.5. Попытаемся понять вы ражей кя ++*data и *++data. Если они для вас не ясны, давайте подробно разберем предложенные конструкции. Первое выражение ++*data увеличивает значение *data на единицу до его использования, и в результате получается 1. Во втором выражении вначале адрес *++data увеличивается на единицу, а затем выбирается значение по этому адресу - оно равно 10. Таким образом, вывод программы с листинга 6.5 будет ++*data = 1 *++data = 10 Листинг 6.5. Еще одни пример использования указателей iinclude <stdio.h> ♦include <alloc.h>
156 Глава 6 main() < int «data = ( int ♦ ) mallocf 20 * sizeoff Int ) ); int i; for ( i = 0; i < 20; i++ ) data! i 1 = 10 ♦ i; printff "\n ++*data = Y.d", ++*data ); printff "\n »++data = %d\n", *++data" ); ) Предупреждение Наиболее серьезные ошибки, связанные с использованием массивов, - выход за границу массива. Ошибки возникают, когда в программе делается попытка записать или прочитать значение по адресу памяти, который не был указан в программе. Следствиями ошибок выхода за границу массива могут быть и неверные результаты, и общий сбой системы. Если вы забыли отвести память для массива, описанного с помощью указателя на какой-либо тип, то все попытки записать или прочитать данные из массива приведут к ошибке выхода за границу массива. Пример ошибки выхода за границу массива приведен на листинге 6.6. Вы видите ошибку? В цикле FOR значение индекса i инициализируется значением 0, и далее выполняются итерации по i до тех лор, пока i не станет меньше, чем size. К сожалению, с использованием функции malloc была отведена память только точно под количество size целых значений. В цикле делается попытка присвоить значение 100*size неопределенному элементу data[size]. Компилятор не сможет диагностировать ошибку такого рода. Операционная система тоже ие выивит ошибку. Ошибка может не проявиться и во времи работы программы. Таким образом, перед нами худшая из возможных ошибок программирования - ошибка, которая может остаться незамеченной. При использовании языка Си ответственность за проверку выхода за границу массива полностью лежит на программисте.
Указатели и массивы 157 Листинг 6.6. Программа, содержащая ошибку выхода за границу массива iinclude <stdio.h> ^include <alloc.h> main() < int *data; int size; int i; printff "Укажите размер массива: "); scanf( "%d", &size ); data = ( int * ) mallocf size * sizeoff int ) ); for ( i = 0; i <= size; i++ ) data! i ] = 100 • i; ) В качестве первого содержательного примера использования массивов и указателей решим следующую задачу. Нужно подсчитать и напечатать частоту появления каждой буквы в текстовом файле. Конкретный файл задается пользователем с использованием переопределения ввода. На листинге 6.7 приведены две реализации решения поставленной задачи. В первой реализации описывается статический массив из 256 длинных целых. Ои инициализируется 256 нулями, поскольку описан как static. В цикле WHILE while ( ch = getcharf), ch != EOF ) freql ch 1++; из входного потока вводятся Символы, проверяется совпадение ch с символом EOF (-1), и если совпадения нет, то изменяется значение массива freq, причем в качестве индекса массива используется ch. Во второй реализации используется следующий цикл WHILE; while ( ch = getcharO, ch != EOF ) ( *( freq + ch ) )++;
158 Глеев 6 В этом цикле изменения в массиве freq выполняются с использованием адресной арифметики. Листинг 6.7. Вычисление частоты появления символов в файле: две реализации /* Программа вывода частот появления символов в текстовом файле. Файл freq. с Вызов программы: freq < файл ♦/ «include <stdio.h> long freqt 256 1; Bain() < int i, ch; while ( ch = getcharO. ch != EOF ) freql ch 1 ++; for ( i = 0; i < 256; i++ ) printf( "\nfreq[ Y.d 1 = Xd\n", i, freqt i 1 ); /* Программа вывода частот появления символов в текстовом файле. Файл freq.с Вызов программы: freq < файл •/ «include <stdio.h> long freql 256 1; maint) { int i, ch; while ( ch = getcharO. ch != EOF > ( *( freq + ch ) )++; for ( i = 0; i < 256; i++ ) printf( "\nfreq[ %d 1 = %d\n\ i, freqt i I I; ) На листинге 6.8 представлен пример еще одной задачи, использующей некоторые конструкции и концепции, связанные с указателями. Программа предлагает пользова-
Указатели и массивы 159 телю ввести несколько вещественны! чисел, соответствующих ставкам. Программа выведет среднюю ставку, все значения ставок, равные или превышающие 75 проиентилей, а в завершение - 10, 20, ... 100- процеитильиые ставжн. Алгоритм, используемый для сбора указанных статистических данных в заданном порядке, запрограммирован и функции select. Первым параметром функции select является адрес массива значений ставок, статистические характеристики которых мы желаем получить. Второй параметр задает размер массива ставок (число ставок). Третий параметр указывает на вид статистики, которую требуется получить (например, если указано 1, то выдать самое маленькое значение, если 2 - то второе меньшее значение, если 3 - третье меньшее значение, и т.д.). Передача массивов в качестве параметров функций (Турбо) Си будет рассмотрена в настоящей главе. Заметим, что у функций select, partition и swap в качестве первого Параметра стоит указатель на тип float. Во всех трех случаях в качестве входной информации для функции передается адрес массива, память для которого уже отведена и значения элементов которого уже заданы. Рассмотрим подробнее несколько фрагментов программы иа листинге 6.8, наиболее ярко иллюстрирующих материал настоящего раздела. Прежде всего проанализируем цикл FOR, используемый для ввода значений ставок. for ( index = 0; index < number_scores; index++ ) { printff "ЧпВведнте ставку %d:", index + 1 ); scanf( "Xf", erKLof_data ); sum += *end_of_data++; ) Указателю end_of_data присваивается начальный адрес массива, содержащего ставки, - data. Идиома *end_of_data++ уже ранее упоминалась и используется здесь для приращения переменной sum и перехода к новому значению в массиве data. Далее рассмотрим цикл FOR, в котором печатаются все ставки, превышающие или совпадающие с 75 - процен- тильным значением. В цикле FOR отсутствует инициализация:
160 Глава 6 for ( ; next_data < end_of_data; next_data++ ) if ( *next_data > percentile75 ) printff "\n\X.2f", *next_data ); Указателю next_data значение адреса массива data присваивается перед циклом FOR. Адрес указателя увеличивается с помощью цикла FOR. Условие окончания цикла включает сравнение адресов next_data и end_of_data. В функции partition массив data разбивается таким образом, что все числа, располагаемые ниже индекса pivot, меньше элемента pivot, а все числа, располагаемые выше индекса pivot, больше элемента pivot. Функция select многократно вызывает функцию partition до тех пор, пока статистика не совпадет со значением, возвращенным функцией partition. Подробное описание алгоритма разделения массива, реализованного в функции partition, дано в книге «Fundamentals of Computer Algorithms» (Horowitz, 1978). Листинг 6.8. Сбор статистики о массиве ставок /* Вводятся значения серии ставок. Программа выводит: (1) Среднюю ставку; (2) Все значения ставок, равные или большие 75 процентилям; (3) Ставки с шагом 10 процентилей. ♦/ ttinclude <stdio.h> ttinclude <util.h> ♦include <alloc.h> const long largest = 99999999; /♦Используется в select*/ float *data /* Набор данных неопределенной длины */ main() < extern float select( float *a, int n, int k );
Указатели и массивы 161 float *end_of_data; float sum = 0.0; int number_scores; Int Index; printff "Введите количество ставок: "); scanf( "7.d", &number_scores ); /«Отводится память под массив data •/ data = ( float * ) mallocf number_scores • sizeof( float ) ); end_of_data = data; /* Ввод значений ставок */ for ( index = 0; index < number_scores; index++ ) { printff "ЧпВведите ставку %d:", index + 1 ); scanf( "Xf", end_of_data ); sum += *end_of^.data++; ) printf( "\п\пСредняя ставка =X.2f. sum / number„scores ); /* Подсчет 75 процеитилей. Длн этого заводится блок. */ { int selectionjmmber; float percentile75; float *next_data = data; printff "\п\пСтавкн, большие или равные 75 процентилян"); printff "\n " ); selection_number = ( int ) ( 0.75 * number_scores ); percent!Ie75 = select( data, numberscores, selection_number ); for ( ; next_data < end_of_data; next_data++ ) if ( *next_data > percentile75 ) printff "\n\X.2f", *next_data ); print( "\n" ); /* Вычисление процеитилей от 10, 20, .... 100 */ 6 Зак. 795
162 Глава 6 /» Заводим еще один блок */ { int selection_number; float percentile; int i; printf( "\п\пСтавки по процентилям" ); printf( "\n " ); for ( i = 1; i <= 10; i++ ) Л selection_number = ( int ) ( 0.1 * i * number_jscores >; percentile = selecU data, number_jscores, selection_number ); printfl "\n%d процент: %.2f", i • 10, percentile ); } } } float select( float *a, int n, int k ) { extern int partition( float *a, int low, int high); int m = 0, r = n, j; a[ n 1 = largest; do { j = r; j = partition( a, m, j ); if ( k - 1 == j ) return al j 1; else if ( k < j + 1 ) r = j; else m = j + 1; ) while ( 1 ); /* Выполняется бесконечный цикл */ ) int partition( float *a, int low, int high )
Указатели и масоны 163 } extern void swap( float «a, int low, int high ); /* Случайным образом выбираем индекс из диапазона от low до high-1 */ int index = random( low, high - 1 ); float pivot = at index 1; int i = low; a[ index 1 = a [ low ]; a[ low I = pivot; /• После завершения этого цикла все значения слева от элемента pivot будут меньше значения элемента pivot, а все значения справа от элемента pivot будут больше, чем значение элемента pivot. •/ do < do i++ while ( at i 1 < pivot ); do high —; while ( at high 1 > pivot ); if ( i < high ) swap( a, i, high ); else break; > while ( 1 ); at low 1 = at high ]; a( high 1 = pivot; return high; void swap( float *a, int low, int high ) < float temp = at low 1; at low 1 = at high 1, at high 1 = temp; } На листинге 6.9 приведена программа сравнения сверхбыстрого алгоритма сортировки quicksort, предложенного Хоаром, с менее эффективным, ио более популярным алгоритмом сортировки методом пузырька — bubblesort.
164 Глава 6 Читатель должен представлять, что алгоритм сортировки методом пузырька чрезвычайно неэффективен. Например, если размер большого массива, сортируемого по методу пузырька, удваивается, объем вычислений учетверяется. В обоих алгоритмах сортировки используются указатели вместе с некоторыми конструкциями и приемами, рассмотренными в настоящем разделе. Читателям, интересующимся подробностями, связанными с реализацией алгоритма quicksort, рекомендуем обратиться к кииге «Fundamentals of Computer Algorithms> (Horowitz, 1978). Мы сиова встречаем функцию partition, введенную намн на листинге 6.8, но иа листинге 6.9 она приведена в другой форме. Оба параметра этой функции на листинге 6.9 являются указателями на тип float (адреса памяти элементов в сортируемом массиве). Функция возвращает адрес памяти, а ие зиачеиие с плавающей точкой. Советуем читателю тщательно проанализировать обе реализации функции partition и убедиться, что обе реализации выполняют одну и ту же работу по разделению массива. В реализации на листинге 6.9 элемент pivot определяется следующим образом: float pivot = *( low_ptr + ( high_ptr - low_ptr) / 2 ); Средний элемент массива выбирается с использованием арифметических операций над указателями. После того как завершится цикл WHILE, зиачеиии элементов массива, лежащих левее элемента pivot, будут меньше, чем зиачеиие самого элемента pivot, а значения всех элементов массива правее pivot будут больше, чем значение элемента pivot. Функция quick_sort использует рекурсию и метод решения, получивший название «разделяй и властвуй». О рекурсии речь будет идти в гл. 8. Предлагаем читателю сравнить функции bubblesort, приведенные иа листингах 6.9 и 2.12.
Указатели и массивы 165 Листинг в. 9. Использование арифметических действий над указателями для реализации алгоритмов быстрой сортировки и сортировкя методом пузырька. /* Quicksort - алгоритм разработан С.А.Р. Хоаром */ «include <stdio.h> «include <util.h> «include <alloc.h> «define size 6400 float *data; main() { extern void load_array_sort ( int s ); data = ( float * ) malloc( size * sizeof( float ) ); load_array_sort( 400 ); ldad_array_sort( 800 ); load_srray_sort( 1600 ) load_array_sort( 3200 ) load_array_sort( 6400 ) printf( "\n" ); > void load_array_sort( int s ) { extern void quicksort( float *low_ptr, float •high_ptr ); extern void bubble_sort ( int n ); int i; printf( "Сортировка Quicksort для 'Ad чисел", s ); for ( i = 0; i < s; i++ ) data! i I = rand_real(); rpttiming( begin ); quick_sort( data, data + s - 1 ); rpttiming( end ); if ( s <= 800 ) <
166 Глава 6 for ( i = 0; i < s; i++ ) data! i J = rand_real(); printf( "Сортировка Bubblesort для Xd чисел", s ); rpttimingl begin ); bubblejsorU s )i rpttimingf end ); > } void quicksort( float *low_ptr, float *high_ptr ) < float *pivot_ptr; extern float *partition( float *low_ptr, float *high_ptr ); if ( low_ptr < high_ptr ) { pivot_ptr = partitiont low_ptr, high_ptr ); quicksort ( low_ptr, pivot_ptr - 1 ); quicksort ( pivot_ptr, high_ptr ); } float *partition( float *low_ptr, float *high_ptr ) { extern void swap( float *ptr_l, float *ptr_2 ); float pivot = *( low_ptr + ( high_ptr - low_ptr ) / 2>; /* После завершения этого цикла все значения слева от элемента pivot будут меньше значения элемента pivot, а все значения справа от элемента pivot будут больше, чем значение элемента pivot. •/ while ( low_ptr <= high_ptr ) { /* Поиск значения, большего чем pivot, в нижней части массива */ while ( *low_ptr < pivot ) low_ptr++; /* Поиск значения, меньшего чем pivot, в верхней части массива */ while ( *high_ptr > pivot )
Указатели и массивы 167 high_ptr—; if ( low_ptr <= high_ptr ) ч swap( low_ptr++, high_ptr—); } return low_ptr; } void swapt float *ptr_l, float *ptr_2 ) { float temp = *ptr_l; *ptr_l = »ptr_2, »ptr_2 = temp; } void bubble_j3ort( int n ) { int i, j; for ( i = 0; i < n - 1; i++ ) for ( j = n - 1; J > i; j— ) { if ( data! j ] < data! j - 1 ] ) swap( data + J, data + j - 1 ); ) } Программа на листинге 6.9 выведет: Сортировка Quicksort для 400 чисел 0 часов, 0 минут, 0 секунд, 16 сотых Сортировка Bubblesort для 400 чисел 0 часов, 0 минут, 2 секунд, 69 сотых Сортировка Quicksort для 800 чисел 0 часов, 0 минут, 0 секунд, 33 сотых Сортировка Bubblesort для 800 чисел 0 часов, 0 минут, 10 секунд, 75 сотых Сортировка Quicksort для 1600 чисел О часов, 0 минут, 0 секунд, 66 сотых Сортировка Quicksort для 3200 чисел 0 часов, 0 минут, 1 секунд, 54 сотых
168 Глава 6 Сортировка Quicksort для 6400 чисел D часов, 0 минут, 3 секунд, 30 сотых в. 5. Инициализация массивов Начальные значения элементам массива можно присвоить в месте описания массива следующим образом: тип имялассива! 1 = { знач1, знач2, ..., значИ >; Начальные значения элементов массива заключаются в фигурные скобки. Если пользователь ие указал в квадратных скобках размер массива, то компилятор сам задает размер массива по числу приведенных начальных значений в фигурных скобках. Посмотрите на листинг 6.10. Здесь описаны три массива: data, scores и prompt. При описании массива data явно указано, что он содержит пять целых. Таким образом, его размер - 10 байтов. Размерности массивов scores[] и prompt[] заданы неявно путем инициализации значений: в первом случае - пяти вещественных чисел, а во втором - восьми символов. Массив scores[] можно описать и как scores[5]. Соответственно вместо описания prompt[] можно подставить и prompt[8]. Если начальные значении не заданы, то все элементы указанных массивов будут нулевыми, поскольку все статические переменные инициализируются значением нуль. Листинг 6.10. Инициализации массивов #include <stdio.h> int data! 5 ] = { 5, 4, 3, 2, 1 >; float scores!] = { 3.4, 2.7, 1.8, 6.9, -24.3 >; char prompt!) = { '0', 'т', 'в', 'е', *т\ *:', ' \ *\0' >; main() { printft "ХпРазмер массива data = 54d", sizeoft data ) );
Указатели и массивы 169 printf( "ЧпРазмер массива scores = Xd", sizeoff scores ) ); printf( "ХпРаэмер иассива prompt = %d", sizeof( prompt ) ); printf\ "\n\nprompt = %s\n, prompt ) } 6.6. Строки Важным подклассом одномерных массивов являются массивы символов, или строки. Пожалуй, наиболее важным фактом, относящимся к строкам, является то, что коистанты строки заканчиваются нулевым символом '\0'. Строка-константа (строка-литерал) ограничивается двойными кавычками. Если вам необходимо включить сам символ двойной кавычки в строку, вы должны предварить его символом «обратная косая черта» Вот пример описания строки-констаиты: char prompt!! = "Не said, \"I love you.V "; Строка prompt инициализируется значением He said, "I love you." На листинге 6.11 показано, как можно на Турбо Си четырьмя способами сказать: "I love you". Первые два способа описания строк мы уже рассмотрели - messagel[ 50 ] и message2[]. Третий способ: char «messages = "I love you."; Отличается от рассмотренных способов инициализации массива символов. Такой способ возможен только дли массивов символов. Компилятор зарезервирует 12 байтов статически и в указатель зашлет адрес строки. В этом случае указатель иа строку message3 не будет коистаитой, несмотря на то что сама строка - констаита. Для всех трех строк имя строки - указатель на адрес первого символа строки. Неправильно было бы записать messagel = message2, а запись message3 = messagel будет правильной. На рис. 6.4 показано содержимое всех четырех
170 Глава 6 строк, представляющих текст "I love you.". Размер каждой из строк - 12 байтов, несмотря иа то что сами строки содержат только 11 символов. В коиец каждой строки компилятор подставляет признак конца строки-литерала, символ '\0'. I 1 О V е У о и \0 Рис. в.4. Строка символов. Листинг в. 11. Примеры описаний строк /* Программа, иллюстрирующая строковые константы */ #include <stdio.h> char messagelt 50 1 = "I love you."; char message2N = "I love you."; char *message3 = "I love you."; mainO { char message4 = "I love you."; printf( "\nmessagel = 54s", message 1 ); printf( "\nmessage2 = "/.s", message2 ); printf( "\nmessage3 = %s", message3 ); printf( "\nmessage4 = Xs\n", message4 ); } Каким образом можио присвоить одиой строке значение другой? Неправильно (дли массивов констант) или опасно (двойственные указатели) присваивать адрес одиой строки адресу другой. Перепись содержимого одной строки (str2) в другую строку (strl) может быть выполнена с помощью цикла i = 0; while( strlt i 1 i++; str2[ i 1 ) Цикл WHILE завершится, когда в строке str2 встретится символ конца строки '\0'.
Указатели и массивы 171 Более простой способ скопировать str2 в strl воспользоваться функцией strcpy из стандартной библиотеки string: strcpy< strl, str2 ); В стандартной библиотеке, интерфейсы с которой описаны в файле string.h, содержатся полезные для пользователя (Турбо) Си функции и макросы манипуляции со строками. Указанная библиотека мобильна для большинства реализаций Си. В табл. 6.2 даны описания интерфейсов функций из этой библиотеки. Таблица 6.2. Функции из стандартной библиотеки для работы со строками Char *strcat( char *dest, char «source ) Конкатенирует строки dest и source. char *strncat( char *dest, char *source, unsigned maxlen ) Присоединяет maxlen символов строки source к строке dest. char *strchr( char *source, char ch' ) Поиск в строке source первого вхождения символа ch. int strcmpt char *sl, char s2 ) Возвращает 0, если si == s2, возвращает < О, если sl<s2, и возвращает > 0, если sl>s2. int strncmp( char *sl, char s2, int maxlen ) Возвращает 0, если si == s2, возвращает < О, если sl<s2, и возвращает > 0, если sl>s2. Сравнивается только первые maxlen символов двух строк. int stricmp( char *sl, char s2 ) Возвращает О, если si == s2, возвращает < О, если sl<s2, и возвращает > 0, если sl>s2. He проверяется регистр букв. int strniempt char *sl, char s2, int maxlen ) Возвращает О, если si == s2, возвращает < О, если
172 Глава 6 Sl<s2, и возвращает > 0, если sl>s2. Сравниваются только первые maxlen символов двух строк. Не проверяется регистр букв. char *strcpy( char *dest, char *source ) Копирование строки source в строку dest. char «strncpyt char *dest, char *source, unsigned maxlen ) Копирование maxlen символов из строки source в строку dest. Int strlent char *s) Выдает число символов в строке s без учета нулевого символа конца строки. char *strlwr( char *s) Переводит всю строку s в нижний регистр (в строчные буквы). char *strupr( char *s) Переводит всю строку s в верхний регистр (в прописные буквы). char *strdup( char *s) Вызывает функцию malloc и отводит место под копию s. char *strset( char *s, char ch) Заполняет всю строку s символами ch. Char »strnset( char *s, char ch, unsigned n) Заменяет первые п символов строки s на символ ch. char *strrev( char *s) Инвертирует все буквы в строке s. int Btrcspn(char *sl, char *s2 ) Возвращает длину начального сегмента строки si, которая состоит исключительно из символов, не содержащихся в строке s2. char ♦strpbrk( char *sl, char «s2 ) Просматривает строку si до тех пор, пока в ней не вст-
Указатели и массивы 173 ретится символ, содержащийся в s2. char *strrchr( char *s, char с ) Просматривает строку s до последнего появления в ней символа с. int strspnl char *sl, char *s2 ) ■Возвращает длину начального сегмента строки si, который состоит исключительно из символов из строки s2. char *strtok( char *sl, char *s2 ) Предполагается, что строка si состоит из фрагментов, разделенных одно- или многосимвольными разделителями из строки s2. При первом обращении к strtok выдается указатель на первый символ первого фрагмента строки si. Последующие вызовы с заданием нуля вместо первого аргумента будут выдавать адреса дальнейших фрагментов из строки si до тех пор, пока фрагментов не останется. В стандартной библиотеке (Турбо) Си stdio.h имеются две функции, являющиеся расширениями функций print! и scan! и поддерживающие ввод/вывод не в файлы, а в строки. Функция sprintf выводит в строку, адрес которой задается первым параметром: int sprintf( char *dest, char *format, ... ); Функция sscanf читает из строки, адрес которой задается первым параметром. int sBcanfl char *dest, char *format, ... ); Программа на листинге 6.12 выведет следующее: destination = -56 destination = 124.968 destination = i = -56. r = 124.967514 Листинг 6.12. Форматный вывод в строку ♦include <stdio.h> char destination^ 100 ];
174 Глава 6 nain() { int i = -56; float г = 124.96752; Bprintf( destination, "%d", i ); printft "\ndestination = 'As', destination ); sprintf( destination, "%.3f", г ); printf( "\ndestination = Xs", destination ); eprintf( destination, "i = %d. г = XT', i, г ); printft "\ndestination = Xs\n", destination ); > Ошибки прм использовании строк Очень важно, чтобы программист был уверен в том, что все строки оканчиваются символом '\0'. Если строки задаются в виде литералов, то компилятор добавляет нулевой символ в конец строки автоматически. Если же строка формируется символ за символом, как показано в примере, ответственность за окончание строки лежит иа самом программисте: my_string[ 0 1 = 'С; ny_string[ 1 1 = 'А'; ny_string[ 2 ) = 'Г; ay_string[ 3 1 = '\0'; Все функции манипуляции со строками, приведенные в табл. 6.2, рассчитаны на то, что каждая нз строк завершена символом нуль. Если это не так, то могут произойти серьезные ошибки. 6.7. Передача массивов и указателей в качестве параметров функции Подробно функции (Турбо) Си будут изучены в гл. 8. Здесь же мы ограничимся рассмотрением лишь таких параметров функций, как массивы и указатели. Мы введем и понятие параметра, передаваемого по ссылке. Если в качестве параметра функции передается массив, то иа самом деле внутрь функции попадает только
Указатели и массивы 175 начальный адрес массива. В связи с этим возникает понятие состояния определенности in/out. Состояния определенности будут рассмотрены в гЛ. 8 при описании функций. Если внутри функции происхбдит изменение в массиве, то такое изменение произойдет и с массивом, адрес которого был передан в функцию. Обычно параметры функций (Турбо) Си передаются по значению (переданное значение оказывается изолированным от копии такого значения, выполненной внутри функции). В функции printtable, приведенной иа листинге 6.13, выводятся значения двух массивов xtab и ytab, состоящих из вещественных чисел с удвоенной точностью. Первым параметром функции printtable идет строка name, содержащая имя таблицы. Второй и третий параметры - xtab и ytab - это начальные адреса двух массивов типа double. В цикле WHILE while ( numpts- ) printft "Xf, %14f\n", *xtab++, *ytab++ ); выводится значения по каждому нз адресов xtab и ytab и затем значения этих адресов увеличиваются на sizeoi( double ) байтов. Листинг 6.13. Печать таблицы void print „tablet char *name, double *xtab, double •ytab, int numpts ) { printf( "\п\пЗначения функции %s \n\n", name ); while ( numpts- ) printft "%f, %14f\n", *xtab++, *ytab++ ); ) В качестве другого примера передачи массива как параметра рассмотрим программу на листинге 6.14. Программа напечатает data! О J = -11 data[ 1 ] = 15 data[ 2 ] = -9 data[ 3 ] = 17 data[ 4 ] = 23
176 Глава 6 Функция modify увеличивает иа 1 значения, расположенные по адресам а[0], а[1], ..., а[4], и возвращает эти значения обратно в массив data, описанный в программе main. Листинг 6.14. Передача массива как параметра Mnclude <stdio.h> Int datatl = { -12, 14, -10, 16, 22 >; maint) { extern void aodifyt int *a, int size ); int i; modifyC data, 5 ); for ( i = 0; i < 5; i++ ) printf( "\ndataf %d J = %d\n", i, dataf i J ); > void modify! int *a, int size ) < int i; for ( i = 0; i < size; i++ ) at i J++; > Еще один пример передачи массива в качестве пара- Метра приведен на листинге 6.15. В программе описывается статический массив scores размером 100. Вызывается функция enterjist, в качестве параметров которой передаются массив scores и адрес целой переменной size. В функции enter_list переменной-указателю nextjgrade присваиваетси адрес входного массива. Далее выполняется несколько странный цикл FOR: for ( ; Bcanft "Xd", next_grade ), *next_grade != -1; ( *size )++, next_grade++ ) printfC "ХпВведите значение ( - 1 для завершения): "); Цикл не выполняет инициализирующих действий (пере- меииая-указатель инициализирована перед входом в цикл
Указатели и массивы 177 FOR). При проверке условия сначала вызывается функция scanf, ей в качестве параметра указываетси адрес переменной nextjgrade, и функция возвращает целое число. Далее следует проверка того, не равно ли значение next jyrade числу М. В том месте оператора цикла, где обычно указывается приращение шага, стоит увеличение значения переменной size и увеличение адреса в next_grade. Как уже отмечалось в гл. 5, циклы FOR - это наиболее общий и весьма мощный вариант реализации повторения. Приведенный цикл хорошо иллюстрирует сказанное. Поскольку на вход программы был передан адрес переменной size, то и значение локальной переменной size будет переслано обратно в функцию main с использованием этого адреса (глобальные и локальные переменные в функциях подробно рассматриваются в гл. 8). Такой способ передачи значений называется передачей по ссылке (by reference). После того как отработает функция enter_Iist, в главной программе будут выведены целые из массива scores. Листинг в. 15. Передача параметров по ссылке и массивов в качестве параметров: ♦include <stdio.h> int scorest 100 ]; main О { extern void enter_list( int «grades, int *size ); int index; int size; enter_list( scores, &size ); for ( index = 0; i < size; index++ ) printfC "\nscoreel '/.d J = Xd\n", index, scores! index J ); > void enter_list( int *grades, int *size ) { /* 8 этой программе пользователю предлагается ввести серию значений. Введенная информация записывается
178 Глава 6 в массив grades. */ int next.grade = grades; /* Начальный адрес вводного массива */ printfC "ЧпВведите значение ( - 1 для завершения): ">; •size = 0; for ( ; scanf( "Xd", next_grade ), •next.grade != -1; ( *size )++, next_grade++ ) printfC "ЧпВведите значение ( - 1 для завершения): "); > Следующий пример использования массивов в подпрограммах - задача выявления и печати всех слов из входного потока. Предполагается, что «слова» в нашем случае отделяются друг от друга иебуквенными символами. На основании данного определения слово «isn't» будет разбито иа два слова - «isn» н «t». Программа, читающая слова из входного потока и выводящая их по слову в строку, приведена на листинге! 6.16. В главной программе описывается массив символов word, рассчитанный на хранение 79 байтов (один байт зарезервируем для хранения признака конца строки). Последовательность слов будет счнтываться из входной цепочки до тех пор, пока функция getword не вернет значение 0. Ненулевое значение будет свидетельствовать о том, что в массиве word появилось слово. Параметр wrd функции get_word соответствует адресу массива word. Важно заметить, что распределение памяти для wrd должно быть выполнено вне функции get_wrd (например, в функции main). Для проверки на небуквенный символ во вводном потоке в первом цикле WHILE используется макро isalpha из файла ctype.h. Оператор *wrd++ = tolower( ch ); используя макро tolower, присваивает значение ch, переведенное на нижний регистр, по адресу wrd и увеличивает этот адрес на 1 байт.
Указатели и массивы 179 В следующем цикле WHILE while ( isalphat ch = getchar О ) && ch != EOF ) *wrd++ = tolowert ch ); производится просмотр входного потока до тех пор, пока не будет введен небуквенный символ или пока не встретится признак конца файла. Сразу после цикла WHILE к строке добавляется символ конца строки '\0'. В программе на листинге 6.16 нет проверки того, что слово может превысить 80 символов. Если во входном потоке встретится последовательность буквенных символов длиной больше или равной 80, то будет произведена запись значений за границу верхнего адреса массива word. Читателю предлагается добавить в функцию get_word несколько строк кода, обеспечивающих защиту от затирания информации вслед за массивом word. Листинг в. 16. Выделение слов из входного потока /* Программа выделения слов во входном потоке */ /* Обращение: word < имя_входного_файла */ /* Файл word.с */ «include <stdio.h> «include <ctype.h> main О { extern int get_word( char *wrd ); char word! 80 J; while ( get_word( word ) ) printft "\nXs", word ); printfl "\n" ); ) int get_word( char *wrd ) { int ch; /* Поиск первого буквенного символа */ while ( !isalpha( ch = getcharO ) 8.8. ch != EOF )
180 Глава 6 if ( ch == EOF ) return 0; else { *wrd++ = tolowert ch ) ; /* Если надо, то преобразует ch в верхний регистр */ while ( isalphal ch = getchar О > && ch != EOF ) *wrd++ = tolowerl ch ); *wrd = '\0'; /* В конец слова поместить нулем»* символ */ return 1; > } 6.8. Многомерные массивы Все выражения (Турбо) Си, содержащие обращения к массивам, переводятся в выражения с указателями. Выражение а[ 6 J эквивалентно выражению с указателем *( а + 6 ) Выражение а[ 2 J [ 3 J эквивалентно выражению с указателем *( *(а + 2 ) + 3 Многомерные массивы располагаются в памяти так, что быстрее всего меняется последний индекс. Данные двумерного массива, описанного как int dt 3 J [ 4 J располагаются в памяти в следующем порядке:
Указатели и массивы 181 (И О И О ], d( О И 1 ], d[ О ][ 2 ], d[ О И 3 ), df 1 If О J, dt 1 1С 1 J, d[ 1 J[ 2 J, df 1 J[ 3 J, d( 2 It 0 ], d[ 2 )[ 1 J, d[ 2 ][ 2 ), df 2 J[ 3 ). Данные трехмерного массива, описанного как int df 2 J f 2 1 [ 2 1 располагаются в памяти в следующем порядке: dt О К О ][ О 1, d[ О ][ О ][ 1 ], df О И 1 Л О 1, df О И 1 Л 1 1, df 1 И О Л О ), df 1 П О И 1 ), df 1 И 1 И О J, dt 1 И 1 И 1 1. Рассмотрим программу на листинге 6.17. Здесь описан двумерный массив datal[50][20]. Массив содержит 1000 целых, причем адрес начала массива находится в datal. Размер массива - 2006 байтов. Имя datal является константой-указателем. В первом фрагменте программы с циклами FOR: for ( row = 0; row < 50; row++ ) for ( col = 0; col < 20; col++ ) datalf row Jf col J = row * col; вычисляется произведение row*col, н оно присваивается элементу массива datal, адрес которого получается как смещение от начального адреса массива, равное значению 20*row+col, умноженному на размер целого (2 байта). Первый параметр (row) определяет номер строки матрицы datal; второй параметр (col) определяет номер столбца матрицы datal. Выражение datalf row И col ] переводится компилятором Турбо Сн в эквивалентное выражение *( *( datal + row ) + col ) Следовательно, во втором цикле FOR на листинге 6.17 двумерному массиву datal присваивается тот же на-
182 Глава 6 бор значений. Кое-кому запись двойных индексов datalfrowjcol] кажется более наглядной, чем ее эквивалент *( *{ datal + row) + col). Имя двумерного массива является указателем на первую строку массива. Выражение int( «datal + row ) задает указатель иа другую строку (указатель на целое), адрес которой вычисляется как произведение смещения row*20 и sizeof(int), добавляемое к адресу datal. Описание int( »data2 ) [ 20 ), на листинге 6.17 определяет указатель на массив (строку) из 20 целых. Круглые скобки здесь существенны. Заметим, что описание int »data2C 20 1 определяет массив из двадцати указателей на тип int и для нашего примера не подходит. Выражение •( •( data2 + row ) + col ) эквивалентно более читабельному выражению data2[ row И col I Как и во всех подобных случаях описания указателей, следует воспользоваться такой функцией • запроса памяти, как, например, malloc, чтобы отвести в куче необходимую память для размещения двумерного массива. В выражении data2 = ( int • ) mallocf 50 • 20 • sizeoff int ) ); для перевода указателя на char, получаемого с помощью функции malloc, в нужный нам указатель на int используется преобразование ( int* ).
Указатели и массивы 183 В цикле FOR for ( row = 0; row < 50; row++ ) for ( col = 0; col < 20; col++ ) •( •( datal + row ) + col ) = row • col; двумерному массиву data2 присваивается тот же набор значений, что и в предыдущих случаях. И в последнем цикле FOR массиву data2 будут присвоены точно такие же значения. Листинг 6.17. Двумерные массивы «include <stdio.h> «include <alloc.h> int datal [ 50 H 20 1; int ( »data2 H 20 1; main!) { int row, col; printff "ЧпРазмер datal = Y.d", sizeoff datal ) ); for ( row = 0; row < 50; row++ ) for ( col = 0; col < 20; col++ ) datal[ row It col ] = row • col; printff "\ndatall 42 If 16 1 = %d", datalI 42 II 16 1 ); for ( row = 0; row < 50; row++ ) for ( col = 0; col < 20; col++ ) •( *( datal + row ) + col ) = row • col; printff "\ndatall 42 И 16 ] = Г.й", datal! 42 IE 16 1 ); data2 = ( int • ) mallocf 50 • 20 • sizeoff int ) ); for ( row = 0; row < 50; row++ ) for ( col = 0; col < 20; col++ ) •( •( data2 + row ) + col ) = row • col; printff "\ndata2I 42 II 16 I = Y.d", data2I 42 II 16 I ); for ( row = 0; row < 50; row++ ) for ( col = 0; col < 20; col++ ) data2I row II col I = row • col;
184 Глава 6 printff "\ndata2I 42 If 16 ] = Xd\n", data2E 42 И 16 1 ); } Программа на листинге 6.17 выведет: Размер datal = 2000 datal[ 42 ][ 16 ] = 672 datal[ 42 ][ 16 ] = 672 data2( 42 1[ 16 1 = 672 data2[ 42 И 16 ] = 672 Программа, приведенная иа листинге 6.18, показывает распределение адресов в двумерном массиве. Описывается статический двумерный массив data. Программа выводит: Адрес data = 1362 Адрес •( data + 1 ) = 1402 Адрес •( data + 2 ) = 1442 Адрес •( data + 2 ) + 1 = 1444 Как уже объяснялось, адрес *(data+l) отстоит от адреса data на 40 байтов. Аналогично адрес *(data+2) отстоит от адреса *{data+l) иа 40 байтов. И наконец, адрес *(data+2) + 1 отстоит всего иа два байта от адреса *{data+2). Адрес *(data+l) является адресом начала первой строки матрицы data. Адрес «(data+2) является адресом начала третьей строки матрицы data. И наконец, адрес *(data+2)+l является адресом второго целого числа во второй строке матрицы data. Листинг 6.18. Адресация в двумерном массиве «include <stdio.h> int data! 50 IE 20 1; main() { printff "ЧпАдрес data = Y.d", data ); printff "ЧпАдрес •( data + 1 ) = VA\ »( data + 1 ) );
Указатели и массивы 185 printfl "ЧпАдрес •( data + 2 ) = №, •( data + 2 ) ); printfl "ХпАдрес •( data + 2 ) + 1 = %d", »( data + 2 ) + 1 ); ) Типичной задачей, в которой применяются двумерные массивы, является вычисление средних значений по строкам и столбцам для набора числовых данных. На листинге 6.19 представлена программа вычисления суммы по строкам и по столбцам в двумерном массиве. Рекомендуем внимательно изучить эту программу, поскольку в ией используются основные приемы работы с адресной арифметикой. Программа выводит: Среднее значение в строке 45 = 54.500000 Среднее значение в столбце 4 = 28.500000 Основными действиями вычисления среднего значения по столбцу с номером col_num являются следующие: int ( »row_ptr_to_col )[ nuaber_cols ] = dta; lnt i, cumulative_gum = 0; for ( i = 0; i < number_rows; i++ ) cumulative_gum += ( •row_ptr_to_col++H col_num ]; В описании int ( »row_ptr_to_col )[ number_cols 3 = dta; задается указатель на массив из 20 целых. Начальный адрес этого массива устанавливается равным начальному адресу первой строки двумерного массива dta. В цикле FOR присутствует выражение cumulative_isum += ( »row_ptr_to_col++)[ col_num ]; увеличивающее значение переменной cumulative_sum на величину, размещенную по адресу row_ptr_to_col[col_num] Далее адрес увеличивается на величину, равную длине одной полной строки матрицы и вычисляемую как 20*sizeof(int) байтов.
186 Глава 6 Основными действиями вычисления среднего значения в строке с номером row_num являются следующие: int ( »col_ptr_to_row ) = &dtaf row_num If 0 1; int i, cumulative_sum = 0; for ( i = 0; i < number_cols; i++ ) cumulative_sum += •col_ptr_to_row++; Указателю col_ptr_to_row вначале присваивается адрес начала строки матрицы с номером row_num. В цикле FOR значение переменной cumulativesum увеличивается иа величину, расположенную по адресу coI_ptr_to_row. Далее этот адрес увеличивается иа sizeof(int), или иа 2 байта. Листинг 6.19. Вычисление средних значений по строкам и столбцам двумерного массива ttinclude <stdio.h> #define number_rows 50 #define number_cols 20 int data! number_rows П number_coLs 1; mainO { extern float column_average( int dtal number_rows ] I number_cols 1, int col_num ); extern float row_average( int dtaf number_rows 1 [ number_cols 1, int row_num ); int row, col; /* Запись значений в матрицу */ for ( row = 0; row < number_rows; row++ ) for ( col = 0; col < number_cols; col++ ) dataf row П col ] = row + col; printff "ХпСредиее значение в строке 45 = Y.f", row_average( data, 45 ) ); printff "ХпСреднее значение в столбце 4 = %f\n".
Указатели и массивы 187 column_average( data, 4 ) ); ) float column_average( int dtaf number_rows 1 [ number_cols 1, int col_num ) { int ( »row_ptr_to_col )[ number_cols ] = dta; int i, cuBulative_sum = 0; for ( i = 0; i < number_rows; i++ ) cumulative_sun += ( •row_ptr_to_col++M col_num 1; return ( float ) cumulative_sum / number_rows; float row_average( int dtal number_rows ] [ number_cols 1, int rowjum ) { int ( •col_ptr_to_row ) = &dtal row_num ][ 0 1; int i, cumulative_isum = 0; for ( i = 0; i < number_cols; i++ ) cumulative_isum += •col_ptr_to_row++; return ( float ) cumulative_gum / number_cols; } Если многомерный массив передается в качестве параметра функции, то необходимо задавать и все его размерности, кроме первой. Листинг 6.20 является модификацией фрагмента листинга 6.19 вычисления среднего значения по столбцам. Здесь в описании функции не задается первая размерность массива (число строк). Чтобы компенсировать этот недостаток, к описанию функции добавлен еще один (третий) параметр. Листинг 6.20. Модификация функции вычисления среднего значения по столбцу в двумерном массиве float column_average( int dta[][ number_cols 1, int col_num, int num_rows ) {
188 Глава 6 int ( »row_ptr_to_col )[ number_cols 1 = dta; int i, cumulative_sum = 0; for ( i ■ 0; i < num_rows; i++ ) cumulative_isum += ( •row_ptr_to_col++H col_num ]; return ( float ) cumulative_sum / number_rows; } Предостережение При описании многомерных массивов следует иметь в виду, что они требуют большого объема памяти. Например, для размещения трехмерного массива, описанного как double data! 50 П 50 II 50 1; потребуется 125000*sizeof( double ) байтов, или 1000000 байтов. 6.9. Параметры, задаваемые в командной строке В программу иа (Турбо) Си можно передавать информацию и с использованием командной строки. Вообще говоря, функция main может быть описана следующим образом: aain( int argc, char* argvN ) { Возможно и иное, но эквивалентное описание: main! int argc, char»» argv ) { В обоих описаниях argv является свободным массивом типа char, или, что эквивалентно, массивом указателей. Термин «свободный» отражает тот факт, что длины всех строк в массиве могут быть различными. В этом состоит отличие от двумерного массива символов, в котором размеры всех строк совпадают. Командная строка указывается при вызове программы из DOS, а параметры командной строки отделяются друг от
Указатели и массивы 189 друга одним или несколькими пробелами. Первый параметр типа int - argc - содержит число параметров в комаидиой строке. Его значение всегда больше нли равно единице. Значение, помещенное в argvf 0 ], - это имя файла программы. Если первый параметр командной строки существует, то он помещается в argvf 1 ]. Второй параметр, если он указан, помещается в argvf 2 ] и т.д. Рассмотрим программу иа листинге 6.21. Возможно, пользователь пожелает скопировать эту программу и провести с ней ряд экспериментов. Если пользователь введет test программа выведет Значение argc = 1 Параметр командной строки 0 = C:\TEST.EXE Значение, помещаемое в элемент argvfO], предоставляет полное имя (включая путь) файла. Если пользователь введет test Турбо Си - компактный и прекрасный язык. то для версии DOS 3.x вывод будет иметь вид Значение argc = 7 Параметр командной строки 0 = C:\TURBOBK\CHAP\TEST.EXE Параметр командной строки 1 = Турбо Параметр командной строки 2 = Си Параметр командной строки 3 = компактный Параметр командной строки 4 = и Параметр командной строки 5 = прекрасный Параметр командной строки 6 = язык. Память для двумерного массива argv отводится системой. Параметры из комаидиой строки могут использоваться во многих прикладных программах с большой пользой. Например, при вызове программы могут указываться имена входных и выходных файлов или числовые значения, кото-
190 Глам 6 рые внутри программы переводятся в числовой тип. Листинг 6.21. Параметры из командной строки /• Файл test.с •/ «include <stdio.h> main( int argc, char»» argv ) { int i; printf("ЧпЗначение argc = %d", argc ); for ( i = 0; i < argc; i++ ) printff "ЧпПараметры из командной строки '/.А = V,s\n", i, argv! i 1 ); } 6.10. Свободные массивы Свободными будем называть двумерные массивы или матрицы, размер каждой из строк которых может быть различным. Ниже мы рассмотрим два примера свободных массивов. В первом примере будем записывать и распечатывать список строк. Каждая из строк может быть различной длины. Посмотрим иа программу на листинге 6.22. Глобальный двумерный массив names инициализируется с использованием массива из 15 строк, так что names! 0 1 = "Paul" names! 1 ] = "Richard" и так далее. Объем памяти, требуемый для хранения такого массива, в точности совпадает с суммой длин всех строк (включая и по одному байту на строку, отведенному под признай* конца строки). Не будет потеряно ни одного байта. Система должна отвести место под массив на основании литеральных строковых коистант, заданных в блоке инициализации. В разделе описаний функции main задан двумерный массив name list
Указатели и массивы 191 char» name_list[ 20 1; Name_list - это массив из 20 указателей на тип char, который может содержать до двадцати строк различной длины. Память под 20 потенциальных строк пока ие отведена. Имеется память лишь под 20 указателей иа строки (для модели памяти small это составляет 40 байтов). У функции printnames два параметра. Первый параметр - tablejptr - задает указатель на область памяти, где расположена первая строка. Второй параметр п определяет число распечатываемых строк. Основными действиями рассматриваемой функции будут следующие: char* »end_ptr = table_ptr + ( n - 1 ); while ( table_ptr <= end_ptr ) printff "Y/!s\n", »table_ptr++ ); Указатель end_ptr ссылается на область держащую адрес последней строки в таблице, нужен для того, чтобы можно было прекратить цикла WHILE, идущего ниже. В выражении •table_ptr++ снимается ссылка иа строку по адресу table_ptr, а затем адрес table_ptr увеличивается на величину, необходимую для размещения в памяти указателя (2 байта для модели памяти small). У функции assignnames также два параметра. Оба они передаются по ссылке, т.е. функция сама засылает какие-то значения по адресам, переданным в качестве параметров, и по соседним с ними. Число введенных пользователем имен возвращается во втором параметре п. При вызове функции assign_names ей должен быть передан адрес целой переменной, в которой будет помещаться число имен в массиве arraynames. (Посмотрите, как вызывается функция assign_value в функции main.) Первый параметр - tab_ptr - является указателем на область памяти, содержащую адрес первой строки, записанной в массив namelist. При помощи следующего фраг- памяти, со- Такой адрес выполнение
192 Глава 6 меита программы в функции assign_names список строк вводится и присваивается массиву namedjist •п = 0; do { printf("ЧпВведите имя : " ); result = scanf( "%s", str ); If ( result == 1 ) { ( *n )++; *tab_ptr = malloc( strlen( str ) + 1 ); strcpy( *tab_ptr++, str ); } } while ( result == 1 ); Имя строки str передается в функцию scant. Это имя задает начальный адрес строки, которую вводит функция scant. После ввода каждой очередной допустимой строки при помощи функции scant значение, размещенное по адресу п, увеличивается на единицу. Память отводится по адресу *tab_ptr, причем ее размер позволяет разместить все символы строки str, включая и нулевой байт окончания строки. Все символы строки переписываются по адресу *tab_ptr с помощью стандартной функции strcpy, и tab_ptr увеличивается иа смещение, соответствующее размеру указателя. Зиачеиие адреса *tab_ptr записывается в область памяти tab_ptr. Понятно ли, почему память заводится по адресу ♦tabptr, а не по адресу tab_ptr? Как и в предыдущих примерах двумерных массивов, *tab_ptr указывает иа начальный адрес каждой строки, пока выполняется цикл DO WHILE. Когда> цикл DO WHILE завершится, то двумерный свободный массив namejist, состоящий ие более чем из двадцати строк, оказывается заполненным. Листинг 6.22. Свободные массивы строк #include <stdio.h> ttinclude <alloc.h> char *names!1 = {
Указатели и массивы 193 "Paul", "Richard", "Lewis", "David". "Marc", "Eric", "Mary", "Livingston", "Irving", "Elizabeth", "Zachary", "Everett". "Emery", "Vincent", "Gary" ); main() { extern void assign_names( char* *tab_ptr, int »n ); extern void prlnt_names( char* *table_ptr, Int n ); int number; int number_names; char* name_list[ 20 1; print_names( names, 15 ); assign_names( name_list, &number ); print .names ( name_list, number ); printf( "\n" );_ } void assignjnames( char» *tab_ptr, int »n ) { char *str = malloc( 80 ); Int result; •n = 0; ': do { printf("\nBBeflHTe имя : " ); result = scanf( "Xs". str ); if ( result == 1 ) < 7 Зак. 795
194 Глава 6 ( »n )++; *tab_ptr = malloc( strlen( str ) + 1 ); strcpy( *tab_ptr++, str ); > } while ( result == 1 ); ) void print_names( char* *table_ptr, int n ) { char» *end_ptr = table_ptr + ( n - 1 ); while ( table_ptr <= end_ptr ) printf( "\7.s\n", *table_ptr++ ); ) Если последними введенными именами перед тем, как введена комбинация CTRL+Z, были имена Sheila и Магу, то вывод результатов программы на листинге 6.22 будет иметь следующий вид: Richard Lewis David Marc Eric Mary Livingston Irving Elizabeth Zachary Everett Emery Vincent Gary Введите имя : Sheila Введите имя : Mary Введите имя : ~Z Sheila Mary
Указатели и массивы 195 На рис. 6.5 графически представлен двумерный массив namedlist со строками переменной длины. В области памяти, иа которую указывает name_list, размещается адрес памяти массива, содержащего строку "Sheila". По адресу памяти, иа который указывает name_list+l, помещается адрес массива, содержащего строку "Магу". name_list name_lisU 0 1 . S м h а е г 1 У 1 \0 а \0 name_list[ 1 ] Рис. 6.5. Свободный массив namejist. На листинге 6.23 представлен вариант аналогичной программы, но для записи и вывода двумерных свободных массивов целых. В глобальном массиве int*data[100] отводится память под 100 указателей иа значения типа int. Каждый из ста указателей из такого массива вообще говоря может ссылаться иа адрес начала какой-то строки матрицы. В функции assignvalues пользователю предлагают ввести количество столбцов в каждой строке и значения элементов каждой строки. Здесь же запрашивается память для размещения значений строки. Обратите внимание иа форму, использованную для записи параметра функции assignvalues, соответствующего двумерному массиву. Описание параметра int *table_ptrQ эквивалентно описанию int* *table_ptr. Вторая форма уже применялась нами в программе, приведенной иа листинге 6.22, которая иллюстрирует свободные массивы. Первый параметр функции assignvalues (os назван table_ptr) указывает на область памяти, содержащую адрес нулевой строки матрицы. Второй параметр отражает число строк, которым следует присвоить значения. Нулевой столбец каждой строки используется для хранения числа элементов в данной строке. При запросе памяти для каждой строки необходимо отвести дополни- 7*
196 Глава 6 тельно еще одну ячейку под целое число, помещаемое в нулевой столбец строки. Ввод значений строк матрицы и запись их в массив осуществляются при помощи следующего фрагмента программы: printf( "ЧпУкажите число значений в строке Xd :", i >; scanf( "Xd", &number_values ); •table_ptr = ( int * ) malloc( ( number_values + 1 ) * sizeof( int ) ); ptr = *table_ptr++; •ptr++ = number_values; for ( J = 1; j <= number_values; J++ ) { printf( "ЧпВведите значение Xd: ", J ); scanf( "Xd", &val ); *ptr++ - val; } Выражение *table_ptr = ( int * ) malloc( ( number_values +1 ) * sizeof( int ) ); отводит в куче память под соответствующее число байтов, и полученный адрес заносится в *table_ptr. Для нулевого столбца запрашивается одна дополнительная единица памяти. Значение *table_ptr записывается в область памяти, иа которую ссылается указатель table_ptr. Указателю иа целое ptr присваивается адрес table_ptr, после чего адрес table_ptr увеличивается иа едииицу. Целочисленное значение number_values заносится в *ptr, и затем адрес ptr увеличивается иа два байта (иа размер значения целого типа). В цикле FOR по указателю *ptr записывается надлежащее количество значений целого типа val и затем адрес ptr увеличивается иа два байта (на размер значения целого типа). У функции printvalues такие же два параметра, как m у функции assign_vaiues. Распечатка каждой строки свободного массива выполняется при помощи операторов:
Указатели и массивы 197 for ( row_num * 0; row_num < num_rows; row_num++ ) { start_col = *table_ptr++; num_cols = *start_col++; for ( coljium = 0; col_num < num_cols; со1_дит++ ) printf( "%d ", *start_col++ ); printf( "\n" ); > Указатель start_col на целое в начале работы устанавливается на начальный адрес каждой строки *table_ptr. Число печатаемых столбцов numcol определяется путем снятия ссылки по start_col. Адрес start_col увеличивается иа единицу. В выражении printf< "%d ", *start_col++); помещенном во внутреиием цикле FOR, снимается ссылка с указателя startcol, и адрес увеличивается иа два байта для того, чтобы сослаться теперь на следующий столбец. Преимущество использования свободного массива целых заключается в том, что ие требуется отводить память с запасом для размещения строк максимально возможной длины. Если ие считать памяти, предназначенной для хранения нулевого столбца, то запрашиваемая память будет в точности соответствовать фактической потребности в ней. Листинг 6.23. Свободный массив целых #include <stdio.h> «Include <alloc.h> int number_rows; BainO { extern void assign_values( int» table_ptr(l, int num_rows ); extern void print_yalues( int* table_ptri1, int num_rows ); int* datal 100 ];
198 Глава 6 printf("Укажите чисжо строк матрицы: " ); scanf( "Xd", &number_rows ); if ( number_rows > 100 11 number_rows < 1 ) { printf("ЧпЧисло строк должно быть от 1 до 100"); exit ( 1 ); ) assign_values( data, number_rows ); print^values( data, number_rows ); printf( "\n" ); ) void assign_values( int* table_ptrll, int num_rows ) { int number_values; int val, i, J; int *ptr; for ( 1=1; 1 <= num_rows; i++ ) { printf( "ХпУкажвте чисжо значения в строке У.й :", i ); scanf( "%d", &number.values ); •table_ptr = ( int • ) malloc( ( number_values + 1 ) * sizeoff int ) ); ptr = »table_ptr++; •ptr++ = number_values; for ( j = 1; j <= number„values; J++ ) { printf( "ЧпВведите значение %d: ", j ); scanf( "%d", &val ); *ptr++ = val; } ) ) void print_values( int* table_ptrU, int num_rows ) i int *start_col; int num_cols, row_num, col_num; for ( row_nuei = 0; rotf_num < num_rows; row_num++ )
Указатели и массивы 199 { start_col = *table_ptr++; num_cols = *start_col++; for ( coljnum = 0; col_num < numjcols; col_num++ ) printf( "%d ", *start_col++ ); printf< "\n" ); ) > Ниже приведен пример выполнения такой программы. Укажите число строк матрицы : 3 Укажите число значений в строке 1:1 Введите значение 1: 1 Укажите число значений в строке 2 : 2 Введите значение 1: 1 Введите значение 2: 2 Укажите число значений в строке 3 : 3 Введите значение 1: 1 Введите значение 2: 2 Введите значение 3: 3 1 1 2 12 3
Гпала 7 Структуры, объединения и ссылочные типы данных В предыдущей главе был рассмотрен структурированный тип данных array (массив). Этот конструируемый самим пользователем тип данных обеспечивает возможность доступа к одиородиым значениям. Обычно все элемеиты массива бывают одинакового типа, а в памяти всегда занимают одинаковое по размеру пространство. Такие элементы языка Си, как структуры и объединения, предоставляют средство для доступа к записям, или группам даииых, в которых каждая единица (запись) содержит поля одного или нескольких типов. Структуры (Турбо) Си являются аналогами записей в Паскале и Моду- ле-2. Более специализированный тип структур - объединение - позволяет разделять память между полями данных. Комбинируя массивы, структуры и объединения, программист может создать огромное разнообразие структур данных, позволяющих моделировать сложные объекты, возникающие при решении задачи. 7.1. Структуры Структуры конструируются следующим образом: struct имя_структуры < тип_1 поле_1; тип_2 поле_2; тип_п полел; >; Обратите внимание на точку с запятой после закрывающей скобки в описании структуры. Отсутствие такого разделителя является частой синтаксической ошибкой. Описание переменной структурного типа выглядит следующим образом: struct struct_name x, у, z; Структурным переменным можно присваивать значение
Структуры, объединения и ссылочные типы данных 201 агрегатио (сразу всей структуре), ио их нельзя сравнивать иа равенство или неравенство. Для сравнения структур программисту следует написать специализированные функции, зависящие от приложения, К структуре можно применять операцию & вычисления ее адреса. К полям структуры можно обращаться, указывая имя переменной, имя поля и разделяя эти имена точкой. Например: type_l valuel = x.field_l; type_3 value3 = z.field_3; Если переменная ptr определена как указатель иа структуру, то для доступа к полям структуры можно использовать операцию ->. Например, struct struct_name *ptr; type_l valuel = ptr -> field_l; type_2 value2 = ptr -> field_2; Оператор ptr -> field 1 эквивалентен выражению (*ptr).field_l . В тех случаях, когда допускается использовать ynapsyio операцию & получения адреса для структуры в целом, можно эту же операцию использовать и для получения адреса компонента структуры. На стыке соседних полей структуры из-за необходимости выравнивания границ памяти под такие элементы могут возникать дыры, или пустоты. Возникновение пустот может привести к .нарушению мобильности программ, сказывающемуся при переходе от одной реализации Си к другой. Если при вызове системы Турбо Си указан ключ -а, то это означает, что компилятор должен выравнивать структуры (или объединения) по границе слова, добавляя к структуре дополнительные байты. Указание ключа -а гарантирует следующее: 1. Структура будет начинаться с границы слова (четный адрес). 2. Каждое иесимвольиое поле будет начинаться со сдвигом . на четное число байтов от начала структуры.
202 Глава 7 3. В конец структуры будет (по необходимости) добавлен один байт, с тем чтобы вся структура содержала четное число байтов. Так как при описании переменной структурного типа требуется, чтобы в описании присутствовал спецификатор struct, обычно полное имя структурного типа оформляют в виде макро. Покажем такой прием на следующем примере. Пусть нам нужно выполнять действия над комплексными типами. Поскольку комплексное число может быть представлено двумя вещественными, то воспользуемся следующей структурой: «define COMPLEX struct complex_type COMPLEX { float real; float imag; >; Приведенное макроопределение COMPLEX позволяет описывать комплексные переменные в естественной форме. Если переменные cl, с2 н сЗ описаны как комплексные, то операция умножения cl на с2 для вычисления сЗ может.быть реализована следующим образом: COMPLEX cl, c2, сЗ; c3.real = cl.real • c2.real - cl.imag • c2.imag; c3.imag = cl.imag * c2.real + cl.real * c2.imag; Удобно ввести абстрактные арифметические операции над комплексными числами, реализовав для каждой такой операции соответствующую функцию. Заметим, что непосредственный доступ к полям структур - это плохой стиль программирования. Структуры можно использовать при построении абстрактных типов данных. Все операции, которые разрешены применительно к структуре, должны быть при этом реализованы в виде отдельных функций. В старых версиях Си при передаче структуры в качестве параметра функции требовалось указывать адрес структуры, а не ее саму. В связи с этим формальным параметром функции должен был служить указатель на струк-
Структуры, объединения и ссылочные типы данных 203 туру. В Турбо Сн н в стандарте Си ANSI разрешается передавать, изменять и возвращать структуры по значению. Тем не менее советуем программистам все-таки использовать указатели для передачи структур, с тем чтобы обеспечить совместимость программ с другими системами программирования Сн. Упомянутый способ передачи указателя иа структуру демонстрируется на листинге 7.1, где три веден интерфейс с пакетом программ работы с комплексными числами. В описании интерфейса имеются макро COMPLEX и структура для представления комплексных чисел. Здесь же приведены интерфейсы для семи операций - сложение ( add ), вычитание ( sub ), умножение ( mult ), деление ( div ), задание вещественной части ( assign_real ), задание мнимой части ( assignimag ) н печать комплексного числа ( print_complex ). Листинг 7.1. Интерфейс к пакету программ для работы с комплексными числами /* Интерфейс к пакету для комплексных чисел */ «define COMPLEX struct coaplex_type COMPLEX { float real; float imag; >; extern void add( COMPLEX »c3, COMPLEX »cl, COMPLEX »c2 >; extern void sub( COMPLEX »c3, COMPLEX »cl, COMPLEX »c2 ); extern void mult( COMPLEX "сЗ, COMPLEX »cl, COMPLEX »c2 ) extern void div( COMPLEX »c3, COMPLEX »cl, COMPLEX *c2 ); extern void assign_real< COMPLEX *c, float г ); extern void assignj.mag( COMPLEX »c, float i ); extern void print_complex( COMPLEX *c ); Программы пакета работы с комплексными числами собраны в отдельный файл и приведены на листинге 7.2. Существенно, что такой файл содержит ссылку на подстановочный файл complex, h. Для того чтобы скомпилировать код на листинге 7.2, компилятору требуется определение COMPLEX, содержащееся в подставляемом файле. Все параметры, соответствующие комплексным переменным, задаются в форме указателей на COMPLEX в соот-
204 Глам 7 аетствнн с рекомендациями, состоящими в том, чтобы параметры структурного типа передавались как указатели. Листинг 7.2. Реализация пакета программ работы с комплексными числами «include "complex.h" void add( COMPLEX »c3, COMPLEX »cl, COMPLEX »c2 > { c3 -> real = cl -> real + c2 -> real; c3 -> imag = cl -> imag ♦ c2 -> imag; > void sub( COMPLEX *c3, COMPLEX »cl, COMPLEX *c2 ) < c3 -> real = cl -> real - c2 -> real; c3 -> imag = cl -> iiiag - c2 -> imag; ) void mult( COMPLEX *c3, COMPLEX «cl. COMPLEX »c2 ) { c3 -> real = ( cl -> real ) • ( c2 -> real ) - ( cl '-> imag ) • < c2 -> imag ); c3 -> imag = ( cl -> imag ) • ( c2 -> real ) ♦ ( cl -> real ) • ( c2 -> Imag ); ) void div< COMPLEX »c3. COMPLEX «cl, COMPLEX »c2 ) { if ( c2 -> real II c2 -> imag I { c3 -> real = ( cl -> real ) * ( c2 -> real ) + ( cl -> imag > * ( c2 -> imag ) / ( ( c2 -> real ) • < c.2 -> real > - ( c2 -> Imag ) * ( c2 -> imag ) ); c3 -> imag = ( cl -> imag ) • ( c2 -> real ) - ( cl -> real ) * ( c2 -> imag ) / ( ( c2 -> real ) * ( c2 -> real > - ( c2 -> imag ) * ( c2 -> imag У ); } ) void assign_real( COMPLEX »c, float г ) { с -> real = г; ) void assign_imag( COMPLEX *c, float i )
Структуры, объединения н ссылочные типы данных 205 < с -> imag = i; ) void print_complex< COMPLEX »c ) ( printff "\nXf + J%f\ с -> real, с -> imag ); > На листинге 7.3 приведен короткий тестовый нример использования пакета программ работы с комплексными числами. Листинг 7.3. Тестовая программа для пакета работы с комплексными числами ^include "complex, h" main!) < COMPLEX nl, n2, n3; assign_real( &nl, 3 ); assign_imag( &nl, 4 ); assign_real( &n2, 2 ); assign_imag( &n2, 3 ); add( &n3, &nl, &n2 ); print_conplex( &n3 ); mult( &n3, &nl, 8.n2 ); print„complex! &n3 ); div( &n3. &nl, &n2 ); print_complex( &n3 ); printf( "\n" ); > Результат работы программы на листинге 7.3: 5.000000 +J7.000000 -6.000000 +J17.000000 6.923077 +J7.307693 Структуры обычно задаются следующим образом: struct coiplex_type < double real; double lHag; ) Byjoomplex;
206 Г лам 7 Имя переменной указывается вслед за описанием структуры. Структуры могут задаваться неполностью. Поле структуры может быть описано как указатель иа саму структуру. Подробнее об этом см. в разд. 7.3. 7.1.1. Битовые поля Целочисленные компоненты могут быть помещены в маленький объем памяти с использованием битовых полей. Битовые поля часто используются в машинозависимых приложениях, когда требуется работать с битовыми картами, описывающими конфигурацию аппаратуры. В^ некоторых реализациях Си для каждого битового поля разрешено использовать только беззнаковый тип. В Турбо Си для битовых полей можно применять как тип int. так и тип unsigned int. Поля целого типа представляются в виде двоичного дополнительного кода, где самый левый бит соответствует Знаку. Существенно, что в Турбо Си битовые поли располагаются от меньших номеров к большим. В других компиляторах с Си может быть принят иной порядок. Программируя иа (Турбо) Си, следует внимательно кодировать работу с битовыми полями, чтобы не столкнуться с проблемами мобильности! Пример работы с битовыми полями и пояснение к этому примеру приведены ниже: struct my_widget_type { int i : 2; unsigned J : 6; lnt : 3; unsigned k : 3; unsigned 1 : 2; > my_widget; Первое поле i имеет размер 2 бита. Второе поле j занимает шесть битов. Следующее поле не имеет имени. Вы видите здесь пример неполного описания структуры. Следующее поле к ограничено тремя- битами. Последнее поле I ограничено двумя битами. На рнс. 7.1 показано распределение памяти под пе-
Структуры, объединения н ссылочные типы данных 207 ременную my_widget. Листинг 7.4 иллюстрирует использование битовых полей. Витовое представление перемевной my_widget показано иа рис. 7.2. Программа выведет 47142 Листинг 7.4. Иллюстрация использования битовых полей «include <stdio.h> struct «y_Hidget_type < unsigned i : 2; unsigned J : 6; int : 3; unsigned k : 3; unsigned 1 : 2; > my_widget; main О { ■y_widget. 1 = 2; my_vidget.J =9; my_widget.k = 7; my_widget.l = 2; printf( "my_widget = Xu\n", iiy_Hidget >; > | 15 14 | 13 12 11 | 10 9 8 | 7 6 5 4 3 2 | 1 0 | 1 k безымянная J i Рис.7.1. Распределение памяти под переменную my_widgef. |10 (121 |000 1001001 |10| Рис. 7.2. Битовое представление переменной mywidget. 7.1.2. Инициализация структур Если структуры являются глобальными или описаны как static внутри функции, то их, как и массивы, можно инициализировать непосредственно в месте описания.
208 Гмм 7 Такая возможность иллюстрируется в примере на листинге 7.5. Задается структура RECORD (struct data_record). Она содержит три поли: lastname (строка), firstname (строка) и id_number (длинное целое). Как показано на листинге 7.5, описывается н инициализируется специфическая структура my_record. Начальные значения для каждой структуры задаются внутри набора скобок. Соответствие между константами и полями устанавливается по порядку записи. Это означает, что первая по порядку константа связывается с первым по порядку полем, втораи - со вторым и т. д. Параметром функции enterdata является адрес структуры RECORD. Память под строки first_name и lastname отводится внутри функции enterdata. Это гарантирует, что для размещения элемента указанной структуры будет, отведено ровно столько памяти, сколько нужно. С помощью функции print_record осуществляется вывод содержимого структуры, адрес которой передается в качестве параметра. Листинг 7.5. Инициализация структуры «include <stdio.h> «include <string.h> «include <alloc.h> «define RECORD struct data_record RECORD { char *last_name; Char *first_name; long idjiumber; >; RECORD иу.record = { "Ричард", "Уинер". 12345678 >; main О { extern void enter_data( RECORD «data ); extern void printj*ecord( RECORD «data ); print„record( &myjrecord );
Структуры, объединения и ссылочные типы данных 209 enter_data( &my„record ); print.record( &my_record ); printf( "\n" >; } void enter_data( RECORD "data ) { char info! 40 1; printf( "ЧпВведите фамилию : " ); scanf( "Xs", info >; data -> last_name = ( char * ) mallocl strlenl info ) + 1 ); strcpyf data ->. last_name, info ); printf( "ЧпВведите имя : " ); scanf( "As", info ); data -> first_name = ( char • ) mallocl strlent info ) + 1 ); strcpyf data -> firstлате, info ); printff "ЧпВведнте номер : " ); scanf( '"/.Id", bdata -> idjiumber »; > void print_record< RECORD «data ) I printf( "\п\пФамилия : Xs", data -> last_name >; printfl "ЧпИия : %s", data -> first_naiie ); printff "ЧпНомер : Hid", data -> idjiumber ); > 7.1.3. Массивы структур Комбинируя структуры н массивы, можно строить универсальные и гибкие структуры данных. Простой пример, подтверждающий эту мысль, приведен на листинге 7.6. Описывается массив структур database типа RECORD (struct data_record). Массив инициализируется значениями, приведенными ниже. Для снятия ссылки с полей lastname, firstname и id_number параметра database, передаваемого подпрограмме, используется операция -> . Программа выводит
210 Глам 7 Фамилия : Уинер Имя : Ирвинг Номер : 1 Фамилия : Уииер Имя : Марк Номер : 2 Фамилия : Уииер Имя : Эрик Номер : 3 Листинг 7.6. Массив структур (вариант 1) «include <stdio.h> «define RECORD struct data_record RECORD { char *last_name; char *first_name; long id_number; >; RECORD data_base( 3 J = < "Уинер", "Ирвин". 1, •Уинер", "Марк", 2, "Уинер". "Эрик". 3 >; main О { extern void print_records( RECORD *data_base, int size ); print.records( data_base, 3 ); printft "\n" ); > void print„records( RECORD »data_base, int size ) < int i; for ( i = 0; i < size; i++ ) < printft "\п\пФамилия : Xs", ( data_base + i ) -> last лаве ); printf( в\пИмя : Xs", ( data_base + i ) -> first_name •; printf( "ЧпНомер : Xld". ( data_base + i ) -> idjiumber );
Структуры, объединения и ссылочные типы данных 211 > ) На листинге 7.7 приведен еще один вариант реализации функции print_records. Результаты работы обеих программ совпадают. Листинг 7.7. Массив структур (вариант 2) «include <stdio.h> «define RECORD struct RECORD { char *last_name; char «first jiame; long idjiumber; }; RECORD data_basel 3 1 data_record = ( "Уинер". "Уинер", "Уинер", >; "Ирвин", "Марк", "Эрик". 1. 2, 3 main() { extern void print„records( RECORD «dataj>ase( 1. int size ); print_records( data_base, 3 ); printff "\n" ); > void print ^records( RECORD data_basef], int size ) ( int i; for ( i = 0; i < size; i++ ) { printff "\п\пФамилия : %s", data_base( i l.last_name ); printff "\пИия : Xs\ data_base( i ].first_name ); printff "ЧпНомер : Ш", data_base[ i l.id_number ); > )
212 Глааа 7 7.2. Объединения Используя объединения (union), можно в одной и той же области памяти размещать данные различных типов. Естественно, что в данный момент времени в памяти могут быть размещены значения только одного включенного в объединение типа. Посмотрите внимательно на тексты программ, приведенные иа листинге 7.8. Здесь определяется объединение ALTERNATIVE_DATA. Объединение содержит следующие поля: char si 4 ]; float г; int i; char ch; unsigned j; Программа выводит следующую строку: sizeofl data ) = 4 Размер памяти, требуемой для размещения объединения, определяется размером наибольшего поля. Для программы, приведенной на листинге 7.8, длина самого большого поля равна четырем байтам. Листинг 7.8. Первое знакомство с объединениями •include <stdio.h> •include <string.h> «define ALTERNATIVEJ3ATA union data_record ALTERNATIVEJ3ATA < char si 4 ]; float r; int i; char ch; unsigned J; ); ваШ) { ALTERNATIVEJATA data; printfJ "\nsizeof( data ) = Xd\n",
Структуры, объединения и ссылочные типы данных 213 sizeoft data ) ); > Объединении используются для преобразования типов. Для этого одному полю объединения присваивается значение, а из другого поля (оно расположено в той же области памяти, что и первое) значение считываетси. Результаты такого преобразования немобильяы я могут быть непредсказуемы. На листинге 7.9 показан пример использования такого сомнительного преобразования. Листинг 7.9. Сомнительная практика использования объединений для преобразования типов «include <stdio.h> «include <string.h> «define ALTERNATIVEJ3ATA union data_record ALTERNATIVEJ3ATA { char st 4 1; float r; int i; char ch; unsigned j; >; main() { ALTERNATIVEJ3ATA data; strcpy( data.s, "ABC" ); printf( "\nr = Xf\n\ data.г ); > Для создания эквивалента записей с вариантами, имеющимися в Паскале или Модуле-2, можно использовать объединения внутри структур. Такой прием рассмотрен на листинге 7.10. Структура' DATA содержит объединение с именем wage. Поскольку не имеет смысла хранить для одного человека и размер его почасовой ставки, и оклад (работник может быть либо почасовиком, либо штатным сотрудником), то в объединении wage поля hourty_wege (почасовой тариф) и annuelsalery (оклад) совмещены, что обеспечивает экономию памити.
214 Глам 7 В структуру включено поле tag, позволяющее узнавать, какое из двух полей (hourly wage нлн annualsalary) является активным в каждом конкретном случае. Не существует другого способа узнать, какое из полей активно. Листинг 7.10. Объединения внутри структур; зарплата и объединения «include <stdio.h> «define DATA struct data_type enum employee_type { salary, hourly ); DATA { char lastjnamef 10 1; char firstjnamef 10 1; enum employee_type tag; union { float hourlyjnage; float annual_salary; > wage; >; main() { DATA person; } 7.3. Связанные структуры ( В гл. 6 была рассмотрены указатели и способы динамического распределения памяти из кучи. Напомним, что все функции управления захватом к освобождением памяти собраны в библиотеке alloc, а интерфейс с этой библиотекой находится в файле alloc, h. Теория структур данных предоставляет нам широкий спектр моделей для построения таких поъторноиспользуе- мых компонентов программного обеспечения, как стеки, очереди, спнсин ■ деревья. Благодаря тому что перечисленные структуры данных имеют множество возможных при- иеиений, они являются важнейшими фундаментальными поня-
Структуры, объединения и ссылочные типы данных 215 тиямн в программной инженерии. В настоящем разделе будет рассмотрена конструкция линейных ссылочных структур, таких, как стеки, очереди я спнскн. Из-за ограниченности объема книги мы обсудим лишь базовые операции над каждой из таких структур. В гл. 9 будет описана реализация обобщенных списков, а также и нелинейных структур - деревьев. 7.3.1. Стек Стек часто называют структурой типа «первым вошел- последним вышел». Базовыми операциями со стеком являются push (втолкнуть) - добавить в стек новый элемент; pop (вытолкнуть) - удалить из стека элемент, втолкнутый туда последним; peek (выбрать) - взять элемент с «верхушки» стека, не изменяя при этом весь стек. Максимальное число элементов, которые можно разместить в стеке, не должно лимитироваться программным окружением. По мере вталкивания в стек новык элементов и выталкивания старых память под стек должна динамически дозапрашиваться и освобождаться. На листинге 7.11 приведен интерфейс с пакетом программ работы со стеком. Предполагается, что базовый тип элементов такого стека - целый. В гл. 9 мы покажем,, как строить обобщенные ссылочные структуры, в которых базовый тип может меняться. На примере описания структуры STACK видно, что можно описывать не все ее поля. Второе поле структуры STACK - указатель на структуру STACK. Такой способ рекурсивного определения, при котором структура содержит ссылку на саму себя, является вполне допустимым. Компилятор отводит под указатель next требуемое количество памяти независимо от того, на какой объект этот указатель ссылиется. В функциях push и pop используются двойные ссылки. Благодаря этому каждая из таких функций может возвращать в качестве результата своей работы указатель на
216 Глам 7 новый элемент STACK (используется передача параметров по ссылке). Входным параметром указанных функций является указатель иа стек, с использованием которого вычисляется и возвращается новый адрес элемента. После каждой операции вталкивания или выталкивания указатель иа связанный список меняется. Функции pop и push имеют еще один параметр, целого типа, передаваемый по ссылке - error. Если в стеке имеется хотя бы один элемент, то функции выдают error = 0. Если стек пуст, а следовательно, бессмысленно удалять нли считывать что-либо из стека, то error принимает значение 1. Листинг 7.11. Интерфейс со стеком /• Интерфейс со стекой •/ /• stack.h */ «define STACK struct stack STACK < int info; STACK «next; >; extern void push< STACK ««s, int item ); extern int pop( STACK **s, int «error ); extern int peek( STACK »s, int «error ); На листинге 7.12 приведены превратны,- реализующие работу со стеком. Внимательно изучим-теист-программы push. Память под новый элемент запрашивается из кучи с помощью первого оператора new_item = ( STACK • ) mallocl sizeoft STACK ) ); Преобразование типа ( STACK * ) нужно для того, чтобы привести, указатель, выдаваемый функцией malloc, к виду, при котором он может ссылаться на STACK. Для вычисления числа байтов, требуемых для размещения структуры, служит встроенная функция sizeof. В следующей строке программы new_item -> info = item;
Структуры, объединения и ссылочные типы данных 217 полю info структуры присваивается значение переменной item. Операция снятия ссылки -> использована здесь потому, что new_item - это указатель нв стек STACK. Далее прн помощи оператора newjtem -> next = *s; элемент new_item присоединяется к голове связаного списка *s. И наконец, с помощью оператора *s = new_item; указателю newitem присваивается значение адреса головы списка *s, и этот указатель возвращается в качестве результата функции. Функция pop, перед тем как сделать попытку обратиться к списку, проверяет значение *s. Если значение равно 0, то сцисок пуст, и в этом случае переменной error присваивается значение 1. Листинг 7.12. Реализация пакета программ работы со стеком /• Реализация стека */ /* stack.с •/ «Include <alloc.h> «include "stack.h" void pushf STACK «""s, int item ) i STACK •newjtes»; new_item = ( STACK * ) ша11ос( sizeof( STACK ) ); new_item -> info = item; new_item -> next = »s; *s = newjtem; ) int pop( STACK «""s, int «error ) { /•error = 0, если операция POP выполнена успешно, иначе = 1 */ STACK •oldjtem = *s; int old_info = 0; if < «s >
218 Гла»а 7 { old_info = oldjtem -> info; •s = ( »s ) -> next; free( old_item ); •error = 0; ) else •error = 1; return ( old_info ); > int peek( STACK »s, int «error ) < /• error = 0, если в стеке не меньше одного элеиента error = 1, если в стеке нет элеиентов •/ if ( »s ) { •error = 0; retnrn ( *s ) -> info; ) else { •error = 1; return 0; } На Листинге 7.13 приведена короткая тестовая программа, иллюстрирующая работу со стеком. Существенно, что стековые переменные si и s2 описаны как глобальные. Это гараитнрует, что каждая переменная будет инициализирована нулем (в гл. 9 описываются преобразования и классы памяти). Если бы переменные были описаны как локальные, то начальные значения их были бы не определены, что в свою очередь могло привести к ошибке в функции pop. В тестовой программе не используется значение переменной error. Результат работы программы с листинга 7.13 выглядит следующим образом: peek( si ) = 12 реек( si ) = 13 реек( si ) = 14
Структуры, объединения н ссылочные типы данных 219 peek( si ) = 15 рор( &s2 ) = 12 рор( &s2 ) - 13 рор( &s2 ) - 14 рор( &s2 ) = 15 Листинг 7.13. Тестовая программа для пакета стеком работы со /* Тестовая програима, использующая переиеиные типа стек */ «include <stdlo.h> «include "stack.h" STACK «si. »s2; mainO { int error; " pushf &sl, 12 ); printf( "\npeek( si ) = %d push( &sl, 13 ); printf( "\npeek( si > = %d push( &sl, 14 ); printf( "\npeek( si ) = Xd push! &sl, 15 ); printfl "\npeek( si ) = %d pusht &s2, pop( &sl push( &s2, pop( &sl push( &s2, pop( &sl, fcerror ) ); push( &s2, pop( &sl, &error ) ); printf( "\npop( &s2 ) = Xd", pop( &s2. &error ) ) printf( "\npop( &s2 ) = Xd", pop( &s2, &error ) ) printft "\npop( &s2 ) = %d", pop( &s2, &error > peek( &sl, &error ) > peek( &sl, &error > ) peek( &sl, &error ) > peek! &sl, &error > fcerror ) ); &error ) ); printfl "\npop( &s2 ) = y.d\n", pop( &s2, &error ) ); ) 7.3.2. Очереди Очередью называют структуру данных, организованную по принципу «первым вошел - первым вышел». Базовыми операциями над очередью являются insert (добавить) - добавить в очередь новый элемент; take_off (удалить) - удалить из очереди первый элемент.
220 Гяа»а 7 Как н для стека, максимальное число элементов в очереди не должно лимитироваться используемым программным обеспечением. Память дли очереди должна запрашиваться и освобождаться динамически по иере того, как в рчередь добавляются новые элементы и из очереди удаляются элементы. Предполагается, что базовым типом элементов очереди является тип int. На листинге 7.14 представлен интерфейс с программами обслуживания очереди. Определяется структура QUEUE. Здесь же описываются интерфейсы с функциями insert и takeout. Функция take_off имеет параметр error, передаваемый по ссылке. Значение параметра равно нулю, если в очереди содержится один или более элементов (целых чисел), и равно единице, если очередь пуста. Листинг 7.14. Интерфейс с пакетом программ работы с очередью /* Интерфейс с пакетом програии работы с очередью */ /•Файл queue.h •/ «define QUEUE struct queue QUEUE { int info; QUEUE «next; >; extern void insert! QUEUE **q, int item ); extern int take_out( QUEUE ««q, int «error ); На листинге 7.15 приведены программы, реализующие работу с очередью. Рассмотрим подробно функцию insert. Описывается локальная переменная current типа указатель, и она инициализируется значением *q. Описывается локальная переменная previous типа указатель, и она инициализируется значением 0. В цикле типа WHILE while ( current ) { previous = current; current = current -> next; }
Структуры, объединения н ссылочные типы данных 221 связанный список просматривается до конца. Указатель previous содержит адрес последнего элемента QUEUE в списке. В фрагменте программы new_node =• ( QUEUE • ) mallocl sizeofl QUEUE ) ); newjode -> info = item; if ( previous ) < newjode -> next = previous -> next; previous -> next = newjode; ) else < •q = newjode; ( »q ) -> next = 0; ) отводится память под очередь QUEUE new node, полю info новой структуры QUEUE присваивается значение item, переменной previous присваивается значеине указателя на new_node, a newnode устанавливается и 0, если очередь ие пуста, и принимает значение указателя иа новый элемент в голове очереди, если очередь прежде была пустой. Код для функции takeout сходен с кодом для функции pop, приведенным на листинге 7.12. Листинг 7.15. Реализация пакета программ работы с очередью /* Реализация очереди */ /* Файл queue.с */ •include <alloc.h> •include "queue.h" void insert! QUEUE ««q, int item ) < QUEUE «current = *q; QUEUE «previous =0; QUEUE «newjode; while ( current ) { previous = current; current = current -> next;
222 Глм« 7 > newjode = ( QUEUE • ) malloc( sizeof( QUEUE ) ); newjode -> info = item; if ( previous ) { newjode -> next = previous -> next; previous -> next = newjiode; > else { •q = newлode; < »q ) -> next = 0; > ) int take_out< QUEUE *»q, int «error ) { int value = 0; QUEUE «oldjieader = *q; if ( »q ) { value = oldjieader -> info; •q = ( *q ) -> next; free( oldjieader ); •error = 0; > else •error = 1; return value; ) На листинге 7.16 приведена короткая тестовая программа, использующая очередь. Результатом ее работы будет remove( &q2 ) = 12 remove( &q2 ) = 13 remove( &q2 ) = 14 remove( &q2 ) = 15 Листинг 7.16. Тестовая программа, использующая очередь /* Тестовая программа, использующая переменные типа очередь •/
Структуры, объединения и ссылочные типы данных 223 /• Файл «Include #iDelude «include queue.с •/ <stdio.h> <alloc.h> "queue.h" QUEUE •ql. »q2; main О int error; Insert( insert( insert( insert( insert( insert( insert( insert( printf( printf( printf( printf( &ql, 12 ) &ql. 13 ) &ql, 14 ) &ql, 15 ) &q2, take_out( &q2, take_out( &q2, take_out( &q2, take_out( "\nremove( &q2 "\nremove( &q2 "\nremove( &q2 "\nremove( &q2 take_out( ( lq2, 1 &ql, berror ) ) &ql, terror ) ) &ql, terror ) ) ftql, terror ) ) ) = Xd", take_out( ) = Xd". take_out( ) = Xd", take_out( ) = Xd\n". terror ) ); &q2. &q2. &q2, terror ) ) terror ) ) terror ) ) 7.3.3. Связанные списки Списки являются весьма популярными структурами и применяются для представления абстрактного аппарата поиска элемента. Элементы в списках обычно располагаются в возрастающем или убывающем порядке. В настоящем разделе описываются интерфейс и реализация связанных списков, элементы которых располагаются в порядке возрастания. Стеки и очереди являются специальными разновидностями обобщенных списков. Простой список может быть определен прн помощи следующих операций: insert - Добавить новый элемент в список, сохраняя установленный порядок следования. take_out - Удалить элемент из списка, сохранив установленный порядок следования. Если элемент отсутствует, то функция не выполняет никаких действий.
224 Г лам 7 is_present - Определить, содержится ли в списке заданный элемент. Если содержится, то возвращается значение единица, в противном случае возвращается значение нуль. display - Вывести все по порядку элементы списка. destroy - Освободить память, занимаемую списком. Как для стека н очереди, так и для связанного списка не должно существовать ограничения на максимальное количество его элементов, налагаемого программным окружением. Память под список должна динамически запрашиваться и освобождаться по мере того, как элементы добавляются н удаляются. Существует множество способов реализовать связанный список. Это одиоиаправлеииые списки, закольцованные списки, двунаправленные списки и т.д. Есля читатель желает подробно ознакомиться с различными вариантами реализации связанных списков, советуем ему обратиться к кинге «Data Structures Using Modula-2» (Sincovec and Wiener, 1986). На листинге 7.17 приведен интерфейс с программами работы со связанными списками. Реализован простой однонаправленный список. Для того чтобы сделать пример работы со списком более реалистичным и для лучшей демонстрации синтаксических особенностей задания структур и объединений предположим, что каждый элемент списка представляет собой достаточно сложную структуру. Пример приведен на листинге 7.17. Список содержит вложенные структуры и вложенные объединения. Возможности здесь безграничны! Вначале определяются структуры STAFF, STUDENT н PROFESSOR, содержащие каждая по два числа (поля). Далее описывается перечислимый тип NODETYPE. С помощью этого типа будет описываться поле списочной структуры, указывающее на тип элемента поля. Списочная структура (LIST) включает пять обычных полей: last name (последнее нмя), firstname (первое имя), age (возраст), next (следующее) и tag (флаг), а также объединение nodetag. Это объединение состоит из трех членов: student (студент), professor (профессор) и staff (персонал). Каждая переменная типа LIST может со-
Структуры, объединения н ссылочные типы данных 225 держать элементы только одного типа из объединения. Такая структура, похожая иа запись с вариантами, позволяет экономно расходовать память, поскольку она занимает пространство, требуемое для размещения первых пяти обычных элементов структуры, плюс пространство, занимаемое элементом максимальной длины нз объединения. В файле list, h кроме описания структуры данных для списка задай еще н интерфейс с пятью функциями: insert, display, is_present, take_out и destroy. Листинг 7.17. Интерфейс с программами работы со связанными списками /* Интерфейс с программами работы со связанным списком персонала университета •/ /» Файл list.h »/ «define STAFF struct stuff_type STAFF { int years_of_service; float hourly_wage; >; ♦define STUDENT struct student_type STUDENT { float grade_pt_average; int level; >; «define PROFESSOR, struct prof_type PROFESSOR { int deptjumber; float annual_salary; >; «define NODEJTYPE enum node_type typedef NODEJTYPE { student, professor, staff >; «define LIST struct list LIST { char lastjiamei 10 1; char first jiamei 10 1; int age; LIST «next; ft П„. 7(\C
226 Глава 7 NODE.TYPE tag; union { STUDENT student; PROFESSOR professor; STAFF staff; > node_tag; >; extern void insert( LIST ••1st, LIST «item ); extern void display( LIST »lst ); extern int is_present( LIST «1st, LIST «item ); extern int take_out< LIST ••lst, LIST «item ); extern void destroy_list( LIST «"lst ); На листинге 7.18 представлена реализация программ работы со связанными списками. Функция create_node (создать элемент), описанная как static LIST» create_node( LIST «item ) спрятана от загрузчика н других программ системы, поскольку задана статически. Ее назначение - отводить память под новый элемент списка и передавать данные, помещенные по адресу item (указатель на структуру), в новый элемент. В функции используется множественное присваивание: •node = «item; для пересылки всей информации, расположенной по адресу item, в область памяти по адресу «node. Для выполнения той же работы можно воспользоваться приведенным ниже н гораздо менее наглядным фрагментом кода strcpy( node -> last_name, item.lastjname ); strcpy( node -> first_name, item.first_name ); node -> age = item.age; node -> tag = item.tag; switch ( node -> tag ) { case staff: node -> node_tag. staff. years_of„service =
Структуры, объединения н ссылочные типы данных 227 item -> node_tag. staff, years_of„service; node -> node_tag.staff.hourly_wage = item -> node_tag. staff.hourly_wage; break; case professor: node -> node _tag. professor, deptjiumber = item -> node_tag.professor.deptjiumber; node -> node_tag. professor. annual_salary = item -> node_tag.professor. annual_salary; break; case student: node -> node_tag.student.grade_pt_average = item -> node_tag.student.grade_pt_average; node -> node_tag. student, level = item -> node_tag.student.level; Цикл типа WHILE в функции destroyjist while ( current_node != 0 ) { previous jiode = current jiode; currentjiode = current_node -> next; free( previousjiode ); > использован для освобождения памяти, занимаемой элементами списка. После завершения цикла WHILE значение переменной «1st, передаваемой по ссылке, оказывается равным нулю. В функции takeout в цикле типа WHILE перебираются элементы списка до тех пор, пока либо не будет достигнут конец списка, либо не будет установлено соответствие между строкой в поле last_name переменной item и полем last_name в текущем элементе current_node. Если цикл прекращается из-за того, что элемент previous_node равен нулю, то изменяется указатель на голову строки «1st и память, занимаемая старым головным элементом списка, освобождается (исключается всегда элемент, находящийся в голове списка). Другими словами, операторы previous_node ~> next = currentjiode -> next; free( current_pode ); 8*
228 Глава 7 продвигают указатель по Слиску и освобождают память нз-под удаляемого элемента. В функции insert указатель LIST current_node инициализируется значением *lst н переменная previous_node инициализируется нулем. В цикле типа WHILE просматривается список либо до конца, либо до тех пор, пока значение поля last_field переменной current_node ие перестанет быть меньше, чем поле lastfield добавляемого элемента. Динамически запрашивается память под указатель new node н даииые из элемента item пересылаются по адресу new_node. Новый элемент new_node подключается к списку при помощи оператора newjiode -> next = current_node; Если добавляемый элемент является первым в первоначально пустом списке (previous_node равен нулю), то указатель иа голову списка «1st устанавливается на new_node. Иначе говоря, указатель продвигается по списку при помощи оператора previousjiode -> next = newjiode; Самая большая программа требуется для функции display, поскольку' ее работа существенно зависит от типа выводимого элемента. После того как будут выведены поля last_name, first_name и age, прн помощи переключателя определяется, как выводить остальные поля. Предположим, что значение поля tag есть student, т.е. мы должны распечатать информацию о студенте. Попробуйте разобраться в том, каким образом осуществляется доступ к глубоко вложенной информации grade_pt_average. Во фрагменте программы printfl "ЧпСпециализация : %.2f", current -> node_tag.student.grade_pt_average ); printf< "\nKypc : %d\n", current -> node_tag.student, level ); выводится номер специализации и номер курса. В выражении current -> node_tag.student.grade_pt.average );
Структуры, объединения и ссылочные типы данных 229 снимается ссылка с указателя current и выбирается член объединения node_tag соответствующей структуры, затем осуществляется доступ к члену объединения student, н наконец, обеспечивается доступ к элементу grade_pt_average структуры student. Листинг 7.18. Реализация пакета программ работы со связанными списками «include <string.h> «include <stdio.h> «include <alloc.h> «include "list.h" static LIST» createjiodef LIST «item ) { LIST «node; node = ( LIST • ) malloct sizeoff LIST ) ); •node = «item; return node; > void destroy_list( LIST ••lst ) { LIST •currentjiode = «1st; LIST •previousjiode = 0; while ( currentjode != 0 ) { previousjiode = currentjiode; currentjode = currentjnode -> next; free( previousjode ); > •lst = 0; > int take_out( LIST ••1st, LIST •item ) { LIST •currentjiode = •1st; LIST «previousjiode = 0; while ( current jiode != 0 && strcmpC current „node -> lastjiame, item -> last_name ) != 0 ) { previousjiode = currentjiode; current jode = current jiode -> next; }
230 Гла.а 7 if ( current_node != 0 && previousjiode != 0 ) { •1st = current jiode -> next; free( current_node ); ) else if ( current jiode != 0 && previousjode != 0 ) { previous_node -> next = current_node -> next; free( current jiode ); > > void insert( LIST **lst, LIST «item ) { /» Фамилия используется как ключевое поле в списке */ char keyf 10 1; LIST «currentjnode = «1st; LIST »previous_node = 0; LIST «newjiode; strcpy( key, item -> lastjame ); while ( current jiode != 0 && strcmp( current_node -> last_name, key ) < 0 ) { previous_node = current_node; current jiode = current jode -> next; > new_node = create_node( item ); new_node -> next = current jiode; if ( previousjiode == 0 ) • 1st = newjiode; else previous_node -> next = new_node; > void display( LIST »lst ) { LIST «current = 1st; while ( current ) { printf( "\nXs, V.s", current -> lastjiame, current -> first_name ); printf( "ХпВозраст = "/.d", current -> age ); switch ( current -> tag ) { case student:
Структуры, объединения и ссылочные типы данных 231 printff "ЧпСнециализация : Й.2Г', current -> node_tag. student.grade_pt.average ); printf( "\nKypc : V.d\n", current -> node_tag.student, level ); break; case professor: printf( "ХпНомер кафедры : %d", current -> node_tag.professor.dept_number ); printf( "ХпОклад : f/..2f\n", current -> node_tag. professor. annual_salary ); break; case staff: printfC "\пСрок службы : Zd", current -> node_tag. staff.years_of.service ); prlntf( "ХпПочасовой тариф : «7..2f\n", current -> node_tag.staff. hourly_wage ); > current = current -> next; > > int is_present( LIST «1st, LIST «item ) { /* Фамилия используется как клсчевое поле в списке */ LIST «current_node = 1st; while ( current_node && strcmp( item -> last_name, current_node -> last_name ) != 0 ) if ( strcmp( item -> lastjiame, current „node ~> lastjiame ) != 0 ) current„node = current_node -> next; return ( current jiode != 0 ); > На листинге 7.19 приведена тестовая программа, использующая большинство функций из пакета программ работы со списками. При помощи следующих операторов LIST «iteml = ( LIST • ) malloc( sizeof( LIST ) ); LIST *item2 = ( LIST * ) malloc( sizeof( LIST ) ); LIST »item3 = ( LIST • ) malloc( sizeof( LIST ) );
232 Глава 7 инициализируются три указателя на списки: iteml, item2 и item3. В список добавляются три структуры, значения полей которых задаются в программе. Можно было бы использовать всего одну структуру, унифицировав работу с ней, но для простоты мы будем работать с тремя разными. Дополнительные действия, выполняемые в программе, очевидны. Программа на листинге 7.19 выведет следующий текст: Evans, Henry Возраст = 19 Специализация : 3.10 Курс : 1 Jones, Richard Возраст = 56 Номер кафедры : 7 Оклад : $48321.00 Smith, Robert Возраст = 26 Стаж : 3 Почасовой тариф : $5.25 Элемент 1 присутствует в списке. Jones, Richard Возраст = 56 Номер кафедры : 7 Оклад : $48321.00 Smith, Robert Возраст = 26 Стаж : 3 Почасовой тариф : $5.25 Smith, Robert Возраст = 26 Стаж : 3 Почасовой тариф : $5.25
Структуры, объединения и ссылочные типы данных 233 Листинг 7.19. Программа тестирования пакета программ работы со списками «include <string.h> «include <stdio.h> «include <alloc.h> «include "list.h" LIST *my_l.ist; main() < LIST «iteml = ( LIST * ) mallocf sizeof( LIST ) >; LIST «item2 = ( LIST * ) mallocf sizeof( LIST > >; LIST «item3 = ( LIST * ) mallocf sizeoff LIST ) ); strcpy( iteml -> last_name, "Smith" ); strcpy( iteml -> first_jiame, "Robert" ); iteml -> age = 26; iteml -> tag = staff; iteml -> nbde_tag. staff.years_of.service = 3; iteml -> node_tag. staff. hourly_wage = 5.25; insert( &my_list, iteml ); strcpy( item2 -> last_name, "Jones" ); strcpy( item2 -> first_name, "Richard" ); item2 -> age = 56; item2 -> tag = professor; item2 -> node_tag.professor. dept_number = 7; item2 -> node_tag. professor.annual_salary = 48321.0; insert( &my_list, item2 ); strcpyl item3 -> last_name, "Evans" ); strcpyl item3 -> firstjiame, "Henry" ); ite»34 -> age = 19; item3 -> tag = student; item3 -> nodeitag. student, level = 1; item3 -> node_tag.student.grade_pt_average = 3.1; insert( &my_list, item3 ); display( my_list ); if ( is_present( my_list, iteml ) ) printf( "ЧпЭлемент 1 присутствует в списке.\п" ); else printf( "ЧпЭлемент 1 отсутствует в списке.\n" ); take_out( &my_list, item3 ); display( my_list ); take_out( &my_list, item2 ); display( my_list );
234 Глава 7 take_out( &my_Iist, iteml ); display( my_list ); printf( M\n" );
Глава 8 Функции, области видимости и классы памяти Функции - это фундаментальные логические элементы, служащие для выполнения действий, связанных с решением поставленной задачи. С первой главы книги в примерах программ использовались функции. В настоящей главе мы более подробно познакомимся с правилами построения функций и задания их параметров. Будут рассмотрены классы памяти и вопросы доступности и видимости. Мы изучим указатели и обсуднм некоторые приложения. 8.1. Представление функций Представление функции включает • Тип значения, возвращаемого функцией (если значение ие возвращается, то тнп функции - void). • Число и тип формальных параметров. • Код (тело) функции, который должен быть выполнен прн вызове функции. • Указание о видимости функции вне файла, где она задается. • Локальные переменные, которые могут маскировать глобальные переменные. Следует различать описание н представление функции. Описание делает возможным доступ к функции (помещает ее в область видимости), про которую известно, что она внешняя (external). Представление задает действия, выполняемые функцией при ее вызове. Как уже отмечалось, в Турбо Си реализован проект стандарта ANSI для прототнпировання функций Си. Компилятор системы предоставляет пользователям услуги по проверке наличия ошибок, связанных с вызовом функций и контролем параметров. Изучая листинги 8.1 и 8.2, читатель может сравнить новый стандарт прототнпировання функций и контроля использования их параметров со старым способом задания заголовков функций без контроля параметров. Представляется функция increment от двух парамет-
236 Глава 8 ров вещественного типа number и amount. Функция increment вызывается из первых двух операторов вывода на печать, стоящих в функции main. Обрабатывая вызов функции increment ( г, 2 ) компилятор переведет литеральную константу 2 (целого типа) в требуемый по описанию функции тип с плавающей точкой (2.0) и лишь затем выполнит вычисления, дающие ответ 18.2. Обрабатывая второй вызов функции increment ( г, 'А' ) компилятор переведет литеральную константу 'А' (целого типа) в требуемый по описанию функции тип с плавающей точкой (65.0) и лишь затем выполнит вычисления, дающие ответ 81.2 . Если на листинге 8.1 убрать комментарии к двум операторам вывода, то компилятор Турбо Си выдаст следующие диагностические сообщения: Turbo С Version 1.0 Copyright (с) 1987 Borland International test:с Error test.с 17: Too few parameters in call to 'increment' in function main (Ошибка в test.с 17: Недостаточно параметров в вызове функции increment из функции main) Error test.с 19: Extra parameters in call to 'increment' in function main (Ошибка в test.с 19: Лишние параметры в вызове функции increment из функции main) «** 2 errors in Compile *** (*** 2 ошибки при компиляции ***) Листинг 8.1. Сравнение нового и старого способов прототнпировании функций : новый способ /* Прототипирование функций, принятое в Турбо Си и новом стандарте ANSI »/ #include <stdio.h> float increment( float number, float amount );
Функции, области «идимости и классы памяти 237 ( return ( number + amount ); ) main() < float r = 16.2; printf( "\nincrement( r, 2 ) = X.lf", increment( r, 2 ) ); printf( "\nincrement< r, 'A' ) * X.lf", increment( r, 'A' ) ); /* printf( "\nincrement( r ) = X.lf", Increment( r ) ); printft "\nincrement( r, 2.0, 3.0 ) = X.lf", increment( r. 2.0, 3.0 ) ); */ > На листинге 8.2 представлен тот же самый код, что m на листинге 8.1, но используется старый способ задания заголовков функций. Самое важное, что компилятор Турбо Ся при анализе кода не выдаст сообщения об ошибках. При использовании старого способа задания заголовков функций контроль типов параметров отключается. Программа на листинге 8.1 напечатает increment (г, 2 ) = 18.2 increment ( г, 'А' ) = 81.2 Программа на листинге 8.2 напечатает increment ( г, 2 ) = 2.8 Increment ( г, 'А' ) = 2.8 increment ( г ) = 2.8 increment ( г, 2.0, 3.0 ) =2.8 Последние результаты абсолютно бессмысленны. Безусловно, бессмысленно и то, что функции, требующей для своей работы два параметра, передаются одно или три значения. Программа на листинге 8.2 была скомпилирована и исполнена с помощью компилятора Си версии 4.0 фирмы
238 Глава 8 Microsoft. Компилятор ие обнаружил ошибок в программе. Результаты работы программы при использовании средств Microsoft следующие: increment ( г, 2 ) = 16.2 increment (г, 'А' ) = 16.2 increment ( г ) = 16.2 increment ( г, 2.0, 3.0 ) = 18.2 Как н большинство компиляторов с языка Си, компилятор фирмы Microsoft сразу после его создания еще не. поддерживал новый предварительный стандарт ANSI по про- тотнпированню функций. Сравнение работы одной и той же программы, подготовленной с помощью разных компиляторов, при использовании старой формы задания заголовков функций и отсутствии контроля за параметрами показывает, что получаются непредсказуемые результаты. По мнению автора, контроль параметров с использованием новейшего механизма прототипирования, реализованного в Турбо Си на основе новейшего стандарта ANSI, является наиболее удачным завоеванием языка, сближающим его с такими сильно типизированными языками, как Паскаль и Модула-2. Листинг 8.2. Сравнение нового и старого способа протототипироваиия функций: старый способ #include <stdio.h> float increment( number, amount ); float number, amount; { return ( number + amount ); ) mainO { float r = 16.2; printf( "\nincrement( r, 2 ) = X.lf", increment( r, 2 ) ); printf( "\nincrement( r. *A' ) = X.lf", increment( r, 'A' ) ); printf( "\nincrenent( r ) = X.lf", increment( r ) );
Функции, области видимости и классы памяти 239 printff "\nincrement( r, 2.0, 3.0 ) = X.lf\n", increment( г, 2.0, 3.0 ) ); ) Если в описании функции ие указывается ее тип, то по умолчанию принимается тип int. В качестве результата функция не может возвращать массив, но может возвращать указатель на массив. Единственный спецификатор класса памяти, который может присутствовать в наборе формальных параметров, - *го спецификатор register (регистр). Подробнее о спецификаторах класса памяти сказано в разд. 8.4. В теле любой функции может содержаться выражение return; не вырабатывающее значения. Если указано, что функция возвращает значение типа void, то ее следует вызывать так, чтобы возвращаемое значение не использовалось. Иначе говоря, такую функцию нельзя применять в правой части выражения. Все программные системы, написанные на (Турбо) Си, должны содержать функцию main, являющуюся входной точкой любой системы. Если функция main отсутствует, то загрузчик не сможет собрать программу. В гл. 6 говорилось о том, что в функции main могут использоваться параметры, передаваемые из командной строки. 8.2. Передача параметров В (Турбо) Си все параметры функции, за исключением параметров типа указатель и массивов, передаются по значению. При передаче параметра по значению в функции создается локальная копия, что приводит к увеличению объема используемой памяти. При вызове функции в стеке отводится память для локальных копий параметров, а при выходе из функции эта память освобождается. Рассмотрен- ,иый способ использования памяти не только требует дополнительного пространства, но и отнимает часть времени счета. На листинге 8.3 приведен пример программы, демонстрирующей, что при вызове функции копии создаются для параметров, передаваемых по значению, а для параметров,
240 Глава 8 передаваемых по ссылке, - не создаются. У функции test function - два параметра first и second, передаваемые по значению, н один параметр third, передаваемый по ссылке. Термин сссылка» здесь означает ссылку иа область памяти. Поскольку параметр third является указателем на тип int. то ои, как и все параметры типа указатель н массивы, передается по ссылке. После того как переменная «third в теле функции test_function увеличивается на единицу, новое значение присваивается переменной с, память под которую отведена в функции main. Так получается значение 25, являющееся результатом работы функции. Программа на листинге 8.3 напечатает Адрес а равен FEC6 Адрес b равен FEC8 Адрес с равен FECA Адрес first равен FEC0 Адрес second равен FEC2 Адрес third равен FECA Значение с = 25 Листинг 8.3. Параметры, передаваемые по значению и по ссылке «include <stdio.h> void test_function( int first, int second, int «third ) { printf("\nАдрес first равен Хр", bfirst ); printf("\nАдрес second равен Яр", &second ); printf("ЧпАдрес third равен Хр", third ); •third += 1; ) main() { int a, b; int с = 24; printf("\nАдрес а равен %р", &a ); printf("ЧпАдрес b равен Хр", &b );
Функции, области видимости и классы памяти 241 prlntf("ЧпАдрес с равен Хр", &с ); test_function( a, b, &с ); printf( "ЧпЗначение с = 54d\n™, с ); > В качестве параметров функции можно использовать выражения. Соответствующий пример приведен на листинге 8.4. Программа напечатает а = 35 с = 20 Листинг 8.4. Использование результата выражения в качестве параметра функции «include <stdio.h> void function_test( int a ) { printf("\na = Xd", a ); ) main() { int с = 3, d = 4, e = 5, f = 6; function_test( f=(c=d*e)+15); printf("\n\nc = Kd\n", с ); } Как уже отмечалось в гл. 6, при задании массива в качестве параметра функции передается адрес первого элемента массива. Если в теле функции заменяются значения элементов массива, то изменяется непосредственно сам передаваемый массив. Если в описании функции задано, что параметр передается по ссылке (т.е. он описан как указатель на тип) то в качестве лараметра при вызове функции передается адрес переменной. Соответствующий пример приведен на листинге 8.3 н во многих программах на листингах гл. 6.
242 Глава 8 8.3. Указатели на функции Можно ли в качестве параметра функции задавать функцию? Может ли функция быть передана в другую функцию н трактоваться последней как данные? Технически это невозможно, но практически - разумно. Указатель на функцию может быть описан как параметр и передаваться в функцию наряду с другими даннымн. Так можно решнть задачу, поставленную в начале раздела, и передавать данные н функции другим функциям. Прн нспользованин указателя в вызываемой функцнн должна быть снята ссылка на передаваемую функцию. Разрешается определять массивы функций. Указатели на функции могут быть компонентами структур. Массивы функций удобно применять прн реализации систем, управляемых с помощью меню. Каждому пункту меню ставится в соответствие реализующая его функция, указатель на которую помещается в массив. После того как пользователь выбрал из меню интересующее его действие, по индексу, соответствующему такому выбору, из массива выбирается функция, реализующая действие. Рассмотрим довольно странную синтаксическую конструкцию, требуемую для описания указателя на функцию. Она выглядит следующим образом: typedef void ( *menu_action ) О; Здесь идентификатор menuaction определен как указатель на функцию, не имеющую параметров н возвращающую тип void. Еслн бы определение типа было задано по-другому: typedef void *menu_action О; то значение конструкции было бы совсем другим. В последнем случае идентификатор menu_action задавал бы имя функцнн, возвращающей указатель на тип void. Снмвол ссылкн * имеет более иизкнй приоритет, чем операция (), н поэтому круглые скобки следует использовать для задания того, что идентификатор menu_action является указателем на функцию, возвращающую тип void, а не указателем на тип void. Теперь рассмотрим еще одно определение типа: typedef float ( «integrand ) ( float г );
Функции, области видимости и классы памяти 243 Здесь сообщено, что integrand - это указатель на функцию с одним параметром тнпа float, возвращающую значение типа float. Такое определение типа можно использовать, например, для написания универсальной функции интегрирования integral, заголовок которой имеет следующий внд: float integral( Integrand f, float lower_limit, float upper_limit ); Первым параметром функции integral является функция f типа integrand. Это означает, что f - указатель на любую функцию от одного вещественного параметра, возвращающую тип float. Если функция myf unction определена так: float my_function( float г ) < return г * г - 3.0; } то вызов функции integral будет выглядеть следующим образом: float answer = integral( my_function, 0.0, 5.0 ); Имя функцнн my_function выступает в качестве указателя на эту функцию, и поэтому не требуется указания ссылкн. На листинге 8.5 приведена простая программа, управляемая с помощью меню на основе массива указателей иа функции. Тип menu_action является указателем на функцию, возвращающую тип void и не имеющую параметров. С помощью оператора nenu_action control! 6 1; задается массив из шестн указателей. В теле функции build_table каждому элементу массива присваивается значение указателя на соответствующую функцию. В нашем примере каждая из таких функций печатает простое сообщение. В функции main выводится меню, и пользователю пред-
244 Глава 8 лагается либо выбрать один из пунктов меню, лнбо завершить работу программы. Выбор, сделанный пользователем, активизирует одну нз шести функций при помощи оператора ( «control[ choice - 1 1 ) О; Приведенный прием является удобным при разработке больших программ, управляемых с помощью меню. Добавление в такую программу новых возможностей требует лишь включення в массив указателей программы build_table имен новых функций. Листинг 8.5. Управление программой с помощью меню и массива указателей на функции «include <stdio.h> «include <util.h> typedef void ( »menu_action ) (); menu_action control! 6 1; /» Внимание: управляющая таблица может быть инициализирована статически. »/ mainO < extern void build_table ( void ); int choice; build_table(); do < clrscreent); printf< printf( printf( printft printf( printf( printft "\nl "\n2 H\n3 "\n4 "\n5 "\n6 "\n7 -> Пункт 1 -> Пункт 2 -> Пункт 3 -> Пункт 4 -> Пункт 5 -> Пункт 6 -> Выход и printft "\n\n Введите номер пункта: " ); scanft "Xd", &choice );
Функции, области видимости и классы памяти 245 if ( choice >= 1 && choice <= 6 ) ( «control[ choice - 1 1 ) О; else break; } while ( 1 ); printf("\n" ); /* Бесконечный цикл */ void build_table( void ) < extern void extern void extern void extern void extern void extern void menu_item_l( void ) menu_item_2( void ) menu_item_3( void ) menu_item_4( void ) menu_item_5( void ) menu_item_6( void ) control! 0 1 = menu_item_l control! control! control! control! control! 1 1 = menu_item_2 2 1 = menu_item_3 3 1 = menu_item_4 4 1 = menu_item_5 5 1 = menu_item_6 } void menu_item_l( void ) { printf( "ЧпВыполиилось действие по пункту 1 меню" ); spacebar!); } void menu_item_2( void ) { printf( "ЧпВыполнилось действие по пункту 2 меню" ); spacebar!); } void menu_item_3( void ) < printf( "ЧпВыполнилось действие по пункту 3 меню" ); spacebar!); }
246 Глава 8 void menu_item_4( void ) ( printfC "ЧпВыполнилось действие по пункту 4 меню" ); spacebarO; } void menu_item_5( void ) { printft "ЧпВыполнилось действие по пункту 5 меню" ); spacebar!); } void menu_item_6( void ) < printft "ЧпВыполнилось действие по пункту 6 -меню" ); spacebarO; } Потребность в передаче указателя функции в качестве параметра другой функции возникает при решеннн задачи о построении таблицы значеннй функции. На листинге 8.6 представлен интерфейс с программой, строящей таблицу значений произвольной функции. Листинг 8.6 содержит текст файла table, h, в котором определяется указатель на функцию, возвращающую значение тнпа float и имеющую однн параметр тнпа float. Первый параметр f функции построения таблицы form_table имеет тип function_type. Так задается исходная функция, значения которой должны быть помещены в таблицу. Второй и третий параметры указывают область определения функции. Четвертый параметр задает интервал (шаг) между соседними значениями аргумента функции. Листинг 8.6. Применение указателей на функцин: интерфейс с функцией form_table /* Файл table.h */ typedef float ( *function_type ) ( float г ); extern void form_table( function_type f, float lower_limit, float upper_limit,
Функции, области видимости и классы памяти 247 float increment ); На листинге 8.7 приведена реализация функции form_table. В цикле WHILE while ( x <= upper_limit ) { printft "\nX-20.5f %-20.5f\ x, < »f ) ( x ) ); x += increment; } вычисляется значение функцнн f при помощи конструкции ( »f ) ( х ) Листинг 8.7. Реализация функции formtable /» Файл table.с »/ «include "table.h" void form_table( function_type f, float lower_limit, float upper_limit, float increment ) { float x = lower_limit; printft "x printft " while ( x <= upper_limit ) < printft "\n5C-20.5f 3C-20.5f", x, ( »f ) ( x ) ); x += increment; } } На листинге 8.8 представлена короткая тестовая программа, с помощью которой строится таблица для квадратичной функцнн my_function, определенной в той же программе. Программа выведет следующую таблицу: у\п" ); \п" );
248 Глава в х у 0.00000 0.10000 0.20000 0.30000 0.40000 0.50000 0.60000 0.70000 0.80000 0.90000 1.00000 1.10000 1.20000 1.30000 1.40000 1.50000 1.60000 1.70000 1.80000 1.90000 0.00000 0.01000 0.04000 0.09000 0.16000 0.25000 0.36000 0.49000 0.64000 0.81000 1.00000 1.21000 1.44000 1.69000 1.96000 2.25000 2.56000 2.89000 3.24000 3.61000 Листинг 8.8. Тестовая программа, использующая функцию form_table /* Тестовая программа печати таблицы */ «include <stdio.h> «include "table.h" float my_function( float x ) { return x » x; } mainO < form_table( my_function, 0.0, 2.0, 0.1 ); printft "\n" ); } На листинге 8.9 приведен еще одни пример использования указателей на функции - обобщенная программа сортировки.
Функции, области видимости и классы памяти 249 Алгоритмы сортировкн не зависят от базового типа данных, подлежащих сортировке. Все, что требуется, это обеспечить возможность сравнения двух элементов базового типа н определения, какой из них меньше. Такнм образом, обобщенная программа сортировки в качестве Входного параметра должна получать заданную пользователем функцию, позволяющую сравнивать два элемента. Логика работы функции сортировки основывается на предположении, что функция сравнения двух элементов, написанная пользователем, многозначна. Напомннм, что на листинге 2.12 н в тексте, поясняющем приведенную там программу, содержится подробное описание алгоритма сортировки методом пузырька. В качестве примера сортируемых данных рассматривается структура RECORD, содержащая два поля - пате и id_number. Тип compare_type typedef int ( «compare_type ) ( void*, void* ); вадает указатель на функцию, возвращающую тип int и имеющую в качестве входных параметров два указателя на >■ тип void. В гл. 6 говорилось о том, что указатель на тип void может совмещаться с любым указателем. На листинге 8.9 описывается глобальный массив fruit, состоящий из элементов типа RECORD. Здесь же ннициализнруются его первые 10 значений. В функции main описываются две функцнн сравнения: extern int compare_by_name( void *fruitl, void *fruit2 ); extern int compare_by_idjumber( void *fruitl, void *fruit2 ); Каждую из этих двух функций сравнения можно отнести к типу compare_type, поскольку они возвращают значенне типа int и нмеют по два параметра, указывающих на тип void. Использование первой из функций compare_by_name позволяет упорядочивать массив записей fruit по полю пате. Использование второй функции compare_by_id_number дает возможность упорядочивать массив записей fruit по полю id number.
250 Глава 8 В функции main описывается и универсальная функция сортровки sort: extern void sort( char* data, int size, int elem_size, compare_type compare ); Первый параметр data описан как указатель на тип char. Такое описание обычно трактуется как задание строки символов. В данном случае мы описали параметр data как указатель на тип char, поскольку значения типа char занимают минимальный объем памяти. Функции sort неизвестен истинный тип сортируемых данных, и поэтому она должна при необходимости пересылать данные цобайтно. Второй параметр функции sort указывает количество сортируемых значений. Третий параметр функции sort указывает размер элемента сортируемого массива в байтах. Четвертый параметр функции sort определяет написанную пользователем функцию сравнення, позволяющую определить ранжировку любых двух элементов базового типа. В программе main вначале выполняется сортировка массива fruit по значеиням поля name, а затем по значениям поля idnumber. После каждой сортировки упорядоченный массив распечатывается. Программа выводит следующие результаты: Apple 5 Apricot 9 Grapefruit 4 Lemon 1 Lime 8 Lime 2 Orange 6 Pear 3 Pineapple 7 Plum 0 Plum 0 Lemon 1 Lime 2 Pear 3 Grapefruit 4 Apple 5
Функции, области видимости и классы памяти 251 Orange 6 Pineapple 7 Lime 8 Apricot 9 Листинг 8.9. Универсальная программа, использующая указатели на функции /» Универсальная программа сортировки »/ /» Файл gensort.c */ «include <stdio.h> «include <string.h> «define RECORD struct fruit_record RECORD < char name! 20 1; int id_number; >; typedef int ( »compare_type )( void», void» ); RECORD fruittl = < "Apple", 5, "Orange", 6, "Lime", 8, "Apricot", 9, "Pineapple", 7, "Pear", 3, "Grapefruit", 4, "Lemon", 1, "Plum", 0, "Lime", 2 >; «define num ( sizeoft fruit ) / sizeoft fruit! 0 1)) mainO < extern int сотраге_Ьу_паще( void *fruitl, void *fruit2 );
252 Гам» 8 extern int compare_by_id_number< void «fruitl, void «fruit2 ); extern void sort( char* data, int size, int elemjsize, compare_type compare ); extern void print( RECORD «f, int size ); sort( ( char* ) fruit, num. sizeof( RECORD ), compare_by_name ); print( fruit, nun ); sor4t< < char* ) fruit, nun, sizeof( RECORD ), compare_by_idjaumber ); print( fruit, num ); printft "\n" ); ) void sort( char* data, int size, int elemjsize, compare_type compare ) /* Алгоритм сортировки методом пузырька */ < int i, J, k; char «offset 1, »offset2; char temp; for < i = 0; i < size - 1; i++ ) for ( J = size - 1; J > 1; j— ) { offsetl = data + j * elemjsize; offset2 = offsetl - elemjsize; if ( ( «compare X offsetl, offset2 ) < 0 ) /• Поменять местами два элемента. Пересылка выполняется побайтно. »/ for ( k = 0; к < elemjsize; k++ ) { temp = offsetl! к 1; offsetl! к 1 = offset2! к 1; offset2! к 1 = temp; > > > void print( RECORD «f, int size )
Функции, области видимости и классы памяти 253 < int i; printf< "ЧгЛп" ); for ( i = 0; i < size; i++ ) printf( "\nXs %d\ < f + i ) -> name, ( f + i ) -> id_riumber ); ) lnt compareJ>y_riame( void «fruitl, void »fruit2 ) < return strcmpf ( ( RECORD • ) fruitl ) -> name, ( ( RECORD • ) fruit2 ) -> name ); ) lnt compare_by_id_number( void «fruitl, void »fruit2 ) < return ( ( ( RECORD • ) fruitl ) -> id_riumber - ( ( RECORD • ) fruit2 ) -> idjiumber ); > Рассмотрим некоторые функции с листинга 8.9 более подробно. В функции compare_by_name int сотраге_Ьу_лате< void «fruitl, void »fruit2 ) < return strcmp( ( ( RECORD • ) fruitl ) ( ( RECORD • ) fruit2 ) > используется стандартная библиотечная функция (Турбо) Си strcmp. Для того чтобы адрес fruitl (указывает иа тип void) указывал иа запись, используетси преобразование типов ( RECORD * ). Далее для получения поля name структуры с указателя снимается ссылка. Те же действия выполняются и для адреса fruit2. В качестве результата функции возвращается результат действия функции strcmp. Функция compare_by_id работает подобным образом. В универсальной функции сортировки sort две ло- -> name, -> name );
254 Гла.а 8 кальиые переменные offsetl и offset2 описываются как указатели иа тип char. Тип char выбраи из тех соображений (об этом говорилось выше), что он требует наименьший объем памяти. При выполнении двух циклов FOR переменные i и j пробегают все значения из указанных диапазонов и переменным offsetl и offset2 присваиваются адреса значений, соответствующие элементам массива datafj] и data[j-l]. Функция compare, задаваемая в качестве входного параметра, используется для выяснения того, нужно ли менять местами два элемента массива. Если перестановка требуется, то она выполняется побайтно при помощи следующего фрагмента программы: for ( k = 0; к < elemjsize; k++ ) { temp = offsetl! к ]; offsetl! к 1 - offset2! к 1; offset2! к 1 = temp; > Удачнаи реализация универсальной программы сортировки, приведенной иа листинге 8.9, оказалась возможной благодаря средству (Турбо) Си, позволиющему передавать функции в качестве параметров. 8.4. Классы памяти Каждая переменная и функция, описанная в программе иа (Турбо) Си, принадлежит к какому-либо классу памяти. Класс памяти переменной определяет время ее существования и область видимости. Класс памяти для переменной задается либо по расположению ее описания, либо при помощи специального спецификатора класса памяти, помещаемого перед обычным описанием. Класс памяти для функции всегда external, если только перед описанием функцяи не стоит спецификатор static. Все переменные (Турбо) Си можно отнести к одному из следующих классов памяти: automatic (автоматическая, локальная) register (регистровая) extern (внешняя) static (статическая)
Функции, области видимости и классы памяти 255 Подробнее о всех классах памяти будет сказано и последующих разделах настоящей главы. 8.4.1. Автоматические переменные Автоматические переменные можно описывать явно, используя спецификатор класса памятя auto. Но такой способ описаняя применяется редко. Обычно указание иа то, что переменная является автоматической, задается неявно и следует из положения в программе точки описания такой переменной. По умолчанию принимается, что всякая переменная, описанная внутри функции (локальная переменная) или внутри блока, ограниченного фигурными скобками, и не имеющая явного указания на класс памяти, относится к классу памяти для автоматических переменных. Поле видимости автоматической переменной начинается от точки ее описания и заканчивается в конце блока, в котором переменная опясана. Доступ к таким переменным из внешнего блока невозможен. Память для автоматических переменных отводится динамически во время выполнения программы пря входе в блок, в котором описана соответствующая переменная. При выходе из блока память, отведенная под все его автоматические переменные, автоматическя освобождается. Теперь понятно происхождение термина автоматические переменные. Доступ к автоматической неремеииой возможен только из блока, где переменная описана, так как до момента входа в блок переменная вообще не существует (т.е. под нее не отведена память). На листинге 8.10 показано, как внутри блока описываются автоматические переменные. В функции main представлен блок, внутри которого описана автоматическая переменная i. Область видимости такой переменной ограничена телом цикла FOR. Применение автоматических переменных внутри локальных блоков позволяет приближать описание таких переменных к месту их использования, делая программу более наглядной и иногда облегчая отладку. Листинг 8.10. Описание автоматических переменных внутри блока /• Описание атоматических переменных внутри блока */ «Include <stdio.h>
256 Глава 8 maln() < printfl "\nB программе main." ); { lnt i; for < 1 = 10; 1 > 0; 1— ) printft "\n%d", i ); > printf< "\n" ); t Скалярные автоматические переменные при их описании не обнуляются. Пользователь должен сам указать начальные значения для переменных в точке их описания. Согласно стандарту Си, не разрешается инициализировать автоматические структурные переменные, такие, как массивы, структуры и объединения. В Турбо Си такой вид инициализации разрешен, однако при этом может быть нарушена мобильность полученного кода. Безопаснее такие структурные переменные описывать как внешние. Об инициализации структур и объединений говорилось в гл. 7, а об яяициализации массивов - в гл. 6. 8.4.2. Регистровые переменные Спецификатор памяти register может использоваться только для автоматическях переменных или для формальных параметров функции. Такой спецификатор указывает компилятору на то, что пользователь желает разместить переменную не в оперативной памяти, а иа одном из быстродействующих регистров компьютера. Компилятор ие обязан выполнить такое требование. На большинстве компьютеров имеется только небольшое число регистров, способных удовлетворить желание пользователя. Спецификацию register рекомендуется использовать для переменных, доступ к которым в функции выполняется часто. Полученный в результате код будет выполняться быстрее и станет более компактным. Классическая задача получения простых чисел с помощью алгоритма, получившего название «Решето Эратосфе- иа», послужит иам хорошим примером по использованию регистровых переменных. Текст программы приведен на листинге 8.11.
Функции, области видимости и классы памяти 257 К классу памяти register относятся индексные переменные i и к, которые наиболее интеисивио используются в программе. Листинг 8.11. Алгорятм получения простых чясел «Решето Эратосфеиа», реализованный с помощью регистровых переменных /• Решето Эратосфеиа •/ «include <stdio.h> «define size 8190 lnt flags! size 1; unsigned char bell = 7; naln() < int prime, iter, count; register lnt 1, k; printf< "Xc", bell ); for ( iter = 1; iter <= 10; iter++ ) ( count = 0; for ( i = 0; i <= size; i++ ) < flags! i 1 = 1; > for ( i = 0; i <= size; i++ ) < if ( flags! i 1 == 1 ) { prime = i + i + 3; k = i + prime; while ( k <= size ) { flags! k 1 = 0; k += prime; > count +=1;. > } > rpttiming! end ); printf( "%d Простые числа\п", count ); > 9 Зак. 795
25& Глава 8 Существуют некоторые ограничения в использовании регистровых переменных, самое существенное из которых состоят в том, что нельзя обращаться к адресу таких переменных. Регистровыми переменными могут быть объявлены только автоматические переменные. В Турбо Си в регистры могут быть помещены лишь переменные типа short и int, a также близкие указатели. 8.4.3. Внешние переменные и функции Любая переменная, описанная в файле вне какой-либо функции и не имеющая спецификатора памяти, по умолчанию относится к классу памяти для внешних переменных. Такие переменные называются глобальными. Для глобальных переменных область видимости простирается от точки их описания до конца файла, где они описаны. Если внутри блока опясана автоматическая переменная, имя которой совпадает с именем глобальной переменной, то внутри блока глобальная переменная маскируется локальной. Это означает, что внутри данного локального блока будет видна именно автоматическаи переменная. Для внешнях переменных память отводится один раз и остается занятой до окончания выполнения программы. Если пользователь не укажет инициирующее значение глобальным переменным, то им будет присвоено начальное значение нуль. Структурные внешние переменные - массивы, структуры и объединения - могут инициализироваться пользователем в точке их описания. Внешняе переменные видны загрузчику, осуществляющему сборку выполняемой программы из множества объектных файлов. Благодаря этому к внешним переменным возможен доступ и из других файлов. Для того чтобы переменную можно было использовать в другом файле, для нее следует задать спецификатор памяти extern. Соответствующий пример приведен на листинге 8.12. Независимо компилируемые единицы разделены иа листинге штриховыми линиями. Вначале компилируется файл separate.с. Затем компилируется файл test.с, и его объектный модуль объединяется с объектным модулем файла separate.с. Если пользователь забудет при загрузке подключить
Функции, области видимости и классы памяти 259 файл separate.с, то загрузчик выдаст сообщение об ошибке: Undefined symbol ' _а* In module test.с (Неопределенный символ '_&' в модуле test.с) Описание extern int a; на листинге 8.12 показывает компилятору, что переменная а типа int описана (и под иее распределена память) вне данного файла. Теперь такая переменная может быть яс- пользоваиа так, как если бы она была описана в данном файле. Если описание extern для переменной расположено внутри функции, то его действие распостраняется только на данную функцию. Если описание extern находится вне функции, то его действие распостраняется от точки описания до конца файла. Таким образом, сочетание внешних переменных я функций в одном файле и описаний extern в других файлах позволяет объединять в один выполняемый файл несколько независимо скомпилированных программ. Листинг 8.12. Раздельная компиляция и описание extern /• Файл separate.с •/ int a = 6; /» Файл test.с */ «include <stdio.h> main() { extern int a; printf( "a = Xd\n", a ); > Возможность видеть внешние переменные извне файла, где они описываются, предоставляет программистам на (Турбо) Си исключительную гибкость в организации физической структуры программной системы. 9*
260 Глава 8 Утилиты системы Турбо Си МАКЕ и PROJECT MAKE обеспечивают поддержку управления версиями. Взаимозависимости между частями программной системы отражаются в специальном файле, являющемся входной информацией для указанных утилит. Как только возникает необходимость перекомпялировать один иля более файлов из всей системы, утилита МАКЕ, анализируя взаимозависимости между файлами, определяет точное множество файлов, подлежащих рекомпиляцин. Для более подробного ознакомления с возможностями утялнты МАКЕ советуем обратиться к документации фирмы Borland «Turbo С Reference Guide», поставляемой вместе с компилятором Турбо Си. Рекомендуется для каждого файла реализации программы (файла с расширением .с), если в нем используются внешние объекты, доступ к которым будет осуществляться из других файлов, создавать интерфейсные файлы (файлы с расширением . h) и помещать туда описание внешних переменных. Тогда для обеспечения доступа к внешним переменным из файлов-потребителей потребуется лишь включить в эти файлы соответствующий интерфейсный файл. Рассмотренный способ организации программ уже использовался нами в предыдущих главах я будет еще раз продемонстрирован на примере программы в разд. 8.5. По умолчанию считается, что все функции внешние. Местом определения функции является та точка программы, где задаются параметры функции и записывается ее тело. Ко всем функциям, не имеющим спецификатора класса памяти static, обращение из других файлов оказывается возможным, если там функция описывается как внешняя. Таким образом, функция определяется один раз, ио может быть описана много раз (с использованием спецификатора extern). 8.4.4. Статические переменные и функции Для упрятывания функций и переменных от загрузчика используется спецификатор памяти static. Функции н переменные, для которых указан такой класс памяти, видимы лишь от точки описания и до конца файла. Если пользователь не указал инициализирующие значения, то все статические переменные, как н внешние, инициализируются значением нуль. Инициализация структурных статически* переменны* выполняется по тем же
Функции, области видимости и классы памяти 261 правилам, что и инициализация внешних. Если статическая переменная описана внутри функции, то оиа первый раз инициализируется при входе & блок функции. Значение переменной сохраняется от одного вызова функции до другого. Такям образом, статические переменные можно использовать для хранения значений виутря функции иа протяжении времени работы программы, причем такие переменные будут невидимы вие файла, где они определяются. Спецификатор static в определении функции делает ее невидимой для загрузчика, т.е. недоступной из других файлов. 8.4.5. Переменные класса volatile Спецификатор volatile, введенный в новый стандарт ANSI для Си и реализованный в Турбо Си, указывает компилятору на то, что переменная может изменяться яе только программой (как обычная переменная), яо и вие программы, например при обработке прерывания. Для переменных, специфицированных как volatile, компилятор ие в праве сделать какое-нибудь предположение об их значениях при вычислении выражеияй, поскольку такие переменные могут измениться даже в момент вычисления выражения. Компилятору запрещено переводить переменную тяпа volatile в регистровый класс памяти. 8.5. Рекурсия Если функция вызывает саму себя, то говорят, что возникает рекурсия. Рекурсию можно рассматривать просто как еще одну управляющую структуру - управление из точки рекурсивного вызова передается иа начало функции. В действительности же рекурсия - это мощный инструмент для разработки программ, с помощью которого даже большие объемы действий могут быть записаны всего несколькими строками программного кода. Рекурсией следует пользоватьси осторожно и внимательно. При исполнении рекурсивного вызова система сохранит в стеке значения всех аатоматических переменных функции и ее параметров. По завершении рекурсивного вызова значения будут восстановлены и управление возвратится на оператор, стоящий непосредственно за операто-
262 Глава 8 ром вызова. Для размещения в стеке автоматических переменных и значений параметров необходимы в память, н время счета. С помощью рекурсии можно объяснить или определить многие понятия. В некоторых случаях рекурсивное построение алгоритма является наиболее естественным н экономичным путем решения проблемы. В разд. 8.6 вводится абстракция поиска по дереву, основанная на двоичном дереве. Поскольку структура дерева по существу рекурсивна, то мы рекурсивно определим и алгоритмы включения, удаления н выбора элемента. В данном разделе будет приведено несколько простых примеров рекурсии. Более сложные примеры рекурсивных алгоритмов можно найти в книгах cData Structures Using Modula-2» (Sincovec н Wiener, 1986) илн «Modula 2: A Software Development Approach» (Ford н Wiener, 1986). После прочтения настоящего раздела читателю будет интересно вернуться к анализу рекурсивного алгоритма быстрой сортировки, текст которого содержится на листинге 6.9. На листинге 8.13 приведен пример весьма простого типа рекурсии. Рекурсивный вызов записей в последней строке функции write name. При возврате из рекурсивного вызова никаких действий в программе больше не выполняется. Такой тип рекурсии практически эквивалентен итерации. Использование на листинге 8.13 параметра count функции writename, передаваемого по значению, ведет к значительному расходу памяти. При каждом, рекурсивном вызове текущее значение параметра count заносится в стек. Программа на листинге 8.13 выведет следующий текст: Турбо Си удобен и эффективен Турбо Си удобен и эффективен Турбо Си удобен и эффективен Турбо Си удобен и эффективен Турбо Си удобен и эффективен Турбо Си удобен и эффективен Турбо Си удобен и эффективен Турбо Си удобен и эффективен Турбо Си удобен и эффективен Турбс Си удобен и эффективен
Функции, области видимости и классы памяти 263 Листинг 8.13. Пример простой рекурсии «include <stdio.h> mainO { extern void write_»ame( char «name, int count ); write_name( "Турбо Си удобен и эффективен ", 10 >; printf( "\n" ); } void write_name( char «name, int count ) { if ( count > 0 ) { printf( "\n%s", name ); write_name( name, count - 1 ); } } На листинге 8.14 приведен пример более сложной рекурсии. Последовательность шагов выполнения программы (трассировка) дана в табл. 8.1. Каждый последующий успешный рекурсивный вызов не зависит от предыдущего. Рекурсия здесь имеет более сложный вид, поскольку в функции print содержатся уже два рекурсивных вызова. Таблица 8.1. Трассировка рекурсивной программы с листинга 8.14 Остановы программы Разъяснения 1. print( п = 10 ) Вход в print с п = 10 2. print( 5 ) Рекурсивный вызов print с п = 5 3. print( п = 5 ) Вход в print с п = 5 4. print( 2 ) Рекурсивный вызов print с п = 2 5. print( п = 2 ) Вход в print с п = 2 6. print( 1 ) Рекурсивный вызов print с п = 1 7. print( п = 1 ) Вход в print с п = 1 8. print( 0 ) Рекурсивный вызов print с п = 0 Вход в print с п = 0 и выход 9. output 1 10. print( 0 ) Рекурсивный вызов print с п = 0 Вход в print с п = 0 и выход 11. output 2 12. print( 1 ) Рекурсивный вызов print с п = 1 13. print( п = 1 ) Вход в print с п = 1
264 Глава 8 14. output 5 15. print( 2 ) Рекурсивный вызов print с n = 2 16. print( n = 2 ) Вход в print с п = 2 17. output 10 18. print( 5 ) Рекурсивный вызов print с n = 5 19. print( n = 5 ) Повторение шагов с 4 по 16 и вывод 1, 2, 1, 5, 1, 2, 1 Листинг 8.14. Пример более сложной рекурсии ^include <stdio.h> mainO extern void print( int n ); print( 10 ); printf( "\n" ); } void print( int n ) < if ( n != 0 ) i print( n / 2 ); printf( u\nXd", n ); print( n / 2 ); ) ) Программа на листинге 8.14 выводит следующие строки: 1 2 1 5 1 2 1 10 1 2 1 5 1 2 1
Функции, области видимости и классы памяти 265 В качестве последнего примера рекурсии в настоящем разделе рассмотрим генератор перестановок. Выводятся все перестановки символов А, В, С в D. Соответствующая программа приведена на листинге 8.15. Читатель может самостоятельно проанализировать текст программы и убедиться в том, что в результате ее работы будут получены следующие строки: ABCD BACD ACBD CABD CBAD BCAD ABDC BADC ADBC DABC DBAC BDAC ADCB DACB ACDB CADB CDAB DCAB DBCA BDCA DCBA CDBA CBDA BCDA Листинг 8.15. Генератор перестановок, основанный на рекурсии ^include <stdio.h> char datal 4 1 = { ' A', 'В*, 'С*, 'D' }; int number = 4; nain() { extern void permutel char *s, int n ); permute! data^ number ); printft "\n" );
266 Глава 8 > Void permute( char «s, -int n ) { char temp; int i,J; if ( n > 1 -> { permute( s, n - 1 ); for ( i = n - 1; i >= 1; i— ) < /• Поменять местами элементы si n - 1 1 и st i - 1 I •/ temp = s[ n - 1 1; st n - 1 1 = st i - 1 1; st i - 1 1 = temp; permute( s, n - 1 ); /• Поменять местами элементы s[ n 1 и si i.l ♦/ temp = st n - 1 1; sf n - 1 1 = sf i - 1 ]; sf i - 1 1 = temp; } } else { for ( j = 1; j <= number; j++ ) printf( '7.c", si j - 1 1 ); printf( "\n" ); } } После небольшой модификации функцию permute можно приспособить для генерации перестановок яз любых объектов. 8.6. Двоичное дерево поиска Двоичные деревья относятся к наиболее важным структурам данных, используемым в информатике. Деревья применяются при синтаксическом анализе, поиске, сортировке, управлении базами данных, в игровых алгоритмах и других важных сферах приложений. Двоичное дерево представляет собой конечное множество элементов, каждый из которых может быть либо
Функции, области видимости и классы памяти 267 пустым, либо содержать корневой узел н, возможно, другие узлы. Эти узлы можно разделить на два непересекающихся подмножества, каждое из которых само является двоичным деревом. Такие подмножества называются левым и правым поддеревьями. Каждый узел двоичного дерева может иметь 0, 1 нлн 2 поддерева. Если у узла нет поддеревьев (потомков), то он называется листом. Для нашего примера будем предполагать, что каждый- узел двоичного дерева имеет идентифицирующий его ключ. Дерево поиска является разновидностью двоичного дерева, обладающей следующим свойством: все узлы дерева, лежащие левее и ниже данного узла, имеют значение ключа, меньшее, чем значение ключа данного узла, а все узлы, лежащие правее и ниже данного узла, имеют значение ключа, большее, чем значение ключа данного узла. Приведенное свойство позволяет эффективно организован вать поиск по двоичному дереву. 50 30 70 15 45 Рис. 8.1а. Двоичное дерево поиска. На рнс. 8.1а показано двоичное дерево, являющееся деревом поиска. На рис. 8.16 изображено дерево, для которого нарушено требование нз определения дерева поиска. На рнс. 8.16 корневой узел имеет два левых потомка, значения ключей у которых больше чем 30. 30 40 90 20 60 Рис. 8.16. Двоичное дерево, поиска. не являющееся деревом
268 Глава 8 Деревья поиска являются весьма важными с практической точки зрения, поскольку они относительно сбалансированы: число уровней дерева примерно равно двоичному логарифму от числа узлов. Например, относительно сбалансированное дерево с 1000000 узлов имеет лишь 20 уровней. Алгоритмы, используемые для добавления, исключения и поиска элементов в дереве, зависят не от количестве узлов, а от количества уровней дерева. Таким образом, нелинейная структура дерева существенно влияет на достижение эффективности вычислений. На Листинге 8.16 представлен интерфейс с программами, реализующими работу с деревом поиска. В этом файле определяется структура данных TREE. Каждый узел дерева описывается с помощью той же самой сложной структуры, которую мы уже использовали в разд. 7.3.3 на листинге 7.17. В качестве ключа, на основе которого строится дерево, используется поле lastname. Единственное отличие описаний структур по сравнению с листингом 7.17 состоит в том, что добавлены поля left и right (указатели на левый н правый потомки) и исключено поле next. Интерфейс с функциями insert, display, is_present, takeout и destroy остался неизмененным. Листинг 8.16. Интерфейс с программами, реализующими работу с деревом поиска /• Интерфейс с деревом поиска для персонала университета •/ /• Файл tree.h •/ «define STAFF struct stuff_type STAFF { int years_of_service; float hourly_wage; >; «define STUDENT struct student_type STUDENT { float grade_pt_average; int level; >; «define PROFESSOR struct prof_type
Функции, области видимости и классы памяти 269 PROFESSOR { int dept_nu»ber; float annualjsalary; >; «define N0DE_TYPE enum node_type typedef NODE_TYPE < student, professor, staff }; «define TREE struct tree TREE < char last„name[ 10 1; char first_namel 10 1; int age; TREE «left, «right; N0DE_TYPE tag; union { STUDENT student; PROFESSOR professor; STAFF staff; ) node_tag; ); extern void insert( TREE ••root, TREE «item ); extern void display! TREE «root ); extern int is_present( TREE «root, TREE «item ); extern int take_out( TREE ••root, TREE «item ); extern void destroy* TREE «root ); На листинге 8.17 представлена реализация функций работы с деревом поиска. Из-за значительного объема приведенного листинга детально проанализируем действие только двух функций - display и insert. Текст остальных функций читателю предлагается разобрать самостоятельно. В функции display рекурсия используется для того, чтобы «посетить> каждый узел дерева ровно один раз. Примененный для этого рекурсивный алгоритм называется «последовательным обходом». Одним из побочных эффектов такого алгоритма является вывод имен из заданного списка в соответствии со значением ключевого поля. Работу функции display можно схематично представить следующим образом:
270 Глава 8 if ( root ) { display( root -> left ); /• — Печать информации — */ display( root -> right ); } Именно условие if(root) заставляет рекурсию возвращаться назад и выполнять просмотр дерева дальше. Когда достигается ннз дерева, значение root -> left равно нулю (для всех узлов, являющихся листьями, соблюдается требование root->left = root->right = 0). Предлагаем читателю тщательно проанализировать работу функции display и убедиться в том, что она осуществляет вывод списка узлов в алфавитном порядке, основанном на значениях ключей в поле lastname. Функция create всегда создает новый узел. Положение нового узла в дереве определяется по следующим правилам. Значение ключа элемента, добавляемого к дереву, сравниваетсн со значением ключа корневого узла. Если ключ элемента меньше чем ключ корневого узла, то нужно переместиться к левому потомку узла; в противном случае следует переместиться к правому потомку узла. Сравнения повторяются, причем каждый раз ключ элемента сопоставляется либо с ключом левого потомка, либо с ключом правого потомка. Если обнаруживается совпадение, то сравнения прекращаются; в противном случае сравнения продолжаются, пока не будет достигнут низ дерева. После завершения обхода дерева указатель parent всегда указывает на узел, лежащий иа один уровень выше, чем узел, на который указывает указатель current, н задает положение предка текущего узла. Если указатель current принимает нулевое значение (т.е. достигнут низ дерева), то вновь создаваемый узел подключается к родительскому узлу либо как левый, либо как правый потомок в зависимости от значения ключа узла. Листинг 8.17. Реализация дерева поиска /• Файл tree.с ♦/ /* Реализация дерева для персонала университета */ «include <string.h>
Функции, области «идимости и классы памяти 271 «include <stdio.h> «include <alloc.h> «Include "tree.h" static TREE» create_node( TREE «item ) < TREE «node; node = ( TREE • ) mallocC sizeof( TREE ) ); •node = «item; return node; } void destroy! TREE «root ) { if ( root ) { destroy( root -> left ); destroy( root -> right ); free( root ); } root = 0; } int take_out( TREE •♦root, TREE «item ) { /• Подробности приведены в книге DATA STRUCTURES WITH MODULA-2, Sincovec, Viiener, Viiley, 1986 на стр. 285 и 286 ♦/ TREE «previous = 0, •present = «root, •replace, •Sr •parent; int found = 0; while ( present && !found ) { if ( strcmp( item -> last_name, present -> last_name ) == 0 ) found = 1; else { previous = present; if ( strcmp( item -> last_name, present -> last_name ) < 0 ) present = present -> left; else
272 Главе 8 present = present -> right; } } if ( found ) < if ( present -> left == 0 ) replace = present -> right; else if ( present -> right == 0 ) replace = present -> left; else { parent = present; replace = present -> right; s = replace -> left; while ( s != 0 ) { parent = replace; replace = s; s = replace -> left; } if ( parent != present ) < parent -> left = replace -> right; replace -> right = present -> right; } replace -> left = present -> left; } if ( previous == 0 ) •root = replace; else if ( present == previous -> left ) previous -> left = replace; else previous -> right = replace; free( present ); } > void insert! TREE «Toot, TREE «item ) < TREE «parent = 0, •current = Toot; TREE »new_node;
Функции, области •идимости и классы памяти 273 int found = 0; while ( current && !found ) < if ( strcmp( item -> last_name, current -> last_name ) == 0 ) found = 1; else { parent = current; if ( strcmpf item -> last_name, current -> last_name ) < 0 ) current = current -> left; else current = current -> right; } } if ( found == 0 ) { if ( parent == 0 ) /• Первый узел дерева •/ { •root = create_node( item ); ( «root ) -> left = ( «root ) -> right = 0; } else { new_node = create_node( item ); new_node -> left = new_node -> right = 0; if ( strcmpf item -> last_name, parent -> last_name ) < 0 ) parent -> left = new_node; else parent -> right = new_node; } } } void display( TREE «root ) { if ( root ) < display! root -> left ); printfl "\n%s, V.s", root -> lastjiame, root -> first_name );
274 Главе 8 printf( "ЧпВозраст = Xd", root -> age ); switch ( root -> tag ) { case student: printf( "ЧпСпециализация : X.2f", root -> node Jag. student.grade_pt.average ); printft "\nKypc : Xd\n", root -> node_tag.student, level ); break; case professor: printn "ЧпНомер кафедры : Xd", root -> node_tag.professor, dept„number ); printff "ЧпОклад : $X.2f\n", root -> node_tag. professor, annual salary ); break; case staff: printf< "ЧпСрок службы : 7.Л", root -> node_tag. staff. years_of_service ); printfl "ЧпПочасовой тариф : $'/..2f\n\ root -> node_tag. staff. hourly_wage ); } display( root -> right ); } } int is_present( TREE «root, TREE «item ) < TREE «current = root; int found = 0; while ( current && .'found ) { if ( strcmpf item -> last_name, current -> last_name ) == 0 ) found = 1; else i if ( strcmpf item -> last_name, current -> last_name ) < 0 ) current = current -> left;
Функции, области «идимости и классы памяти 275 else current = current -> right; } } return found; } На листинге 8.18 представлена тестовая программа, сходней с программой на листинге 7.19. Программа выведет следующий результат: Evans, Henry Возраст = 19 Специализация : 3.10 Курс : 1 Jones, Richard Возраст = 56 Номер кафедры : 7 Оклад : S48321.00 Smith, Robert Возраст = 26 Стаж : 3 Почасовой тариф : S5.25 Элемент 1 присутствует в списке. Jones, Richard Возраст = 56 Номер кафедры : 7 Оклад : $48321.00 Smith, Robert Возраст = 26 Стая : 3 Почасовой тариф : $5.25 Smith, Robert Возраст = 26 Стаж : 3 Почасовой тариф : $5.25
276 Глав. 8 Листинг 8.18. Тестовая программа работы с деревом, поиска «include <string.h> «include <stdio.h> «include <alloc.h> «include "tree.h" TREE »my_tree; mainf) { TREE «iteml = ( TREE * ) malloc< sizeof( TREE ) ); TREE »item2 = ( TREE * ) mallocf sizeof( TREE ) ); TREE »item3 = ( TREE • ) malloc< sizeof( TREE ) ); strcpy( iteml -> lastjiame, "Smith" ); strcpy( iteml -> first_name, "Robert" ); iteml -> age = 26; iteml -> tag = staff; iteml -> node_tag.staff.years_of_service = 3; iteml -> node_tag. staff. hourly_wage = 5.25; insert! &my_tree, iteml ); strcpy( item2 -> lastjiame, "Jones" ); strcpy( item2 -> firstjiame, "Richard" ); item2 -> age = 56; item2 -> tag = professor; item2 -> node_tag. professor, dept„number = 7; item2 -> node_tag. professor. annual_salary = 48321.0; insert( &my_tree, iten>2 ); strcpy( item3 -> last_name, "Evans" ); strcpy( item3 -> first_name, "Henry" ); item3 -> age = 19; item3 -> tag = student; item3 -> node_tag.student.level = 1; item3 -> node_tag. student. grade_pt_average = 3.1; insert! &my_tree, item3 ); display! my_tree ); if < is_present( my_tree, iteml ) ) printf< "\n 1 .\n" ); else printff "\n 1 An" ); take_out( &my_tree, item3 ); display( my_tree ); take_out( &my_tree, item2 ); display! my_tree );
Функции, области видимости и классы памяти 277 take_out( &my_tree, iteml ); display! ay_tree ); take_out( &my_tree, itea3 ); display! my_tree ); printff "\n" ); 8.7. Приложения двоичных деревьев поиска Пусть иам требуется написать на Турбо Си программную систему, подсчитывающую частоту поивления каждого слова в текстовом файле. Для демонстрации задачи декомпозиции разделим программную систему на ряд модулей. Hord_fq.c Главный управляющий модуль, в котором будет считываться файл ввода и в выводной файл будет передаваться частотная таблица слов. Hord_ut.h Интерфейс с модулем word_ut.c. Здесь содержатся две важные функции - stringsave и get_word. В первой из функций отводится память, требуемая для размещения введенной строки s, и строка s копируется в отведенное место памяти. Во второй функции из файла выделяется и выдается (если оно есть) очередное слово. Если слово найдено, возвращается его значение; в противном случае возвращается значение нуль. В нашем примере слова будут разделяться иебуквенными символами. Hord_ut.c Реализации функций string_save и get_word. tree.h В этом интерфейсном файле определяется двоичное дерево. Каждый узел дерева содержит поли word (строка), freq (счетчик появлений слова в файле), left и right (указатели на левый и правый потомки данного узла). В файле tree.h представлен интерфейс с функциями createtree и display. С помощью функции createtree из слов файла строится дерево поиска. При этом .используется функции get_word из файла word_ut.c. Функция display no дереву строит частотную таблицу слов. tree, с Реализация функций create_tree и display.
278 Гмм В На листинге 8.19 приведен файл word_ut.h. Листинг 8.19. Интерфейс с word_ut /* Файл word_ut.h •/ «define MAXSIZE 50 extern char »string_save( char »s ); extern int get_word( char *w ); На листинге 8.20 представлена реализации программ из wordut.c . В функции string_save отводится область памяти размером strlen(s)+l байтов, и ее адрес р (указатель на тип char) возвращается вызывающей программе. В функции get_word используете и стандартное библиотечное макро isalpha: while ( !isalpha( ch = getcharO ) && ch != EOF ) В цикле WHILE из входного потока считываются символы, пока они ие являются буквами. Если символ EOF встретится до того, как будет обнаружено слово, то функция прекратит свою работу и вернет значение нуль. В следующем цикле WHILE из входного потока считываются символы, пока они остаются буквами. Каждый символ переводится в строчную букву (в нижний регистр) и переписывается в строку w. В конце цикла к слову w добавляется завершающий нулевой символ. Возвращается значение 1, сигнализирующее о том, что слово выделено успешно. Листинг 8.20. Реализации wordut /• Файл word_ut.c •/ «include <string.h> «include <stdio.h> «include <ctype.h> «include <alloc.h> «include "word_ut.h" char •string_save( char *s ) < char »p; if ( ( p = mallocf strlenl s ) + 1 ) ) != NULL )
Функции, области видимости и классы памяти 279 strcpy( p, s ); return ( р ); } int get_word( char »w ) < int ch, count = 0; while ( !isalpha( ch = getcharO ) && ch != EOF ) if'( ch == EOF ) return 0; else < *w++ = tolower( ch ); count++; while ( !isalpha( ch = getcharO ) && ch != EOF ) if ( ++count < MAXSIZE ) •w++ = tolower( ch ); *w = '\0'; /* В конец слова дописываем завершающи строку нулевой байт •/ return 1; } ) На листинге 8.21 определиетси структура NODE и приводится интерфейс с фуикциими createtree и display. Листинг 8.21. Интерфейс с программами работы с деревом /• Файл tree.h •/ ftdefine NODE struct tree_node NODE < char «word; unsigned freq; NODE *left; NODE «right; ); NODE »create_tree(); void displayU;
280 Глам 8 На листинге 8.22 приведена реализация дерева. Программа построения дерева похожа иа текст функцив insert, описанной в разд. 8.6 и приведенной иа листинге 8.17. Листинг 8.22. Реализации программ работы с деревом /* Файл tree.с •/ «include <stdio.h> •include <string.h> ♦include <alloc.h> «include "tree.h" «include "worxLut.h" static NODE »alloc_node() /• Эта функция скрыта »7 < return ( ( NODE * ) malloc( sizeof( NODE ) ) ); } NODE »create_tree() { extern char »string_save(); extern NODE •allocjiode(); NODE «current, «previous; NODE «root = 0; char wl MAXSIZE +11; int distinct; while ( get_word( w ) ) { if ( root == 0 ) { root = alloc_node(); root -> word = string_save( w ); root -> left = root -> right = 0; root -> freq = 1; distinct = 0; } else { previous = 0; current = root; distinct = 1; while ( current != 0 ) { previous = current;
Функции, области видимости и классы памяти 281 if ( strcmp( w, current -> word ) < 0 ) current = current -> left; else if ( strcmpf w, current -> word ) > b ) current = current -> right; else /* w уже имеется в дереве */ { current -> freq++; distinct = 0; break; } } if ( distinct ) { current = alloc_node(); current -> word = string_save( w ); current -> left = current -> right = 0; current -> freq = 1; if ( strcmpf w, previous ->. word ) < 0 ) previous -> left = current; else previous -> right = current; } } } return root; } void NODE { if < } display! *p; ( p != 0 display! printfl ' display! P > ) P "> P "> left ); 40s X-6d right ) p -> word, p -> freq ); На листинге 8.23 приведена основная управляющая программа wordfq.c. Благодаря введенным в файлах wordut.c и tree.c абстракциям программа main содержит всего несколько строк.
282 Глам в Листинг 8.23. Главная управляющая программа для построения частотной таблицы слов /• Корректный вызов: Файл worcLfq. с worcLfq <Infile >outfile •/ «include <stdio.h> «include "tree.h" mainO { NODE «root; root = create_tree(); display( root ); printf( "\n" ); > Если рассмотренные на листингах 8.19 - 8.23 программы применить к первому абзацу разд. 8.7 оригинала книги, то результат работы будет следующий: а с decomposition every file following frequency illustrate in into modules occurence of partitition problem software system tabulates text that the to
Функции! области видимости и классы памяти 283 turbo 1 we 2 wish 1 Kord 1 write 1 8.8. Функции с переменным числом параметров Турбо Си поддерживает функции Сн с неизвестным (переменным) числом параметров в соответствии со спецификациями, данными в новом стандарте ANSI для Си. В библиотеке, интерфейс с которой описан в файле stdarg.h, содержатся некоторые функции и макро, поддерживающие мобильный способ манипулирования со списками параметров переменной длины. Функции н макро из библиотеки stdarg, обеспечивающей работу с функциями с переменным числом параметров, приведены в табл. 8.2. Таблица 8.2. Функции и макро, поддерживающие работу со списками параметров функций переменной длины ♦include <stdarg.h> va_list Такой тип используется для описания локальной переменной argptr, которая служит для обхода списка аргументов void va_start( va_list argptr, last_fixed_parameter ); ~ Функция инициирует переменную argptr и должна быть вызвана перед первым обращением к функции va_arg нлн va_end. Внутренний указатель устанавливается на argptr, первый параметр передается va_start type va_arg( va_list argptr, type ); Макро возвращает значение очередного параметра нз списка аргументов н продвигает внутренний указатель argptr к следующему аргументу, если он существует void va_end( va_list argptr ); Эту функцию нужно вызывать после того, как будут прочитаны все аргументы в va_arg. Функция выполняет все необходимые операции очистки
284 Глаы в На листинге 8.24 приведена тестовая программа, в которой используются функции и макро, описанные в табл. 8.2, и иллюстрируется применение функций с переменным числом параметров. Функция vprintf, используемая в функции listnames на листинге 8.24, подробно рассматривается в разд. 10.1.25. Программа на листинге 8.24 выводит Список имен -> Richard Erik Marc Список имен -> Sheila Irving Произведение 5 • 4 * 3 = 60 Листинг 8.24. Функции с переменным числом параметров «include <stdio.h> «include <stdarg.h> void multiply_arguments( char «message, ... ) < int product = 1.0; int argument; va_list argptr; va_start( argptr, message ); while ( ( argument = va_arg( argptr, int ) ) != 0 ) product *= argument; printf( message, product ); > void list_jiames( char «format, ... ) < va_list argptr; printff "\п\пСписок имен ->\п" ); va_start( argptr, format ); vprintf( format, argptr ); va_end( argptr );
Функции, области видимости и классы памяти 285 mainO < list_names( "Ks\nXs\nXs\n", "Richard", "Eric", "Marc" ); list_names( '"Xs\nXs\n", "Sheila", "Irving" ); multiply_arguments( "\п\пПроизведение 5*4*3= Kd\n", 3, 4, 5, 0 ); )
Глава 9 Родовые структуры данных Базовые операции определения абстракций списка и дерева поиска не зависят от типа данных, содержащихся в структуре списка нлн дерева. Поэтому такие абстракции являются идеальными кандидатурами для их обобщенной (родовой) реализации. Родовой пакет структур данных при условии его эффективной реализации может стать полезным повторно используемым программным компонентом. Программисту не придется для каждого нового используемого типа данных создавать свою реализацию структуры. В двух последующих разделах будут представлены реализации родовых списков н деревьев поиска, выполненные с помощью Турбо Сн. 9.1. Родовой список На листинге 9.1 представлен интерфейс с программами, реализующими родовой список с рднотипаымн элементами. Определяются структуры LIST н NODE. Структура LIST является заголовком связного списка. Структура включает три поля: next, elem_size н display. Поле next содержит указатель на первый элемент списка, имеющий тип NODE. При инициализации поле получает значение нулевого указателя (0). Размер каждого элемента списка в байтах помещается в поле elem_size. Поле display содержит указатель на определяемую пользователем функцию, с помощью которой можно вывести значение элемента. Эта функция вывода приписывается заголовку списка при вызове функции define. Пользователь должен сам позаботиться о разработке программы вывода значений элементов для каждого конкретного списка. Невозможно организовать вывод значений с помощью самого родового пакета, поскольку заранее не известен базовый тнп элементов списка н, следовательно, не определена форма представления значений. Структура NODE содержит поля info н next. Поле info описывается как указатель на минимальный предопределенный тнп, а именно на тип char. Как и в случае родового алгоритма сортировки, представленного на листинге 8.9, данные в поле info должны передаваться байт за
Родовые структуры данных 287 байтом. Поле next указывает на следующий элемент списка типа NODE. На рис. 9.1 показана структура данных родового связного списка. LIST NODE elemsize display info info Рис. 9.1. Родовой связный список. Интерфейс с функциями define, isert_front, insert_back, getfront н display приведен на листинге 9.1. В предлагаемом простом родовом пакете программ для работы со списками допускаются только два способа включения новых элементов в список: добавление в начало списка (функция insert_front) и добавление в конец списка (insert_back). Удалить элемент из списка можно единственным способом, а именно исключив элемент из начала списка. Перед началом работы с пакетом следует обратиться к функции define. С помощью этой функции в заголовок списка заносится размер элемента списка в байтах (поле elem_size) и указатель на программу вывода значений, определяемую пользователем (поле display). Листинг 9.1. Интерфейс с программами, реализующими родовой список /* Файл genlist.h */ «define LIST struct header «define NODE struct node typedef void ( «display„function )( char *data ); LIST < NODE «next; int elem_size; displayjfunction display;
288 Глава 9 >; NODE { char *info; NODE *next; >; extern void define( LIST **lst, int size, display_function disp_fun ); extern void insert_front( LIST *»lst, char «data ); extern void insert_back( LIST **lst, char *data ); extern char *get_front( LIST **lst ); extern void display_list( LIST *lst ); На листинге 9.2 представлена реализация программ работы с родовым списком. Как уже отмечалось, с помощью функции define с заголовком списка связывается соответствующая функция вывода значений элемента и задается размер элемента списка. Используя передачу' параметра по ссылке, первый оператор функции define ♦1st = malloc( sizeoff LIST ) ); возвращает адрес нового списка. Следующие три оператора инициируют список и заносят в соответствующие поля заданные пользователем значения elem_size и disp_fun. В программе, реализующей функцию insert back, сначала отводится память для указателя newnode. Далее отводится память для одного нз полей этой структуры - поля new_node->info. Число байтов, отведенных по адресу new_node->info, равно значению elem_size. Советуем читателю внимательно разобрать все преобразования типов, используемые в двух операторах запроса памяти. В первом операторе адрес, полученный от функции malloc для структуры new_node, преобразуется к виду указателя на тип NODE. Во втором операторе адрес, полученный от функции malloc для поля new_node->info, преобразуется в указатель на тип char. Далее полю next структуры *new_node присваивается значение нуль. Затем осуществляется побайтовая пересылка данных из поля data в поле new_node -> info. Функции
Родовые структуры данных 289 insertback не известен тип информации, которая заносится в список, но, несмотря на это, пересылка информации все-таки осуществляется. Если создается уже не первый элемент в списке, то для перебора элементов используется цикл WHILE. По достижении конца списка элемент newnode присоединяется к списку. Если в список заносится первый элемент, то в поле next структуры *lst заносится указатель на структуру new_node. Функция get_front возвращает указатель на значение типа char. В функции отводится память для возвращаемой информации и адрес такой информации передается в виде указателя на тип char. Если список не нустой, то по адресу ret_Jnfo отводится elem_size байтов. Затем осуществляется побайтовая пересылка информации из поля old_node->info в массив ret_info. Изменяются ссылки между элементами списка и в качестве результата функции возвращается адрес массива ret_info. В функции display_list осуществляется обход списка и вызывается функция display: 1st -> display! current -> info ); Листинг 9.2. Реализация функций работы с родовый списком /* Файл genlist.c */ «include "genlist.h" «include <alloc.h> void define( LIST ««lst, int size, display_function disp_fun ) ♦1st = mallocl sizeof( LIST ) ); ( «1st ) -> next = 0; ( *lst ) -> elem_size = size; ( *lst ) -> display = disp_fun; ) void insert_front( LIST **lst, char *data ) < NODE »new_node; int index; 10 Зак. 795.
290 Глава 9 /* Отводится память для структуры newjode и ее полей •/ newjode = ( NODE • ) malloc( sizeoft NODE ) ); newjode -> info = ( char * ) malloc( ( «1st ) -> elem_size ); /• Пересылка данных в элемент newjode •/ for ( index = 0; index < ( *lst ) -> elem_size; index++ ) newjode -> info! index ] = data! index 1; /* Присоединение элемента newjode к списку •/ if ( ( «1st ) -> next ) newjode -> next = ( *lst ) -> next; else newjode -> next = 0; ( *lst ) -> next = newjode; > void insert_back( LIST **lst, char «data. ) { NODE «previous, «current, "newjode; int index; /• Отводится память для структуры newjode и ее полей */ newjode = ( NODE « ) malloc( sizeofl NODE ) ); newjode -> info = ( char * ) malloc( ( *lst ) -> elem_size ); newjode -> next = 0; /* Пересылка данных в элемент newjode */ for ( index = 0; index < ( «1st ) -> elenusize; index++ ) newjode -> infol index 1 = data! index 1; if ( ( *lst ) -> next ) < /* Поиск конца списка *"/ previous = ( *lst ) -> next; current = previous -> next; while ( current != 0 ) { previous = current; current = current -> next; > /• Присоединение элемента newjode к списку */ previous -> next = newjode; )
Родовые структуры данных 291 else /* Элемент new_node является первым в списке */ ( *lst ) -> next = newjiode; У char *get_front( LIST *»lst ) { NODE *old_node = ( *lst ) -> next; char *ret_info; int index; if < ( *lst ) -> next == 0 ) < printf( "ЧпОшибка: список пуст" ); return 0; } else { ret_info = ( char * ) malloc( ( *lst ) -> elem_size ); for ( index = 0; index < ( *lst ) -> elem_size; index++ ) ret_info[ index 1 = oldjiode -> info! index ]; ( *lst ) -> next = old_node -> next; free( old_node ); return ret_info; } } void display_list( LIST «1st ) < NODE "current = 1st -> next; while ( current ) < 1st -> display! current -> info ); current = current -> next; > > На листинге 9.3 приведена тестовая программа построения двух списков, использующая пакет программ работы с родовыми списками. Определяются два списка, number_list и record_list. Каждый элемент списка record_list содержит поле name (строка длиной до 30 символов) н поле id (число с плавающей точкой). 10*
292 Глава 9 В тестовой программе определяются две функции вывода значений display_number и displayjrecord. В функции displaynumber используется преобразование типа (int *), с помощью которого вначале результат переводится в тип указателя на тип int, который, в свою очередь, служит для вывода значения элемента списка как целого числа. В функции displayrecord определяется локальная переменная temp, которая инициализируется значением (RECORD*). Результатом работы функции будут значения полей элементов списка temp->info и temp->id. Функция main состоит из двух блоков. В первом блоке в список заносится несколько элементов: вначале с помощью функции insertfront, а затем с использованием функции insert_back. Далее из списка удаляется элемент с помощью следующего оператора: value = ( int * ) get^frontt &number_list ); Функция getfront возвращает указатель на значение типа char. Такой адрес преобразуется к виду указателя на значение типа int. Затем по адресу выбирается значение, и оно печатается. И наконец, осуществляется вывод списка number_list. Аналогичные действия выполняются и для второго списка record_list. Программа на листинге 9.3 выведет Элемент = 6 5 4 -12 -13 -14 -15 Имя Ссссс Имя -> Bbbbb Номер -> 2.2345000 Имя -> Ааааа Номер -> 1.2345000 Имя -> Ddddd Номер -> 4.2345000
Родовые структуры данных 293 Имя -> Еееее Номер -> 5.2345000 Имя -> Fffff Номер -> 6.2345000 Листинг 9.3. Тестовая программа для родового списка /* Программа работы с родовым списком */ •include <stdio.h> «include <stdlib.h> «include <string.h> «include "genlist.h" LIST *number_list; LIST »record_list; «define RECORD struct record RECORD < char name! 30 1; float id; >; main() { extern void display_jmmber( char *info ); extern void display_record( char *info ); { int item; int *value; define( &number_list, sizeof( item ), displayjiumber >; item = 4; insert_front( &number_list, > char * ) &item ) item = 5; insertJ'ronU &number_list, ( char * ) Sitem ) item = 6; insert J"ront( &number_list, ( char * ) Sitem ) item = -12; insertjback( &number_list, ( char * ) Site* ); item = -13; insert_back( &number_list, ( char * ) &item ); item = -14;
294 Глава 9 insert_back( &number_list, ( char * ) Sitem ); item = -15; insert_back( &number_list, ( char * ) Sitem ); value = ( int * ) getJYonU &number_list ); printf( "\n = %d\ «value ); display_list( number_list ); } { RECORD *item = ( RECORD * ) malloc( sizeof( RECORD ) ); RECORD *value; definet &record_list, sizeoft RECORD ), display„record ); strcpy( item -> name, "Aaaaa" ); item -> id = 1.2345; insert_front( &record_list, ( char * ) item ); strcpy( item >•> name, "Bbbbb" ); item -> id = 2.2345; insert_front( &record_list, ( char * ) item ); strcpyt item -> name, "Ccccc" ); item -> id = 3.2345; insert_front( &record_list, ( char * ) item ); strcpy( item -> name, "Ddddd" ); item -> id = 4.2345; insert_back( &record_list, ( char * ) item ); strcpy( item -> name, "Eeeee" ); item -> id = 5.2345; insert_back( &record_list, ( char * ) item ); strcpyf item -> name, "Fffff" ); item -> id = 6.2345; insert_back( &record_list, ( char * ) item ); value = ( RECORD * ) getJYonU &record_list ); printff "\п\пИмя 7.s", value -> name ); display_list( record_list ); } printf( "\n" ); }
Родовые структуры данных 295 void display.number( char «info ) { printf( "\n%d", *( ( int * ) info. ) ); } void displayj*ecord( char *info ) { RECORD Hemp = ( RECORD * ) info; printf( "\пИмя -> Xs", temp -> name ); printf( "ЧпНомер -> %f", temp -> id ); 9.2. Родовое дерево поиска На листинге 9.4 представлен интерфейс с программами работы с родовым деревом поиска, все элементы которого имеют одинаковый тип. Определяются структуры TREE и NODE. Структура данных родового дерева изображена на рис. 9.2. TREE NODE JfinF info left right elem_size display less_tban equal next — infc ) loft rigl it , NODE info left right Рис. 9.2. Родовое дерево поиска. Определяются три типа, устанавливающие указатели на функции. Тнп displayfunction задает указатель на функцию, имеющую один параметр data и возвращающую зна-
296 Глеев 9 чение типа void. Тип less_than_function задает указатель на функцию, использующую два параметра, datal и data2, которые указывают на значения типа char и возвращают значение типа int. Тип equalfunction задает указатель на функцию, использующую два параметра, datal и data2, которые указывают на значения типа char, и возвращающую значение типа int. Три рассмотренных определения типа являются прототипами для описываемых пользователем функций, используемых в функции define. Поскольку пакет программ для родового дерева не рассчитан на работу с каким-то конкретным базовым типом данных, то средствами пакета невозможно ня определить способ сравнения двух элементов базового типа, ни задать способ вывода значений элементов. Функции disp, lthan и eq, определяемые пользователем, связываются с корневым узлом дерева с помощью функции define. С применением этой же функции в корень дерева заносится и размер элемента в байтах. Для обеспечения целостности работы программ пакета вызовам функций должно предшествовать обращение к функции define. Первым параметром каждой из трех функций define, insert и take_out является указатель на структуру TREE. Любая из приведенных функций может что-либо изменить в корневом узле дерева. Так, функция define всегда изменяет корневой узел. Функции insert и take_out изменяют корневой узел, если только к дереву добавляется новый элемент нли из дерева удаляется элемент. Листинг 9.4. Интерфейс с программами работы с родовым деревом /* Файл gentree.h */ #define TREE struct TREE #define NODE struct node typedef void ( *displayJ"unction )( char *data ); typedef int( •lessjhanj'unction )( char *datal, char *data2 ); typedef int( *equaU"unction )( char *datal, char »data2 ); TREE {
Родовые структуры данных 297 NODE *next; int elem_size; displayjunction display; lessjhanj'unction less_than; equal_function equal; »; NODE { char *info; NODE *left, «right; }; extern void define* TREE *»tree, int size, displayJ"unction disp, less_than_i"unction lthan, equaUunction eq ); extern void insert( TREE **tree, char *item ); extern void take_out( TREE *»tree, char *item ); extern void display_tree( TREE *root ); extern int is_present< TREE *root, char *item ); extern void destroy* TREE **root ); На листинге 9.5 приведена реализация программ работы с родовым деревом. Первая функция на листинге 9.5 create_node невидима для загрузчика, так как снабжена спецификатором static. Обращение к этой функции выполниется нз функции insert. Очевиден родовой характер реализации функции create_node. Новый узел дерева описывается как объект типа NODE. Память отводится как для структуры узла, так и для информационного поля into. Пересылка данных из элемента в узел осуществляется побайтно. В качестве результата функции возвращается адрес нового узла. В функции destroy описывается локальный указатель current, причем ему присваиваетси значение поля next структуры «root. Затем вызывается рекурсивная функция postorder, параметром которой является указатель current. Обход дерева вглубнну реализуется фрагментом программы
298 Глав* 9 if ( current ) { post_order( current -> left ); post_order( current -> right ); free( current -> info ); free( current ); } где для доступа к узлам применяется рекурсия, при которой всегда вначале обращаются к левому, а затем к правому потомку узла. Под доступом подразумевается, что сначала освобождается память для поля info структуры NODE, а только потом - для самой структуры NODE. Обращаем внимание читатели на то, что при использовании алгоритма обхода дерева вглубину узлы удаляются от нижних к верхним, причем связи между узлами никогда раньше времени не разрываются. В функции define отводится память под новый корневой узел. Затем в этот узел заносятся размер элемента в байтах (elemsize) и указатели иа определяемые пользователем функции disp,' lthan и eq. В функциях take_out и insert используются те же алгоритмы, что и приведенные на листинге 8.17. Основное отличие заключается в том, что здесь для сравнения полей last_name заданной структуры применяется не функция strcmp (как на листинге 8.17), а используются определяемые пользователем функции equal и lessthan. В функции display_tree описывается локальный указатель current, которому присваивается значение адреса корневого узла дерева. Затем вызывается функция traverse. Вторым параметром функции traverse передается адрес определяемой пользователем функции display. В функции traverse, описанной как static, для обхода дерева вглубь используется следующий фрагмент программы: if ( current ) { traverse( current -> left, display ); ( «display )( current ->' info ); traverse( current -> right, display ); )
Родовые структуры данных 299 При обходе с помощью функции display, указатель на которую передается в качестве параметра, выводятся значения, содержащиеся в каждом узле. В функции is_present после завершения обхода дере-' ва вызывается определяемая пользователем функция equal. Если сравнение значений ключей с помощью функции equal показало их совпадение, то функция is_present возвращает 1, в противном случае результат функции - нуль. Листинг 9.5. Реализация программ работы с родовым деревом поиска /* Файл gentree.c */ #include <string.h> #include <alloc.h> «include "gentree.h" static NODE* create_node( char *item, int size ) { NODE *node; int i; node = ( NODE * ) mallocf sizeoff NODE ) ); node -> info = ( char * ) malloc( size ); /* Выполняется побайтная пересылка */ for ( i = 0; i < size; i++ ) node -> info! i 1 = item! i ]; return node; } static void post_order( NODE «current ) < if ( current ) { post_order( current -> left ); post_order( current -> right ); free( current -> info ); free( current ); } } void destroy( TREE **root ) { NODE «current = ( «root ) -> next; post_order( current ); ( *root ) -> next = 0; }
300 Глава 9 Void definef TREE *»tree, int size, display Junction disp, less_than_funciion ltban, equal_function eq ) { •tree = ( TREE * ) mallocf sizeof( TREE ) ); ( *tree ) -> next = 0; ( *tree ) -> display = disp; ( *tree ) -> less_than = ltban; ( *tree ) -> elem_size = size; ( *tree ) -> equal = eq; } int take_out( TREE *»root, char *item ) { /* См. страницы 285 и 286 книги DATA STRUCTURES WITH MODULAR, Sincovec. Wiener, Wiley, 1986 •/ NODE «previous = 0, •present = ( Toot ) -> next, •replace, *s, •parent; int found = 0; while ( present && !found ) { if ( ( «root ) -> equal( present -> info, item ) ) found = 1; else { previous = present; if ( ( *root ) -> less_than( item, present -> info ) ) present = present -> left; else present = present -> right; } } if ( found ) { if ( present -> left == 0 ) replace = present -> right; else
Родовые структуры данных 301 if ( present -> right == 0 ) replace = present -> left; else { parent = present; replace = present -> right; s = replace -> left; while ( s != 0 ) { parent = replace; replace = s; s = replace -> left; } if ( parent != present ) { parent -> left = replace -> right; replace -> right = present -> right; } replace -> left = present -> left; } if ( previous == 0 ) ( Toot ) -> next = replace; else if ( present == previous -> left ) previous -> left = replace; else previous -> right = replace; free( present -> info ); free( present ); } > void insert( TREE **root, char *item ) < NODE «parent = 0, •current = ( «root ) -> next; NODE *new_node; int found = 0; while ( current && !found ) { if ( ( *root ) -> equal( current -> info, item ) ) found = 1; else {
302 Глйм 9 parent = current; if ( ( «root ) -> less_than( itea, current -> info ) ) current = current -> left; else current = current -> right; > > if ( found == 0 ) < if ( parent == 0 ) /• Первый узел дерева */ < new_node = create_node( itea, ( «root ) -> elem_size ); new_node -> left = nevjiode -> right = 0; ( «root ) -> next = new_node; > else < new_node = create_jiode( item, ( Toot ) -> elem_size ); new_node -> left = new_node -> right = 0; if ( ( «root ) -> less_than( itea, parent -> info ) ) parent -> left = new_node; else parent -> right = new_node; > > } static void traverse( NODE «current, displayjunction display ) < if ( current ) < traverse( current -> left, display ); ( «display )( current -> info ); traverse( current -> right, display ); > > void display_tree( TREE «root ) <
Родовые структуры данных 303 NODE «current = root -> next; traverse( current, root -> display ); ) lnt is_present( TREE «root, char «item ) { NODE «current = root -> next; int found = 0; while ( current && !found ) < if ( root -> equal( item, current -> info ) ) found =1; else { if ( root -> less_than( current -> info, item ) ) current = current -> left; else current = current -> right; > > return found; > Обращаем внимание читателя на две вполне допустимые синтаксические конструкции, используемые для вызова функций на листинге 9.5. Обращение вида ( «root ) -> less_than( item, current -> info ) ) в теле функции insert соответствует форме вызова функции имя_функции( параметры ) В функции traverse обращение ( «display И current -> info ) соответствует форме ( «имя_функции )( параметры ) На листинге 9.6 приведена тестовая программа, использующая пакет программ работы с родовым деревом поиска
304 Глава 9 В тестовой программе оределяется структура RECORD, содержащая два поля. Первое поле пате служит ключом, иа основе которого строится дерево. Второе поле id содержит числовое значение типа float. Дерево my_tree описывается как глобальная структура. В качестве параметров функции define передаются три определяемые пользователем функции, displayrecord, lessthan и equal. В качестве первого параметра функции define передается адрес структуры my_tree. Указатель item на структуру RECORD многократно используется для занесения в дерево новых элементов. Затем один из элементов удаляется из дерева. Значения элементов дерева выводятся на печать, после чего дерево уничтожается, а потом частично восстанавливается. В заключение значения элементов дерева распечатываются еще раз. Программа иа листинге 9.6 выводит следующие результаты: Имя -> Ааааа Номер -> 1.234500 Имя -> Ссссс Номер -> 3.234500 Имя -> Ddddd Номер -> 4.234500 Имя -> Еееее Номер -> 5.234500 Имя -> Fffff Номер -> 6.234500 Имя -> Ddddd Номер -> 4.234500 Имя -> Fffff Номер -> 6.234500 Листинг 9.6. Тестовая программа для родового дерева поиска «include <stdio.h> «include <alloc.h> «Include <stdlib.h> «Include <slring.h> «Include "gentree.h" TREE *my_tree; «define RECORD struct record
Родовые структуры денных 305 RECORD { char name! 30 1; float Id; >; main!) < extern void display_record( char «info ); extern int less_than( char *iteml, char »item2 ); extern int equal( char «iteml, char *item2 ); RECORD «item = ( RECORD • ) mallocl sizeoff RECORD ) ); RECORD *value; definef &my_tree, sizeof( RECORD ), display_record, less_than, equal ); strcpyt item -> name, "Ccccc" ); item -> id * 3.2345; insert( &my_tree, ( char • ) item ); strcpyl item -> name, "Eeeee" ); item -> id = 5.2345; insert( &my_tree, ( char * ) item ); strcpy( item -> name, "Aaaaa" ); item -> id = 1.2345; insert( &my_tree, ( char * ) item ); strcpyt item -> name, "Ddddd" >; item -> id = 4.2345; insert( &my_tree, ( char * ) item ); strcpyt item -> name, "Fffff" ); item -> id = 6.2345; insert( &my_tree, ( char * ) item ); strcpyl item -> name, "Bbbbb" ); item -> id = 2.2345; insert( &my_tree, ( char * ) item ); take_out< &my_tree, ( char * ) item );
306 Главе 9 display_tree( my_tree ); destroy( &my_tree ); strcpy( item -> name, "Ddddd" ); item -> id = 4.2345; insert( &my_tree, ( char * ) item ); strcpyt item -> name, "Fffff" ); item -> id = 6.2345; insert( &my_tree, ( char * ) item ); display_tree< my_tree ); printff "\n" ); > void display_record< char «info ) < RECORD «temp = ( RECORD * ) info; printft "\пИмя -> V.s", temp -> name ); printf( "ЧпНомер -> Xf", temp -> id ); > int less_than( char «iteml, char *item2 ) < RECORD «first = ( RECORD • ) iteml; RECORD «second = ( RECORD * ) item2; return strcmpf first -> name, second -> name ) < 0; } int equal( char «iteml, char *item2 ) < RECORD «first = ( RECORD • ) iteml; RECORD «second •= ( RECORD * ) itea2; return strcmpt first -> name, second -> name ) == 0; > 9.3. Абстракции данных, объектио-орнентнрованное программирование н разработка программного обеспечения Абстрактные типы данных, такие, например, как списки и деревья, характеризуются набором допустимых значений и соответствующим набором операций над этими значениями. Для действительно абстрактного типа данных пра-
Родовые структуры данных 307 вильность их использования гарантируется тем, что над данными указанного базового типа разрешаются только те операции, которые присутствуют в заданном наборе. Такой подход называют инкапсуляцией данных. Приведенные в разд. 9.1 и 9.2 интерфейсы и реализации родового списка и дерева поиска определяют набор операций для произвольного базового типа элементов таких структур данных. Манипуляции с переменными типа LIST и TREE можно осуществлять из любого внешнего файла (модуля), если для этого будут использованы функции, специфищ.рованные в фалах genlist.h или gentree.h. К сожалению, внутренние структуры родового списка и родового дерева никак не защищены системными средствами. Например, в тестовую программу на листинге 9.6 пользователь может включить оператор my_tree -> next -> right = my_tree -> next -> left; Хотя такой оператор синтаксически вполне корректен, он не имеет смысла. В (Турбо) Си компилятор и загрузчик не защищают от некорректного обращения к структуре. (Турбо) Си не является объектно-ориентированным языком. В истинном объектно-ориентированном языке внутренняя структура типа данных может быть спрятана и защищена от действий, аналогичных приведенным выше. Правильность доступа обеспечивается тем, что все действия над базовым типом осуществляются только посредством заданного набора операций. Новейшими объектно-ориентированными языками являются Си++ и Objective С. Они основываются на Си и полностью поддерживают объектно-ориентированное программирование. В указанных языках внутренняя структура типа данных, аналогичная LIST или TREE, может быть полностью защищена. Доступ к внутренним структурам таких типов из внешних модулей невозможен. По проблематике объектно-ориентированного программирования и связи такого подхода с Си советуем обратиться к книгам "The C++ Programming Language" (Stroustrup, 1986), "An Introduction to Object-Oriented Programming and C++" (Wiener and Pinson, 1988) или "Object-Oriented Programming - An Evolutionary Approach" (Cox, 1986).
308 Глава 9 Повторная используемость программного обеспечения в (Турбо) Си частично обеспечивается на уровне исходного кода, а частично на уровне объектного кода. Связь между модулями на уровне исходного кода осуществляется посредством директив #include. На уровне же объектного кода связь обеспечивается загрузчиком, который собирает объектные коды программ из многих файлов и строит из них исполняемый объектный файл. Во время компиляции компилятор Си не имеет возможности осуществлять проверку взаимных ссылок между обрабатываемыми файлами, чтобы получить гарантию того, что интерфейсы с внешними функциями заданы правильно или что такие функции вообще существуют! Эта работа выполняется загрузчиком. Как только в программе на (Турбо) Си появляется описание extern, имя функции и ее интерфейс заносятся в список. Позднее загрузчик просматривает список и согласует внешние ссылки. К сожалению, сборка производится только на конечном этапе процесса создания программы. Компиляция выполняется на более раннем этапе, практически одновременно с программированием. Поэтому проверка взаимных ссылок загрузчиком может оказаться слишком запоздалой и поэтому малополезной. В новейших языках разработки программного обеспечения, таких, как Модула-2 и Ада, проверка взаимных ссылок выполняется на ранней стадии, уже при компиляции. Когда компилируется модуль, написанный на одном из таких языков, то сразу же проверяются все ссылки на внешние функции и устанавливается их соответствие реальным спецификациям. Все несоответствия сразу же выявляются. Такой способ организации работы весьма разумен, поскольку стоимость исправления ошибок на ранних стадиях процесса программирования гораздо ниже стоимости исправления ошибок на более поздних этапах. С другой стороны, (Турбо ) Си предоставляет программисту больше гибкости в определении физической структуры программной системы, чем указанные объектно- ориентированные языки. Реализацию типа данных можно рассредоточить по нескольким файлам. Пользователь может включить в итоговую программу только то подмножество функций, которые фактически требуются для конкретного приложения; в языке Модула-2 пользователь вынужден
Родовые структуры данных 309 включать в итоговую программу сразу все функции, связанные с конкретным типом данных. Суммируя вышеизложенное, отметим, что качество программной системы зависит от разумности разбиения программистом всей программы на логически однородные модули, каждый из которых должен иметь хорошо продуманный интерфейс и тщательно документированный текст реализации.
Глава 10 Ввод и вывод в Турбо Си Система Турбо Си, как и большинство систем программирования на Си, - это нечто большее, чем просто компилятор и загрузчик. Турбо Си предоставляет программисту возможность работать более чем с 300 полезными библиотечными функциями и макро. Большинство таких функций и макро совместимы (единообразны во многих системах программирования на Си). Для всех библиотек Турбо Си созданы интерфейсные файлы (файлы с расширением . h), в которых содержатся прототипы всех библиотечных функций. В библиотеку включены функции и макро, поддерживающие ввод и вывод на терминал, ввод и вывод текстов, работу с файлами. Сам язык (Турбо) Си не содержит средств поддержки ввода/вывода. Каждая реализация Си должна сопровождаться библиотечными функциями и макро, обслуживающими ввод/вывод. По мере развития языка Си функции ввода/вывода становились стандартизированными. Такая стандартизация обеспечивала высокий уровень мобильности программ на Си. В настоящей главе будет представлен стандартный набор средств ввода/вывода, поддержанных как в системе Турбо Си, так и во многих других системах программирования иа Си. Большинство описываемых средств рассчитаны на работу в системе UNIX, но могут быть использованы и во многих других операционных окружениях Си. Тем, кто желает узнать, как Турбо Си может служить для построения сложных и специализированных средств ввода/вывода, советуем обратиться к документации фирмы Borland «Crafting Turbo С Software Components and Utilities» (Wiener, 1988). В табл. 10.1 приведен список многих функций Турбо Си, поддерживающих как стандартный ввод/вывод, так и ввод/вывод нижнего уровня MS-DOS. Функции поддержки ввода/вывода сгруппированы в трех библиотеках, интерфейсы с которыми описаны в файлах stdio. h, io. h и conio. h. К сожалению, объем книги не позволяет рассмотреть все такие функции ввода/вывода. Подробное описание каждой функции дано в документации фирмы Borland «Turbo С Reference Guide», поставляемой вместе с системой.
Ввод н вывод в Турбо Си 311 В настоящей главе основное внимание будет сосредоточено на подмножестве наиболее широко используемых функций ввода/вывода. Таблица 10.1. Некоторые функции ввода/вывода Турбо Си « функции access cgets close cprintf с puts с re at creatnew creattemp eof fclose fcloseall fdopen feof ferror fflush fgetc fgetchar fgets filelength fileno fputc fputchar fputs fread freopen fscanf Библиотека io.h conio.h lo.h conio.h conio.h io.h io.h io.h io.h stdio.h stdio.h stdio.h stdio.h stdio.h stdio.h stdio.h stdio.h stdio.h io.h stdio.h stdio.h stdio.h stdio.h stdio.h stdio.h stdio.h Имя функции fseek fwrite getc getch getchar gets getw kbhit lock lseek open perror printf puts read remove rename rewind scanf setbuf setmode sopen sprintf sscanf ungetc ungetch Библиотека stdio.h stdio.h stdio.h conio.h stdio.h stdio.h stdio.h conio.h io.h io.h io.h stdio.h stdio.h stdio.h io.h stdio.h stdio.h stdio.h stdio.h stdio.h io.h io.h stdio.h stdio.h stdio.h io.h 10.1. Потоковый ввод/вывод Термин поток происходит из представления о последовательной структуре информационных записей. Такой поток информации располагается на диске. Базовыми операциями над потоком являются следующие: • считывание блока данных из потока в оперативной памяти (одна или несколько записей); • запись блока данных из оперативной памяти в поток;
312 Глава 10 • обновление блока данных в потоке; • считывание определенной записи данных из потока; • занесение определенной записи данных в поток. Состав потока задается структурой FILE, описание которой содержится в файле stdio.h. Эта структура, скопированная непосредственно из файла stdio.h системы Турбо Си, имеет вид typedef struct < short level; /• Уровень буфера »/ unsigned flags; /• Флаги статуса файла »/ char fd; /• Дескриптор файла »/ char hold; /» Предыдущий символ, если нет буфера»/ short bsize; /» Размер буфера »/ unsigned char «buffer; /* Буфер передачи данных »/ unsigned char *curp; /» Текущий активный указатель»/ short token; /* Для проверки корректности»/ } FILE Глобальные файловые переменные stdin, stdout и stderr инициируются системой перед началом работы любой прикладной программы. В начале работы файл stdin приписывается к клавиатуре, ио с использованием стандартных команд переадресации ввода MS-DOS он может быть переназначен. Такие команды переадресации ввода происходят от появившихся ранее и более устоявшихся команд переадресации в системе UNIX. Для того чтобы переназначить stdin на ввод из файла input_ file, пользователю следует набрать следующую команду запуска своей задачи: programjname < input_file Все функции, рассчитанные на ввод информации из файла stdin, будут теперь получать информацию из файла input_file. Файл stdout приписывается к выводному терминалу, ио также может быть переназначен в файл пользователя (или иа принтер). Для того чтобы переназначить stdout иа файл output_file, пользователю следует набрать следующую команду запуска своей задачи:
Ввод и аыаод в Турбо Си 313 programjname > output_file Все функции, рассчитанные иа вывод информации в stdout, будут выводить теперь ее в файл output_file. Файл stderr приписывается к пользовательскому выводному терминалу и ие может быть переиазиачеи иа другой файл. Таким образом, если пользователь желает послать сообщение иа терминал без учета того, что интерпретация файла по умолчанию могла быть изменена, он должен использовать выводной поток stderr. Функции, реализующие работу с потоковым вводом и выводом в Турбо Си, собраны в библиотеке stdio.h. Все такие функции совместимы с аналогичными функциями в системе UNIX. 10.1.1. Текстовые и двоичные потоки Текстовый поток состоит из последовательности символов, разбитой на строки. Для деления на строки используется управляющий символ '\п\ Текстовые потоки (файлы) оказываются переносимыми с одного типа компьютера на другой, если символы, содержащиеся в символьном потоке, принадлежат стандартному набору символов (т.е. если ие используются дополнительные символы, такие, например, как псевдографика фирмы IBM). Каждая реализация Си должна отображать стандартный набор текстовых символов на конкретную аппаратуру компьютера. В Турбо Си, как и в большинстве реализаций Си для машин с процессором типа 80x86, применяется отображение целых чисел в символы в соответствии со стандартом ANSI. При построении текстовых файлов в Турбо Си разрешено использовать 256 символов, включающих и псевдографические символы фирмы IBM. Жесткий стандарт языка Си ANSI, поддерживаемый системой Турбо Си, предписывает, чтобы реализация обеспечивала работу не менее чем с 254 символами. Двоичный поток - это последовательность значений типа char. Использование двоичных потоков делает программы немобильными при их переносе из одной среды в другую. Любой набор данных может быть представлен как набор символов, но такое представление может изменяться при переходе от одной реализации Си к другой.
314 Глам 10 10.1.2. Символ EOF Символ EOF определяется следующим образом: «define EOF ( -1 ) Этот символ в операциях ввода/вывода служит для обозначения и проверки конца файла. Подразумевается, что символ EOF имеет тип значения символьный знаковый. Если символьный тип - беззнаковый, то использовать EOF нельзя. 10.1.3. Функция fopen Функция fopen используется для открытия потока (файла). Интерфейс с функцией fopen- описывается следующим образом: FILE »fopen ( char «filename, char »type ); В качестве первого параметра функции должно передаваться правильное имя файла. Второй параметр определяет тип открываемого файла. Допустимы следующие типы файлов: "г" Открыть уже существующий файл на ввод. "к" Создать новый файл или очистить уже существующий файл и открыть его на вывод, "а" Создать новый файл- для вывода илн осуществить вывод в конец уже существующего файла. "г+" Открыть существующий файл для обновления, которое будет проводиться с начала файла. nw+" Создать новый или очистить существующий файл для обновления его содержимого. "а+" Создать новый файл или подстроиться в конец существующего файла для обновления его содержимого. Дополнительно к каждой из приведенных строк можно добавить символ 'Ь', указывающий на то, что открывается двоичный файл. Функция fopen возвращает указатель на структуру FILE, описывающую файл. Если при открытии файла прои-
Ввод и вывод ■ Турбо Си 315 зошли какие-либо ошибки, то в глобальную переменную errno будет записан код (номер) ошибки и будет возвращен нулевой (0) указатель. 10.1.4. Функция fflush Функция fflush служит для выталкивания каждого внутреннего буфера на соответствующее устройство. Интерфейс с этой функцией выглядит следующим образом: int fflush ( FILE «stream ); После вызова функции поток остается открытым. Если при выполнении функции появятся ошибки, то будет возвращено значение EOF; в противном случае будет возвращен 0. 10.1.5. Функция freopen Функция freopen служит для закрытия существующих цепочек и открытия их заново. Чаще всего эта функция используется для переназначения стандартных потоков (stdin, stdout, stderr) на другие файлы. Интерфейс с функцией freopen имеет вид FILE *freopen(char «filename,char «type,FILE «stream); 10.1.6. Функция fclose Интерфейс с функцией fclose выглядит следующим образом: int fclose( FILE «stream ); С помощью этой функции в файл выталкиваются соответствующие буферы и указанный поток закрывается. Если закрытие выполняется правильно, то вырабатывается значение нуль, в противном случае вырабатывается значение EOF. 10.1.7. Функции fgetc и get с Интерфейс с функцией fgetc описывается следующим образом:
316 Глава 10 int fge.tc( FILE «stream ); С помощью этой функции из указанного входного потока считывается очередной символ и его значение переводится в тип int. Если при считывании обнаруживается ошибка или достигается конец файла, то возвращается значение EOF. Макро getc действует точно так же, как и функция fgetc. 10.1.8. Функции getchar Интерфейс с функцией getchar выглядит следующим образом: int getcharO; С помощью этой функции из стаидартиого входного потока stdin считывается очередной символ и его значение переводится в тип int. Если переназначения стандартного входного потока не производилось, го ввод осуществляется с клавиатуры. В противном случае ввод будет осуществляться из файла, назначенного для входного потока и указанного в командной строке при вызове программы. 10.1.9. Функция ungetc Интерфейс с функцией ungetc выглядит следующим образом: int ungetc( char ch, FILE «stream ); С помощью этой функции символ ch вталкивается обратно во входной поток. Если сразу же будет вызвана функция fgetc, getc или getchar, то такой символ будет считан первым. Если при вталкивании не произошло ошибок, го будет возвращено значение этого символа, в противном случае будет возвращено значение EOF. Функцию ungetc удобно использовать в том случае, если требуется просмотреть входной поток на одни символ вперед, не нарушая сам поток.
Ввод и вывод в Турбо Си 317 10.1.10. Функция fseek Интерфейс с функцией fseek описывается следующим образом: int fseek( FILE «stream, long offset, int wherefrom ); Эта функция служит для произвольного доступа к байтам, обычно внутри двоичных потоков. Первый аргумент задает поток, к которому должен осуществляться прямой доступ. Второй аргумент offset является длинным целым числом со знаком и указывает число байтов смещения от точки, определяемой третьим параметром функции. Третий параметр wherefrom указывает точку, от которой следует начинать отсчет смещения, заданного вторым аргументом: Значение 0 - смещение от начала файла. Значение 1 - смещение от текущей позиции файла. Значение 2 - смещение от конца файла. Для облегчения работы с функцией fseek определены следующие константы: «define SEEKJ5ET 0 «define SEEK.CUR 1 «define SEEK.END 2 Например, для установки стрелки (указателя) файла на конец файла нужно воспользоваться таким обращением: fseekt stream_name, 0L, SEEK_END ); 10.1.11. Функция rewind Интерфейс с функцией rewind выглядит следующим образом: void rewindt FILE «stream ); С помощью этой функции стрелка файла перемещается на начало потока. Аналогичное действие может быть выполнено и с помощью вызова fseekt streamjname, 0L, SEEK_SET );
318 Гласа 10 10.1.12. Функция fgets Интерфейс с функцией fgets выглядит следующим образом: char *fgets( char *s, Int n, FILE «stream ); С помощью этой функции в строку s считываются символы до тех пор, пока ни будет выполнено одно из условий: 1. Начнется новая строка. 2. Достигнут конец файла. 3. Условия 1 или 2 ие выполнились, но прочитано п-1 символов. После того как из входного потока в строку s будут прочитаны символы, строка дополняется символом нуль. Если при чтении встретился символ конца строки (условие 2), то он переносится в строку s и нулевой символ записывается за иим. Если операция считывания прошла успешно, то возвращается адрес строки s, в противном случае возвращается значение нуль. 10.1.13. Функция gets Интерфейс с функцией gets имеет вид char »gets( char *s ); С помощью функции gets выполняется считывание символов из стандартного входного потока stdin. Если входной поток прерывается символом перехода на новую строку '\п', то этот символ отбрасывается и не попадает в строку s. Поскольку в функции gets (в отличие от функции fgets) не задается числовой параметр, ограничивающий длину вводимой строки, то следут соблюдать осторожность. Ведь число символов, введенных из потока stdin, может превысить размер памяти, отведенной под строку s. 10.1.14. Функции fputc и putc Интерфейс с функцией fputc выглядит следующим образом:
Ввод и вывод • Турбо Си 319 int fputc( char ch, FILE »streaa ); С помощью этой функции символ ch записывается в указанный выходной поток. Если запись прошла успешно, то возвращается значение ch целого типа, в противном случае возвращается значение EOF. Действие функции putc аналогично действию функции iputc, однако первая обычно оформляется в виде макро. 10.1.15. Функция putchar Интерфейс с функцией putchar описывается следующим образом: int putchar( char ch ); Функция putchar действует аналогично функции fputc, но записывает символ ch в стандартный выходной поток stdout. Обращение к функции putchar можно заменить иа эквивалентное: putc( ch, stdout ); 10.1.16. Функция fputs Интерфейс с функцией fputs выглядит следующим образом: int fputs( char »s, FILE «stream ); Строка s, ограниченная символом нуль, переписывается в выходной поток, причем символ нуль отбрасывается. Если при переписи возникает ошибка, то возвращается значение EOF, в противном случае возвращается ненулевое значение. 10.1.17. Функция puts Интерфейс с функцией puts оформляется следующим образом: int puts( char »s ); Эта функция выполняется аналогично функции fputs, за исключением того, что символы переписываются в ставдарт-
320 Глава 10 ный выходной поток stdout и строка s независимо от того, содержит она символы или иет, дополняется символом конца строки '\п\ 10.1.18. Функция fread Интерфейс с функцией fread выглядит следующим образом: lnt fread( void »ptr, unsigned elemjsize, int count, FILE «stream ); С помощью этой функции из входного потока считываются и по адресу *ptr записываются не более чем количество count элементов размером eletn_size байтов каждый. Функция возвращает число фактически считанных элементов. 10.1.19. Функции fwrite Интерфейс с функцией fwrite описывается следующим образом: lnt fwrite( void *ptr, unsigned elem_size, lnt count, FILE «stream ); С помощью этой функции, начиная с адреса *ptr, считываются не более чем количество count элементов размером elem_size байтов каждый, и эти элементы записываются в выходной поток. Функция возвращает число- фактически записанных элементов. 10.1.20. Функция feof Интерфейс с функцией feof выглядит следующим образом: lnt feof( FILE «stream ); Если при чтении из указанного потока достигнут конец, файла, то возвращается значение нуль, в противном случае возвращается ненулевое значение. Если не предпринималась попытка прочитать из файла етсутствующий символ, следующий за последним, то функция feof не будет сигнализировать о том, что достигнут конец файла.
Ввод и вывод в Турбо Си 321 10.1.21. Функции ferror Интерфейс с функцией ferror имеет вид lnt ferror( FILE «stream ); Функция ferror позволяет опросить состояние признака ошибки указанного потока после чтения или записи в него. Возвращаемое нулевое значение показывает, что ошибок не было, в то время как ненулевое значение сигнализирует об ошибке. Для очистки признака ошибки используется функция Clearerr. 10.1.22. Функция clearerr Интерфейс с функцией clearerr описывается следующим образом: void clearerr( FILE «stream ); С помощью этой функции устанавливается в нуль состояние признака ошибки в указанном потоке. Признак ошибки потока устанавливается в нуль и при закрытии потока. 10.1.23. Функции rename Интерфейс с функцией rename выглядит следующим образом: lnt rename( char «oldname, char «newname ); С помощью указанной функция имя файла oldname меняется на новое имя newname. Если переименование прошло успешно, то возвращается значение нуль, в противном случае возвращается ненулевое значение. 10.1.24. Функции fprintf, printf, sprintf и cprintf Описание действия этой функции частично заимствовано из документация фирмы Borland «Turbo С Reference Manual>. С помощью функций fprintf, printf, sprintf и Cprintf выполняется форматный вывод соответственно в 11 Зак. 795
322 Глава 10 указанный выходной поток (или в стандартный выходной поток stdout), в указанную строку или иа терминал. Рассмотрим интерфейсы с указанными функциями: int fprintf( FILE «stream, char *fon»at, Дополнительные аргументы> ); iot printf( char «format, Дополнительные аргументам ); int sprintf( char *s, char «format, <дополнительные аргументам ); int cprintft char «format,<дополнительные аргументам ); Каждая из описываемых функций в случае возникновения ошибки возвращает значение EOF, в противном случае она возвращает число символов, переданных по назначению. Строка формата состоит из символов, переносимых в соответствующее место без изменения, и символов, задающих преобразование данных. Для такого преобразования могут понадобиться дополнительные аргументы, не указываемые в строке формата Спецификации преобразования формата имеют следующий вид: % [флаги] [ширина] [.точность! [ F I N I h I 1 ] тип где: [флаги] - необязательная последовательность символов, указывающих форму вывода; [ширина! - необязательное указание ширины поля для вывода; [.точность] - необязательная спецификация точности представления данных при выводе; [FlNihil] - необязательный указатель размера (типа) соответствующего элемента данных; тип - указатель типа соответствующего элемента данных. Более строго, необязательные флаги задают способ выравнивания данных в поле вывода, предписывают вывод
Ввод и вывод в Турбо Си 323 знака у числа, определяют положение десятичной точки, регулируют исключение правых нулей, задают восьмеричные или шестиадцатеричиые префиксы. Ширина определяет минимальное число выводимых символов, дополииемых пробелами или нулями. Точность задает максимальное число символов для вывода. Для переменных целого типа точность определяет минимальное число цифр при выводе. Размер аргумента задается следующим образом: N = близкий указатель; F = дальний указатель; h = короткое целое; I = длинное целое. В табл. 10.2 приведены символы преобразования формата и указаны их значения. Таблица 10.2. Символы преобразования формата для функций семейства printf Символ Исходный аргумент Представление при выводе d 1 о U х.Х f С е С g с Целое Целое Целое Целое Целое плав, точкой плав, точкой плав, точкой Е С плав, точкой G С плав.точкой 11* Числа Деситичное целое со знаком Десятичное целое со знаком Восьмеричное целое без знака Десятичное целое без знака Шестнадцатеричное целое без знака для х: а, Ь. с, d, e, f для X: А, В, С, D, Е, F Значение со знаком в форме f-ldddd.dddd Значение со знаком в форме [-lld.dddd e[+/-lddd Значение со знаком в форме е или f в зависимости от значения и точности. Правые нули и десятичная точка выводятся, только если требуется Аналогично е, но для указания экспоненты используется символ Е Аналогично g, но при использовании формата е для указания экспоненты служит символ Е
324 Глава 10 Символы с Символ Простой символ s Указ. на строку Выводит строку до тех пор, пока либо встретится ограничитель строки (символ нуль), либо будет достигнута заданная точность % Вывод символа % Указатели п Указ. на целое По адресу, задаваемому аргументом, помещается число символов, выведенных к текущему моменту р Указатель Выводится адрес указателя. Дальние указатели представляются в форме SSS&OOOO, а близкие - в форме ОООО (смещение) Примечания Для символов е или Е • Перед десятичной точкой стоит всегда по крайней мере одна цифра. • Количество цифр после десятичной точки равно указанному значению точности. • Десятичный порядок всегда содержит три цифры. • Десятичная точка выводится только тогда, когда число действительно содержит дробную часть. Для символа f • Аргумент переводится в десятичную форму как [-]ddd.ddd..., причем количество цифр после десятичной точки равно указанному значению точности. • Десятичная точка выводится только тогда, когда число действительно содержит дробную часть. Для символа g или G • Аргумент выводится согласно формату е, Е яли f, причем точиость задает количество значащих цифр. Незначащие нули не выводятся. Десятичная точка выводится только тогда, когда число действительно содержит дробную часть.
Ввод и вывод в Турбо Си 325 • Формат е используется только тогда, когда порядок числа, получаемого после преобразования, либо больше чем заданная точность, либо меньше чем -4. В табл. 10.3 приведены символы флагов и их значения. Таблица 10.3. Флаги и их значения Выравнивать результат по левому краю поля и дополнять справа пробелами. Если флаг отсутствует, то результат выравнивать по правому краю н дополнять слева нулями, или пробелами + Всегда выводить знак плюс (+) или минус (-) Пусто Если значение неотрицательно, то начать поле с пробела, а не со знака плюс. Отрицательные значения предваряютси знаком минус # Указывает, что аргументы должны быть преобразованы с использованием альтернативных форм Альтернативные формы Символ преобразования Действие с, s, d, i, u He оказывает воздействия о Ненулевой аргумент будет предварен символом О х или X Аргумент будет предварен символами Ох или ОХ е, Е или f Результат всегда будет содержать десятичную точку, даже если в дробной части нет значащих цифр g или G Аналогично е или Е с той разницей, что ие будут выводиться незначащие нули В табл. 10.4 приведены спецификаторы ширины и их значения. Таблица 10.4. Спецификаторы ширины и их значения Спецификатор Действие ыирины п Выводится по крайней мере п символов. Если выводимое значение содержит менее п символов, то поле дополняетси пробелами (спра-
326 Глава 10 ва, если задано с->, или слева в противном случае) On Выводится по крайней мере п символов. Если значение содержит менее чем п символов, то оно дополняется нулями слева * Список аргументов содержит спецификатор ширины, помещаемый в списке непосредственно перед выводимым аргументом В табл. 10.5 приведены спецификаторы точности и указано их иазиачеиие. Таблица 10.5. Спецификаторы точности и их иазиачеиие Спецификатор Действие точности Пусто Умолчание: 1 для d, i,o,u,x,X; 6 для е, Е, i; все значащие цифры для g и G; нуль для s . О Для d, i, о, и, х устанавливается значение точности по умолчанию. Для е, Е и f точка ие выводится . п Выводится п символов или п десятичных позиций. Если выводимое значение содержит более п символов, то выводимый текст будет усечен нли округлен * В списке аргументов должен присутствовать аргумент, содержащий спецификацию точности и находящийся в списке аргументов непосредственно перед выводимым аргументом Влияние точности на преобразование d, i .n указывает на то, что рледует вывести по крайней мере п цифр о, и Если выводимый аргумент содержит менее п цифр, х, X то выведенное значение дополняется нулями слева. Если выводимый аргумент содержит более п цифр, то выводимое значение ие усекается
Ввод и вывод в Турбо Си 327 е, Е .п указывает на то, что после десятичной точки следует вывести п символов f Последние цифры округлиются g, G .n указывает на то, что следует вывести по крайней мере, п значащих цифр с .п не оказывает влияния на выводимые величины s .л указывает на то, что следует вывести не более п символов Пример использования спецификатора точности .* и спецификатора ширины * приведен иа листинге 10.1. Программа с листинга 10.1 напечатает следующее: г = 1.235 Ширина 20 и точность 1 г = 1.2 Листинг 10.1. Использование спецификатора точности .* ftinclude <stdio.h> main () { double г = 1.2345678; int precision = 3; int width = 20; printf ( "\nr = '/..•£", precision, г ); precision = 1; printf ( "ЧпШирина Ы и точность %d г = JC*.»f\n", width, precision, width, precision, r ); > Модификаторы размера (F, N, h и 1) указывают на то, как функции семейства printf интерпретируют свои аргументы. Модификаторы F и N относятся только к аргументам - указателям (%р, %s, %n), a h и 1 относятся к числовым аргументам. В табл. 10.6 приведены спецификаторы размера и указано их назначение.
328 Главе 10 Таблица 10. в. Спецификаторы размера и их назначение Спецификатор Действие размера F Аргумент трактуется как дальний указатель N Аргумент трактуется как близкий указатель Нельзя использовать в модели памяти huge h Аргумент трактуется как короткое целое для d, i, о, и, х нли X 1 Аргумент трактуется как длинное целое для d, i, о, и, х или X d Аргумент трактуется как вещественное значение с двойной точностью для е, Е, f, g или G 10.1.25. Функции vfprintf или vprintf Рекомендуем читателю, прежде чем приступить к чтению настоящего раздела, обратиться к разд. 8.8, где описываются функции с переменным числом параметров. Интерфейс с функциями vfprintf и vprintf имеет следующий вид: int vfprintf( FILE «stream, char «format, va_list argptr ); int vprintf( char «format, va_list argptr ); Функция vfprintf осуществляет вывод в указанный поток, a vprintf осуществляет вывод в стандартный поток stdout. Как уже было показано в разд. 8.8, функцию vprintf можно использовать для вывода переменного числа параметров. Функции list_names (список_имеи) приведена на листинге S.24 и имеет следующий вид: void listjiamesf char «format, ... ) { va_list argptr; printff "\п\пСписок имен ->\п" ); va_start( argptr, Message ); vprintf( format, argptr ); va_end( argptr ); }
Ввод и вывод в Турбо Си 329 Функцию можно вызвать так: list_names( "%s\nXs\nXs\n", "Ричард", "Эрик", "Марк" ); list_names( ,7.s\n,/.s\n\ "Шейла", "Ивинг" ); 10.1.26. Функция ftell Интерфейс с функцией Hell оформляете и следующим образом: long ftelll FILE «stream ); Результат функции - текущий указатель на файл. 10.1.27. Функции scant, fscant, sscanf и cscanf Интерфейс с функциями scant, fscanf, sscanf и cscanf имеет вид int scanf( char «format, Дополнительные аргументы> ); int fscanf( FILE «stream, char «format, Дополнительные аргументы> ); int sscanf( char «string, char «format, <дополнительные аргументы> ); int cscanf( char «format, <дополнительные аргуиенты> ); С пойощью функции scanf осуществляется ввод из стандартного потока stdin. Функция fscanf вводит из потока, указаииого пользователем. Функция sscanf вводит нз заданной строки. Функция cscanf вводит с консоли. Все функции семейства scant вводят поля символ за символом, переводя их в соответствии с указанным форматом. Первое вводимое поле преобразуется в соответствии с первой спецификацией формата и полученное значение заносится по адресу первого аргумента. Следующее» поле переводится в соответствии со второй спецификацией формата и т.д. Спецификация формата для функций семейства scant имеет следующий вид: У- I * 1 [ ширина I [ F I N 1 [ h I 11 скмвол_типа
330 Глава 10 Значения всех необязательных спецификаций формата для функций семейства scanf приведены в табл. 10.7. Таблица 10.7. Компоненты строки формата Символ Действие * Запрещает присваивание следующего введенного значения Ширина Максимальное число вводимых символов. Меньшее число символов может быть прочитано, если встретится разделитель полей или непереводимый символ Размер Изменяет размер, принимаемый по умолчанию для адресного аргумента. (N = близкий указатель, F = дальний указатель) Тип Изменяет тип, принимаемый по умолчанию для аргумента адресного аргумента. (h = указатель на короткое целое, 1 = указатель на длинное целое) В табл. 10.8 приведены спецификаторы типа и их значения для функций семейства scanf. Таблица 10.8. Спецификаторы типа для функций семейства scanf !имво/ d D о 0 i I и и X X е.Е f,F 1 Ожидаемое значение Числа Десятичное целое Десятичное целое Восьмеричное целое Восьмеричное целое Десятичное, восьмеричное или шестнадцатеричное целое Десятичное, восьмеричное или шестнадцатеричное целое Десятичное целое без знака Десятичное целое без знака Шестнадцатеричное целое Шестнадцатеричное целое С плавающей точкой С плавающей точкой Тип аргумента int *arg long *arg int *arg long *arg int *arg long *arg int »arg long *arg int »arg long *arg float *arg float *arg
Ввод и вывод в Турбо Си 331 Символы s Строка символов char arg[] с Символ char *arg X Символ % Без преобразования Указатели п Нет int *arg. Количество символов, прочитанных до JJn, помещается по данному указателю. Р Шестнадцатеричиые числа в формате SSSS:0000 или 0000 Поля ввода определяются по следующим правилам: • Группа символов до (но ие включая) символа - разделителя. • Группа символов до такого символа, который ие может быть преобразован в соответствии с текущей спецификацией. • Группа из п символов, где п - указатель ширины поля. Соглашении по организации ввода данных с помощью функции scanf весьма сложны, и их рассмотрение выходит за рамки иастоищей книги. Подробнее о работе функций ' семейства scanf пользователь может узнать из документации фирмы Borland «Turbo С Reference Guide> на с. 198-202. 10.1.28. Функция setbuf Интерфейс с функцией setbuf выглядит следующим образом: votd setbuf( FILE «stream, char «buffer ); С помощью этой функции системе ввода/вывода предлагается для выполнения обменов использовать буфер, указанный в качестве параметра функции, вместо буфера, подключаемого автоматически. Если указан нулевой буфер, то ввод/вывод будет небуферизованиым, в противном слу-
332 Глава 10 чае он будет полностью буферизоваться. Размер буфера должен быть равен BUFSIZ байтам (соответствующая константа описана в файле stdio.h). Если обмены не буферизуются, то символы передаются потребителю напрямую, без предварительного накопления. Обращаем внимание на то, что следует обязательно закрывать файлы, работа с которыми ведется через динамически определяемые буферы пользователя. Закрытие рекомендуется осуществлить при выходе из функции, в которой такие буфера заводятся. 10.2. Ввод и вывод нижнего уровни Кроме функций ввода/вывода, работающих с потоком данных н описанных в разд. 10.1, Турбо Си поддерживает И другие совместимые с UNIX функции обмена нижнего уровня. Прототипы таких функций содержатся в файле io.h. К указанным функциям относятся access close creat dup dup2 eof filelength getftlme isatty lseek open read setftime setmode tell write В последующих разделах будет дано краткое описание перечисленных функций. 10.2.1. Функция access Интерфейс с функцией access имеет следующий вид: int access( char «filename, int amode ); Функция access используется для проверки того, существует ли указанный файл, можно ли прочитать его содержимое, можно лн писать в файл или является ли файл исполняемым. Битовая шкала для параметра amode может принимать следующие значения: 06 Проверка разрешения чтения и записи 04 Проверка разрешения чтении 02 Проверка разрешения записи
Ввод и вывод в Турбо Си 333 01 Исполниемый файл 00 Проверка наличии файла В MS-DOS все существующие файлы имеют доступ по чтению, и поэтому использование значений параметра amode 00 и 04 дает один и тот же результат. Результаты работы функции со значениями параметра amode 06 н 02 в MS-DOS также совпадают. Если имя файла - это (под) директория, то функция access определяет, существует ли такая (под)директория или нет. Функция access возвращает в качестве результата 0, если требуемый вид доступа разрешен, и -1 в противном случае. При этом переменной errno присваиваются значения: EN0ENT - Директория или файл не найдены. EACCES - Указанный вид доступа не разрешен. 10.2.2. Функция close Интерфейс с функцией close описывается следующим образом: int closet int handle ); Описатель файла (handler) может быть получен из одной из функций creat, creatnew, creattemp, dup, dup2 или open, которые будут описаны ниже. Функция close закрывает работу с указанным файлом. Она возвращает 0, если закрытие осуществилось успешно, н -1 в противном случае. 10.2.3. Функция creat Интерфейс с функцией creat выглядит так: int creat( char «filename, int permiss ); С помощью функции creat создается новый файл или осуществляется подготовка к перезаписи уже существующего файла. Имя файла задается первым аргументом функции. Параметр permiss относится только к новым файлам.
334 Глава 10 Значения параметра permiss определены в файле sys\stat.h: S_IHRITE Разрешена запись S_IR£AD Разрешено чтение S_IREAD I S_IWRITE Разрешены и запись, н чтение В MS-DOS разрешение записи означает н разрешение чтения. Если файл уже существует и параметр permiss задает разрешение записи, то функция creat усекает файл до нуля байтов, не меняя при этом атрибутов файла. Если существующий файл имел атрибут, разрешающий только читать из файла, то функция creat не выполняется и файл не изменяется. 10.2.4. Функции dup и dup2 Функция dup реализована дли всех версий системы UNIX, в то время как функция dup2 отсутствует в версии UNIX III. Интерфейс с этимк функциями выглядит следующим образом: lnt dup( int handle ); int dup2( int oldhandle, int newhandle ); Функции dup и dup2 строят новый описатель файла, который: • соответствует тому же открытому файлу, что и исходный описатель; • имеет тот же указатель файла, что и исходный описатель; • имеет те же права доступа, что и исходный описатель. Функция dup2 возвращает следующий доступный описатель файла. Если функция dup возвращает новый описатель файла, то такой описатель является неотрицательным целым числом, в противном случае функция возвращает -1. При удачном завершении действий функция dup2 воз,-
Ввод и вывод в Турбо Си 335 вращает 0, в противном случае возвращает -1. Если произошла ошибка, то переменной еггпо присваивается одно нз значений: EMFILE Открыто слишком много файлов EBADF Неверный номер (описатель) файла 10.2.5. Функция eof Интерфейс с функцией eof определяется следующим образом: int eof( int handle ); С помощью этой функции можно выяснить, достигнут ли конец файла, связанного с указанным, описателем. Если текущая позиция файла - его конец, то функция eof возвращает 1, в противном случае она возвращает 0. Результат -1 свидетельствует об ошибке. В этом случае переменной еггпо присваивается значение EBADF - неверный номер файла. 10.2.6. Функция filelength Интерфейс с функцией filelength выглядит следующтм образом: Int filelength! int handle ); Функция возвращает длину файла в байтах. Если возникает ошибка, то функция возвращает -1L и переменной еггпо присваивается значение EBADF - неверный номер файла. 10.2.7. Функции getftime и setftime Рассматриваемые функции реализованы только для MS-DOS и являются несовместимыми с системой UNIX. Интерфейс с функциимн выглядит так: int getftime( int handle, struct ftime *ftime_date ); int setftime( int handle, struct ftime *ftime_date );
336 Глааа 10 С помощью этих функций можно получить нлн записать новые время и дату для файла с заданным «писателем. Время и дата размещаются в структуре ftime_date, указатель на которую передается вторым параметром функции. Структура определяется так: struct ftime { unsigned ft_tsec unsigned ftjnin unsigned ftjiour unsigned ft_day unsigned ftjnonth unsigned ft_year } В случае успешного завершения действий функции возвращают 0, в противном случае они возвращают -1. Если возвращается -1, то переменной errno присваивается одно из следующих значений: EINVFNC Неверный номер функции EBADF Неверный номер файла 10.2.8. Функция isatty Интерфейс с функцией isatty выглядит следующим образом: int isatty( int handle ); Функция проверяет, является ли файл с указанным Описателем терминалом, консолью, принтером или последовательным портом. Если файл является одним из таких устройств (обмен со всеми перечисленными устройствами осуществляется побайтно), то функция возвращает ненулевое целое число, в противном случае функция возвращает нуль. 10.2.9. Функция (seek Интерфейс с функцией lseek имеет следующий вид: long lseek( int handle, long offset, int fromwhere );
Ввод и вывод в Турбо Си 337 С помощью функции lseek указатель соответствующего файла можно установить на позицию, отстоящую на offset байтов от значения fromwhere. Параметр fromwhere может принимать одно нз трех значений, связанных с соответствующими константами: БЕЕК_БЕТ (0) Начало файла SEEK_CUR (1) Текущий указатель файла SEEK_£ND (2) Конец файла Функция возвращает смещение новой позиции указателя файла, измеренное от начала файла. Если возникли ошибки, то функция возвращает -1 и переменной еггпо присваивается одно из следующих значений: EBADF Неверный номер файла EINVAL Неверный аргумент 10.2.10. Функция open Для работы с функцией open следует включить в программу файлы <fcntl.h> и <sys\stat.h>. Интерфейс с функцией open имеет следующий вид: int open( char «filename, int access I, int permissl ); Функция open открывает файл с указанным именем и подготавливает его для чтения нли записи в зависимости от того, что указано в параметре access. Значения параметра access разбиваются на две группы. Только одно из значений первой группы можно использовать в качестве значения параметра. Значения нз второй группы могут применяться в сочетаниях. Группа 1 - значения переменной access 0_RDONLY Открыть только на чтение 0_WR0NLY Открыть только на запись 0_RDWR Открыть на чтение н запись Группа 2 - значения переменной access 0_APPEND Перед началом любой записи указатель устанавливается на конец файла
338 Глава 10 0_CREAT Если файл уже существует, то данный флаг не оказывает никакого действия. Иначе файл создается и биты параметра permiss используются для установки атрибутов файла 0_TRUNC Если файл существует, то его длина усекается до нуля 0JINARY Файл открывается в двоичном режиме 0_ТЕХТ Файл открывается в текстовом режиме Если флаг 0_CREAT установлен, то необязательный аргумент permiss может принять одно из следующих значений: S_IHRITE Разрешена запись S_IREAD Разрешено чтение S_IREAD I S_IWRITE Разрешены н запись, н чтение Если функция open заканчивается успешно, то она возвращает описатель файла и устанавливает указатель файла на его начало. Если возникает ошибка, то возвращается значение -1 н переменной еггпо присваивается одно из следующих значений: EN0ENT Директория или файл не найдены EMFILE Открыто слишком много файлов EACCES Неразрешенный вид доступа EINVACC Неверный код доступа 10.2.11. Функция read Интерфейс с функцией read описывается следующим образом: int read( int handle, void «buffer, int nbytes ); С помощью функции read осуществляетси считывание nbytes байтов из файла с заданным описателем в указанный буфер. Если файл открыт в текстовом режиме, то операция чтения игнорирует коды «возврат кареткн> и определяет конец файла по коду CTRL-Z. Описатель файла получают с помощью одной из следующих функций: creat, open, dup или dup2. Описание ука-
Ввод и вывод в Турбо Си 339 занных функций приведено в подразделах настоящего раздела. После осуществлении чтения указатель файла продвигается на число прочитанных байтов. Если чтение прошло успешно, то функция возвращает число прочитанных байтов. При чтении в текстовом режиме не учитываются коды «возврат кареткн> и Ctrl-Z. Если при чтении достигнут конец файла, то возвращается значение нуль. Если были ошибки при чтении, то возвращается -1 и переменная еггпо принимает одно из следующих значений: EACCES Неразрешенный вид доступа EBADF Неверный номер файла 10.2.12. Функция setmode Интерфейс с функцией setmode выглядит следующим образом: int setmode( int handle, unsigned mode ); С помощью функции setmode для открытого файла с заданным описателем устанавливается режим в соответствии с одним из следующих значений, указанных в файле <fcntl.h>: 0JINARY Двоичный режим 0_ТЕХТ Текстовой режим Если функция заканчивает работу успешно, то возвращается нуль, в противном случае возвращаетси -1 и переменная еггпо принимает значение EINVAL (неверный аргумент). 10.2.13. Функция tell Интерфейс с функцией tell имеет следующий вид: long tell( int handle ); Функция возвращает номер текущей позиции файла (на нее установлен указатель файла).
340 Глава -10 10.2.14. Функция write Интерфейс с функцией write выглядит следующим образом: int write( int handle, void «buffer, int nbytes ); С помощью функции write делается попытка записать nbytes байтов из буфера в указанный файл. Для текстовых файлов символ конца строки заменяется на последовательность CR-LF («возврат кареткн»-«перевод строки»). Функция возвращает число фактически записанных байтов. Если это число меньше, чем значение nbytes, то выдается ошибка «переполнение диска». Если файл открыт с параметром 0_APPEND, то перед началом записи указатель файла позиционируется на конец файла. В случае ошибок функция write возвращает -1 и переменная еггпо принимает одно из следующих значений: EACCES Неразрешенный вид доступа EBADF Неверный номер файла 10.3. Пакет вводв/вывода нижнего уровня Дополнительно к большому числу функций ввода/вывода, встроенных в Турбо Си, программист может захотеть написать свои функции работы с файлами. В книге «Crafting Turbo С Software Components» (Wiener, 1988) представлено множество таких весьма полезных функций. С разрешения издателей в настоящем разделе мы приведем одну из полезных библиотек - fileio. Для детального ознакомления с реализацией библиотеки fileio обратитесь к первоисточнику. Программы, собранные в библиотеке fileio, обеспечивают очень быстрый ввод и вывод файлов. На листинге 10.2 приведен интерфейс с библиотекой ввода/вывода. Листинг 10.2. Интерфейс с библиотекой ввода/вывода для файлов /* Интерфейс с библиотекой ввода/вывода. Библиотеку использовать с большой моделью памяти. Файл fileio.h
Ввод и вывод в Турбо Си 341 */ int openfile( char* filename ); /* Возвращает описатель файла. По описателю можно понять, были ли ошибки. */ void delete_file( char» filename ); /* Удаляет файл с указанным именем. */ int lookupfile( char* filename, int *newfile ); /• Возвращает описатель файла. */ unsigned writefilet int filehandle, Unsigned bytestowrite, void *fromlocation ); /* Возвращает число записанных байтов. */ unsigned readfile( int filehandle, unsigned bytestoread, ' void *tolocation ); /• Возвращает число прочитанных байтов. */ void closefile( int filehandle ); /* Закрывает файл с заданным описателей. */ На листинге 10.3 приведена реализация библиотеки fileio. В каждой функции используются обращения к DOS. Поэтому такие функции немобильны и могут работать только в среде MS-DOS. Объединение REGS и структура SREGS определяются в файле dos.h. Для осуществлении обращений к DOS используются две немобильные функции intdosx и intdos. Эффективность выполнения функций библиотеки в основном обуславливается именно использованием указанных системоза- висимых функций нижнего уровня. Подробно о вызове DOS можно узнать из уже упоминавшейся книги «Crafting Turbo С Software Components and Utilities:». Листинг 10.3. Реализация функций ввода/вывода дли файлов /* Реализация библиотеки ввода/вывода. Библиотеку использовать с большой моделью памяти. Файл fileio.с */ ttinclude <dos.h>
342 Глава 10 union REGS inreg, outreg; Struct SREGS segreg; int openfile( char» filename ) < inreg.x.ax = 0x3D02; inreg.x.dx = FP_0FF( filename ); segreg.ds = FP_SEG( filename ); intdosx( &inreg, &outreg, &segreg ); return outreg.x.ax; ) void delete_file( char» filename ) < inreg.x.ax = 0x4100; inreg.x.dx = FP_0FF( filename ); segreg.ds = FP_SEG( filename ); intdosx( &inreg, &outreg, &segreg ); > int lookupfilef char» filename, int «newfile ) /• Возвращает описатель файла. */ < int result; result = openfilef filename ); if ( result == 2 ) < inreg. x. ax = ОхЗСОО; inreg.x.dx = FP_0FF( filename ); segreg.ds = FP_SEG( filename ); inreg.x.ex = 0; intdosx( &inreg, boutreg, bsegreg ); •newfile = 1; return outreg.x.ax; > else { •newfile = 0; return result; } > unsigned writefile( int filehandle, unsigned bytestowrite, void «fromlocation ); { inreg.x.ax = XiOOO;
Ввод и вывод в Турбо Си 343 inreg.x.bx = filehandle; inreg.x.cx = bytestowrite; inreg.x.dx = FP_0FF( fromlocation ); segreg.ds = FP_SEG( fromlocation ); intdosxf &inreg, &outreg, bsegreg ); return outreg.x.ax; } unsigned readfilef int filehandle, unsigned bytestoread, void «tolocation ); { inreg.x.ax = 0x3F00; inreg.x.bx = filehandle; inreg.x.cx = bytestoread; inreg.x.dx = FP_0FF( tolocation ); segreg.ds = FPJ5EG( tolocation ); intdosx( &inreg, &outreg, bsegreg ); return outreg.x.ax; } void closefilef int filehandle ); < inreg.x.ax = ОхЗЕОО; inreg.x.bx = filehandle; intdost &inreg, &outreg ); } На листинге 10.4 представлена тестовая программа, С помощью которой можно сравнить эффективности чтении двоичного файла с помощью стандартной функции fread и с помощью функции readfile из библиотеки fileio. Листинг 10.4. Тестовая программа сравнения эффективности функции fread из библиотеки stdio и функции readfile из библиотеки fileio /• Компилировать для модели памяти large */ tinclude <stdio.h> «include ."fileio.h" «include "util.h" «define buff_size 30000 FILE *fl; int f2; /• Описатель файла •/ int buffer! buff_size ];
344 Глава 10 main() { extern void createjile( void ); extern void access_filel( void ); extern void access_file2( void ); create_file(); access_filel(); access^fileZO; printf( "\n" ); } void create_file( void ) { int i, new; for ( i » 0; i < buff_size; i++ ) buffer! i 1 = i; f2 = lookupfilef "testfile.dta", &new ); writefilef f2, buff_size • sizeoff int /, buffer ); closefilef f2 ); ) void access_filel() < int i, num_iterns; fl = fopenf "testfile.dta", "rb" ); rpttimingf begin ); /* Из util.h */ num_iterns = freadf buffer, sizeoff int ), buff_size, fl ); rpttimingf end ); /* Из util.h */ printf ("\п\пПрочитано элементов - "/.d\n", num_i terns ); for ( i = buff_size - 1; i >= buff_size - 20; i~) printf( "\n%d", buffer! i ■] ); fcloset fl ); } void access_file2( void ) { int i, bytes_read; f2 = openfilef "testfile.dta" ); rpttiming( begin ); bytes_read = readfile( f2, buff_size * sizeof(int), buffer ); rpttiming( end ); printf( "\п\пПрочитано байтов = %u\n", bytes^ead );
Ввод и вывод в Турбо Си 345 for < i = buff_size - 1; i >= buff^ize - 20; i— ) printff "\nXd", buffer! i 1 ); closefilef f2 ); } Программа с листинга 10.4 выведет 0 часов, 0 минут, 2 секунд, 20 сотых Прочитано элементов = 30000 29999 29998 29997 29996 29995 29994 29993 29992 29991 29990 29989 29988 29987 29986 29985 29984 29983 29982 29981 - 29980 0 часов, 0 минут, 0 секунд, 22 сотых Прочитано байтов = 60000 29999 29998 29997 29996 29995 29994 29993
346 Глава 10 29992 29991 29990 29989 29988 29987 29986 29985 29984 29983 29982 29981 29980 Программа readfile выполняет операцию ввода в 10 раз быстрее, чем функция fread из библиотеки stdio.
Глава II Препроцессор Турбо Си Препроцессор (Турбо) Си обрабатывает исходный текст программы на (Турбо) Си, прежде чем последний поступит иа вход компилятора. Препроцессор расширяет все макровызовы и подставляет все внешние файлы. 11.1. Команды препроцессора Для обозначения команд препроцессора используется символ # Этот символ в (Турбо) Си нигде больше ие применяется. В табл. 11.1 перечислены все команды препроцессора и указано их назначение. Таблица 11.1. Список команд препроцессора Команда Назначение #define Определение макро ttundef Отмена определения макро ttinclude Подстановка текста из внешнего файла #if Условная подстановка фрагмента текста в зависимости от значения константного выражения #ifdef Условная подстановка фрагмента текста в зависимости от того, определено макро или иет ttifndef Условная подстановка фрагмента текста, выполняемая, если макро не определено ttelse Альтернатива для #ifdef и #ifndef ttendif Обозначение конца условно подставляемого фрагмента текста #line Содержит номер текущей строки и может использоваться при генерации сообщений периода компиляции #elif Альтернатива для #ifdef и #ifndef вместе с проверкой условия defined Может использоваться вместе с #if. Определяет, является ли имя именем макро
348 Глава 11 ttpragma Задает указания компилятору в мобильной форме. Компилятор Си, в котором данная команда не реализована, ее игнорирует #еггог" Формирует сообщение об ошибке периода компиляции В результате работы препроцессора формируется исходный текст на (Турбо) Си, который в дальнейшем обрабатывается компилятором. В иовейшем стандарте ANSI предложены пять новых предопределенных макро. Все оии реализованы в Турбо Сн. Имя каждого предопределенного макро начинается и заканчивается двумя символами «подчеркивание»: LINE Номер текущей строки в исходном файле FILE Имя текущего исходного файла DATE Дата начала обработки текущего файла препроцессором TIME Время начала обработки текущего файла препроцессором STDC Принимает значение 1, если при компиляции указан флаг проверки совместимости со стандартом ANSI (-A); в противном случае значение ие определено На листинге 11.1 даны примеры, иллюстрирующие использование большинства команд препроцессора, приведенных в табл. 11.1, и новых предопределенных макро. Программа выведет с меньше d. Константа SMALL определена Константа SMALL не определена Константа SMALL не определена Константа MORE определена Имя текущего файла test.с Дата Jun 16 1987 Номер текущей строки 51 Время 13:05:07
Препроцессор- Турбо Си 349 Если бы последние три строки файла ие были заключены в комментарий, то компилятор выдал бы следующее сообщение об ошибке: test.с Error test.с 50: Error directive:"Допущена серьезная ошибка в программе" in function main ••• 1 errors in Compile •*• Листинг 11.1. Пример использования команд препроцессора #include <stdio.h> #define greater_than( a, b ) ( ( a > b ) ? 1 : 0 ) «define SMALL main() { int с = 5, d = 7; if ( greater_than( c, d ) ) printff "\nc больше d." ); else printff "\nc меньше d." ); iundef greater_than #if defined( SMALL ) printff "ЧпКонстанта SMALL определена" ); ttelse printff "ЧпКонстанта SMALL не определена" ); «endif ttundef SMALL #ifdef SMALL printff "ЧпКонстанта SMALL определена" ); #else printff "ЧпКонстанта SMALL не определена" ); «endif «define MORE #ifndef SMALL printff "ЧпКонстанта SMALL не определена" ); ielse printff "ЧпКонстанта SMALL определена" ); #endif #if definedf SMALL ) printff "ЧпКонстанта SMALL не определена" ); #elif definedf MORE )
350 Глава 11 printf( "ХпКонстанта MORE определена" ); ttelse printff "ХпВетка else" ); ttendif printff "\п\пИмя текущего файла %з", FILE ); printff "\п\пДата %s", _DATE_ ); printff "\п\пНомер текущей строки Xd", LINE ); printff "\п\пВремя %s", _TIME_ ); /• #if defined( MORE ) terror "Допущена серьезная ошибка в программе" #endif •/ } В системе Турбо Сн предопределены некоторые дополнительные немобильные макро: TURB0C Содержит номер версии компилятора Турбо Си PASCAL Отражает состояние флага -р компилятора. Если флаг -р указан, то устанавливается значение 1; в противном случае значение не определено MSDOS Для всех компиляций константа 1 CDECL Сигнализирует, что флаг -р не указан XINY Используется крошечная модель памяти- SMALL Используется маленькая модель памяти __MEDIUM Используется средняя модель памяти COMPACT Используется компактная модель памяти LARGE Используется большая модель памяти HUGE Используется огромная модель памяти 11.2. Условная компиляция Команды условной компиляции, которые приведены в табл. 11.1, могут указывать компилитору определенные фрагменты текста программы, которые должны быть скомпилированы при выполнении различных условий. Такой процесс работы с текстом программы называется условной компиляцией.
Препроцессор Турбо Си 351 Условная компиляция может применяться для удаления из текста программы фрагментов, используемых для отладки. Соответствующий пример приведен иа листинге 11.2. Программа выведет Основной код Отладочная секция 1 Снова основной код Отладочная секция 1 Листинг 11.2. Условная компиляция, используемая при отладке «Include <stdio.h> «define DEBUG main() { printff "ЧпОсновной код" ); /• ... Код •/ ttifdef DEBUG printfl "ЧпОтладочная секция 1" ); /• ... Код •/ ttendif printff "ЧпСнова основной код" ); /• ... Код •/ #ifdef DEBUG prlntf( "ЧпОтладочная секция 2" ); ttelse printff "ЧпНе отладочная секция" ); «endtf printf( "Чп" ); ) Полезный пакет программ ввода/вывода fileio, описанный в разд. 10.3 и приведенный на листинге 10.3, может быть расширен так, чтобы ои становился применимым для любой модели памяти. Версия пакета, рассмотренная в гл. 10, может работать только с большой моделью памитн. Расширение пакета, позволяющее теперь использовать его для любой модели памяти, осуществляется средствами условной компиляции. Измененная версия пакета программ с листинга 10.3 приведена на листинге 11.3. В программе содержится типичное условное вы раже-
352 Глам 11 ние, использующее команды препроцессора #if, defined, ♦else и #endif: «if defined( _COMPACT_ ) 11 defined( _JLARGE_ )\ 11 def ined( _JUGE_ ) inreg.x.dx = FP_0FF ( filename ); Begreg.ds = FPJSEG ( filename ); intdosxt &inreg, boutreg, &segreg ); #else inreg.x.dx = filename; intdost &inreg, &outreg ); ftendif Компилируется либо один, либо другой набор строк программы в зависимости от того, какая из моделей памяти применяется при компиляции. Обращаем внимание читателя на то, что для переноса команды препроцессора на новую строку служит символ «обратная косая черта» \ . Листинг 11.3.' Расширенная версия пакета программ с листинга 10.3 с применением условной компиляции /* Реализация полезной библиотеки ввода/вывода файлов. Должна компилироваться в большой модели памяти. Файл fileio.c */ •include <dos.h> union REGS inreg, outreg; struct SREGS segreg; int openfilet char «filename ) < inreg.x.ax = Ox3D02; #if defined( _C0MPACT_ ) II defined( _JLARGE_ )\ 11 def ined( _flUGE_ ) inreg.x.dx = FP_0FF ( filename ); segreg.ds = FPJ5EG ( filename ); intdosxt &inreg, &outreg, &segreg ); ttelse inreg.x.dx = filename; intdost &inreg, &outreg ); ttendif
Препроцессор Турбо Си 353 return outreg.x.ax; > void delete_file( char «filename ) { inreg.x.ax = 0x4100; #if defined( _C0MPACT_ ) 11 defined( _JLARGE_ ) \ 11 def lned( _JUGE_ ) inreg.x.dx = FP_0FF ( filename ); segreg.ds = FPJ5EG ( filename ); inreg.x.cx = 0; intdosxt &inreg, boutreg, bsegreg ); ttelse inreg.x.dx = filename; intdost &inreg, boutreg ); ttendif > int lookupfile( char «filename, int «newfile ) /* Возвращает описатель файла */ { int result; result = openfilet filename ); if ( result == 2 ) < inreg.x.ax = ОхЗСОО; #lf defined( _C0MPACT_ ) 11 defined( _JLARGE_ >\ 11 def ined.( _JRJGE_ ) inreg.x.dx = FP_0FF ( filename ); segreg.ds = FP_SEG ( filename ); inreg.x.cx = 0; intdosxt &inreg, &outreg, &segreg ); ttelse inreg.x.dx = filename; intdost &inreg, &outreg ); ttendif «newfile = 1; return outreg.x.ax; > else i •newfile = 1; return result; >
354 Глам 11 Unsigned writefilef int filehandle, unsigned bytestowrite, void •fromlocation ) < inreg.x.ax = 0x4000; inreg, x.bx = filehandle; inreg.x. ex = bytestowrite; #if defined! _C0MPACT_ ) II defined( _JLARGE_ )\ 11 def ined( _JUGE_ ) inreg.x.dx = FP_0FF ( fromlocation ); segreg.ds = FPJ5EG ( froBlocation ); intdosxt &inreg, &outreg, &segreg ); #else inreg.x.dx = fromlocation; intdosf &inreg, &outreg ); «endif return outreg.x.ax; ) unsigned readfilet int filehandle, unsigned bytestoread, void *tolocation ) { Inreg.x.ax = 0x3F00; inreg.x.bx = filehandle; inreg.x.ex = bytestoread; #if defined( _C0HPACT_ ) 11 defined! _LARGE_ )\ 11 defined! _HUGE_ ) inreg.x.dx = FP_0FF ( tolocation ); segreg.ds = FPJ5EG ( tolocation ); intdosxt binreg, &outreg, &segreg ); ttelse inreg.x.dx = tolocation; intdosf &inreg, &outreg ); ttendif return outreg.x.ax; > void closefilel int filehandle ) i inreg.x.ax = ОхЗЕОО; inreg.x.bx = filehandle; intdost &inreg, goutreg ); >
Препроцессор Турбо Си 355 11.3. Команда #define Команда #define указывает препроцессору, что в теле программы все вхождении идентификатора, записанного за #define, должны быть заменены на блок текста. Поскольку препроцессор не отличает зарезервированных слов языка от других идентификаторов, следует избегать использования в командах #define имен, совпадающих с зарезервированными словами. В простейшем варианте команды #define вслед за идентификатором не ставится открывающая круглая скобка. Такая форма применяется для присваивания имен литеральным константам. Предостережение В конце строки команды *define не должна стоять точка с запятой. Вот пример типичной ошибки: ttdefine max_size 50; Команда #define может содержать параметры. Например, ttdefine sum( х, у ) ( х + у ) В общем случае команда #define имеет вид ttdefine идентификатор ( параметре параметр2, ..., параметрп) элементы Разрешено ставить пробелы между именем макро н левой скобкой, а также в списке аргументов. Можно определить макро без параметров, как это сделано в макро getchar(): ttdefine getchar() getc( stdin ) Макровызов в таком случае должен иметь вид getchar(). Можно определить макро fordo, облегчающее запись цикла for. ttdefine for_do( x, lower, upper ) \ 12*
356 Глам 11 for ( ( х ) = ( lower ); ( x ) <= {upper );\ ( x ) ++ ) Обращение к макро может быть таким: for_do( w, 3, 5 ) < оператор(ы) > 11.4. Побочные эффекты в макро Если определить макро недостаточно тщательно, то его использование может привести к неожиданным и непредсказуемым побочным эффектам. Различают побочные аффекты периода лексического анализа н периода счета. Рассмотрим пример типичного макро, вызывающего побочный эффект периода лексического анализа: •define sq( х ) х * х Пусть это макро, вычисляющее квадрат числа, будет вызвано следующим образом: int result = sq( j + 2 ); Предположим, что j равно 3. Ожидаемый результат - 25. Но иа самом деле при расширении макровызова будет построено выражение j + 2 » j + 2 дающее в результате 11. Существует два способа исправить положение. Первый - определить функцию square: int square! int x ) { return x * x; > Второй способ - более аккуратно определить макро sq, а именно:
Препроцессор Турбо Си 357 «define sq( х ) ( ( х ) » ( х ) ) Преимущество использования макро состоит в том, что вызов sq не требует дополнительных накладных расходов. С другой стороны, расходы на вызов функции square являются типичными для оформления вызова обычной функции. Побочные эффекты периода выполнения обусловливаются многократным вычислением одних и тех же аргументов макро. Например, следующее определение макро приведет к возникновению побочного эффекта периода выполнения. «define maxf x, y)((x)<(y)?(y):(x)) int i = 4, У; у = maxf ++i, 2 ); Значение переменной i будет увеличено на единицу дважды, и значения i и у станут равны шести. Если то же самое макроопределение max использовать как max( getchar(),'z'), to функция getchar будет вызвана дважды, если первый считанный с ее помощью символ окажется лексикографически меньшим, чем символ V. 11.5. Специальные средства препроцессора Турбо Сн Две возможности препроцессора Турбо Сн, описываемые ниже, являются новейшим расширением Си в соответствии со стандартом ANSI. Два элемента в макроопределении можно соединить вместе (склеить). Для этого между элементами следует поставить знак ## . Допускается ставить по одному необязательному пробелу слева н справа от знака. При раскрытии макроопределения препроцессор удалит пробелы и знак ## и объединит оба элемента в один. Такой прием может использоваться для формирования идентификаторов, например, «define identifier! a, b ) ( а ## b ) Обращение identifier(x,4) раскрывается в (х4). Вложенные макровызовы, расположенные в строке мак-
358 Глава 11 роопределения, раскрываются только в момент раскрытия самого макро, а не в момент определения макро. Для преобразования аргумента макро в строку перед ним нужно поставить символ #. В Турбо Сн игнорируются макровызовы, стоящие внутри строк н символьных констант.
Глава 12 Специальные средства Турбо Си В Турбо Сн реализован новейший стандарт ANSI для Си. Но, кроме того, в язык включены некоторые средства, отсутствующие в других, более ранних стандартах для компиляторов с Сн. Многие нз важных средств новейшего стандарта уже были представлены в предыдущих главах. В Турбо Сн имеется также сильная поддержка работы в среде MS-DOS для процессора 80x86, реализованная в виде расширений языка, которые обычно не представлены в других реализациях Сн. Использование указанных расширений делает немобильной часть системы или всю систему, реализованную на Турбо Сн. Однако предоставляемые при этом преимущества могут иметь для программиста решающее значение. Сочетание мощности языка (Турбо) Сн по доступу и выполнению манипуляций над «внутренностями» системы с расширениями языка дает в результате хорошее средство разработки программных систем, применимых во многих приложениях. 12.1. Программирование со смешанными моделями иамяти Напомним читателю, что о шести моделях памяти говорилось в разд. 6.1. В Турбо Си определены семь новых зарезервированных слов, отсутствовавших в новейшем стандарте ANSI для Си: near, far, huge, _cs, _ds, _es и _ss. Эти зарезервированные слова могут использоваться "в качестве модификаторов при указателях. Зарезервированные слова _cs, _ds, _es и _ss применяются как модификаторы 16-битовых указателей, соответствующих регистрам сегментов CS, DS, ES и SS. Заметим, что эти слова не являются именами самих регистров сегментов. Имена, эквивалентные зарезервированным словам, ио записанные прописными буквами (_CS, DS и т.д.), а также _АХ, _ВХ н т.д., обозначают псевдопеременные, непосредственно отображаемые на регистры процессора 80x86. Зарезервированные слова _cs, _ds, _es и _ss используются при описании близких указателей (16-бнтового смещения), задаваемых относительно начала соответствующего сегмента.
360 Глава 12 Например, описание int _jss *p; означает, что р содержит шестнадцатнбитовое смещение в сегменте стека. Функции и указатели могут модифицироваться с помощью зарезервированных слов near, far и huge. В табл. 12.1 показана связь между моделями памяти, указателями на функции и указателями на данные. Таблица 12.1. Связь между моделями памяти, указателями на функции и указателями на данные Модель памяти Крошечная Маленькая Средняя Компактная Большая Огромная Указатель near, _cs near, _cs far near, _cs far far на функцию Указатель иа данные near, _ds near, _ds near, _ds far far far Используемая в программе модель памяти и применяемые виды указателей на функции н данные задаются при вызове компилятора. Но иногда бывает удобно или необходимо изменить модель памяти для отдельных функций или данных. Это изменение можно произвести с помощью модификаторов near, far и huge, например, следующим образом: int far «data; Указатель «data теперь является дальним указателем независимо от того, какая модель памяти указана компилятору при обработке программы. В качестве примера использования модификатора при описании функции рассмотрим следующий оператор: float near example( int x, char *s ) { }
Специальные средства Турбо См 361 Если программа, содержащая функцию example, компилируется для большой модели памяти, то благодаря модификатору near все вызовы этой функции будут близкими я тем самым сэкономятся место в стеке н время счета. К функции example можно обращаться только из того фрагмента программы, в котором она компилировалась. Это ограничение может породить некоторые проблемы. 12.2. Глобальные переменные В Турбо Сн предопределены некоторые нестандартные глобальные переменные. Если такие переменные описать как extern, то они оказываются доступными в любом месте программной системы, создаваемой на Турбо Си. Глобальные переменные сведены в табл. 12.2. Таблица 12.2. Глобальные переменные Турбо Си н их назначение Глобальная переменная int daylight long timezone int errno int _doserrno char *sys_errlistlJ Int sys_nerr int jfmode unsigned _psp char *environIJ unsigned int . unsigned char unsigned char .version _osmajor _osminor Где описана time.h time.h errno.h fcntl.h stdlib.h dos.h dos.h dos.h dos.h Назначение 1 - время задается с учетом половины суток; 0 - стандартная форма Разница между местным н гринвичским временем в секундах Номер системной ошибки Код ошибки MS-DOS Массив строк диагностики Количество строк в sys_errlist[] Режим файла (текстовый или двоичный) Адрес сегмента префикса программы Строки описания среды DOS Версия MS_DOS Старший номер версии -Младший номер версии
362 Гмм 12 В табл. 12.3 приведены обозначения и описания значений, помещаемых в массив syserrliet. Таблица 12.3. Значения, помещаемые в syserrlist Обозначение Описания E2BIG Слишком длинный список аргументов EACCES Нарушены права доступа EBADF Неверный номер файла EC0NTR Разрушены блоки памяти ECURDIR Попытка удалить текущую директорию EDOM Выход за границу области EINVACC Неверный код доступа EINVAL Неверный аргумент EINVDATA Неверные данные EINVDRV Неверное указание устройства EINVENV Неверное задание среды EINVFMT Неверный формат EINVFNC Неверный номер функции EINVMEM Неверный адрес блока памяти EMFILE Открыто слишком много файлов ENMFILE Больше нет файлов EN00EV Такого устройства нет EN0ENT Нет такого файла или директории EN0EXEC Ошибка формата исполняемого файла EN0FILE Нет такого файла или директории EN0MEM Мало памяти ENOPATH Указанный путь не найден EN0TSAM Не то же самое устройство ERANGE Результат вне диапазона EX0EV Перекрестное указание устройства EZER0 Ошибка номер О В табл. 12.4 приведены обозначения кодов ошибок MS-DOS, которые могут быть присвоены глобальной переменной _doserrno. Таблица 12.4. Значения кодов ошибок MS-DOS Обозначение E2BIG EACCES EACCES Значение Неверно задана среда Нарушены права доступа Неверный доступ
Специальные средстм Турбо Си 363 EACCES Работа с текущей директорией EBAOF Неверный описатель файла EFAULT Зарезервировано EINVAL Неверная функция EINVAL Неверные данные EMFILE Открыто слишком много файлов EN0ENT Файл не найден EN0ENT Путь не найден EN0ENT Больше нет файлов ENOEXEC Ошибка формата исполняемого файла EN0MEM Разрушен блок управления памятью EN0MEM Исчерпана память EN0MEM Испорчен блок EX0EV Испорчен диск EXDEV Не то же самое устройство 12.3. Важные нестандартные библиотеки Кроме стандартных библиотек, разработанных в Беркли для UNIX V.3 и поддерживаемых Турбо Си, в систему включены также нестандартные библиотеки взаимодействия с MS-DOS, обеспечивающие доступ к среде DOS. Интерфейсы с тахими нестандартными библиотеками функций представлены в файлах dos.h, bios.h и dir.h. 12.3.1. Библиотека dir.h В табл. 12.5 приведены функции из библиотеки dir.h. Некоторые из этих функций совместимы с UNIX, но большинство из них являются нестандартными и специфичными для MS-DOS. Совместимость каждой из функций отражена в табл. 12.5. Детальное описание каждой из функций даио в документации фирмы Borland «Turbo С Reference Guide>. Таблица 12.5. Функции из библиотеки dir.h Функция Совместимость с UNIX Назначение chdir Да Смена директории findfirst Нет Поиск первого файла flndnext Нет Поиск следующего файла fnmerge Нет Смена имени файла fnsplit Нет Расщепление именн файла
364 Гтн 12 getcurdir getcwd getdisk mkdir ■ktemp mdir searchpath setdisk Нет Нет Нет Да Да Да Нет Нет Опрос текущей директории Опрос рабочей директории Опрос номера текущего диска Создание директории Создание уникального имени файла Удаление директории Поиск файла Задание текущего диска 12.3.2. Библиотека dos.li В табл. 12.6 приведены функции нз библиотеки dos.h. Все зти функции являются специфичными для MS-DOS. Детальное описание каждой из функций даио в документации фирмьг Borland «Turbo С Reference Guide». Таблица 12.6. Функции из библиотеки do3.h Функция absread abswrite bdos bdosptr country ctrlbrk disable dosexterr enable FP_OFF FPJ5EG freemen getinterrupt getcbrk getdfree getdta getfat Назначение Чтение указанного .сектора диска Запись указанного сектора диска Системный вызов MS-DOS Системный вызов MS-DOS Задание формата представления даты в соответствии с традициями страны Установха нового вектора реакции на иажатие клавиш Control+Break Запрещение прерываний Заполнение структур DOS при ошибке Разрешение прерываний Получение смещения в далеком указателе Получение сегмента в далеком указателе Освобождение занятого ранее блока Получение сведений о - прерывании Получение сведений о прерывании от иажатия клавиш Control+Break Получение сведений о свободном пространстве на диске Получение сведений о диске Получение таблицы расположения файлов
Специальные средства Турбо Си 365 getfatd getpsp getvect getverify harderr hardresume hardretn inport inportb int86 int86x intr keep MK_fP outport outportb parsfnB peek peekb poke pokeb randbrd randbwr segread setdta setvect setverify sleep unlink Получение таблицы расположения файлов Получение сегмента префикса программы Получение адреса вектора прерываний Опрос состояния флага verify Установка описателя ошибки аппаратуры Обработка прерывания по ошибкам аппаратуры Обработка прерывания по ошибкам аппаратуры Ввод из физического порта Ввод из физического порта Программное прерывание Программное прерывание Программное прерывание Выход из резидентной программы н сохранение ее в памяти Построение дальнего указателя из адреса сегмента и смещения Вывод в физический порт Вывод в физический порт Просмотр строки Чтение по указанному адресу памяти Чтение по указанному адресу памяти Запись по указанному адресу памяти Запись по указанному адресу памити Прямое чтение блока Прямая запись блока Считывание сегментных регистров Установка информации о диске Установка вектора прерывания Установка флага verify Прекращение работы иа заданный интервал времени Уничтожение файла 12.3.3. Библиотека bios, fa В табл. 12.7 приведены функции из библиотеки bios.h. Все эти функции являются специфичными для MS-DOS. Детальное описание каждой из функций дано в документации фирм» Borland «Turbo С Reference Guide».
366 Гми 12 Таблица 12.7, Функции из библиотеки bios.h Функция Назначение Moscom Работа с портом связи RS232 biosdisk Работа с диском непосредственно через BIOS biosequip Проверка оборудования bioskey Операции с клавиатурой biomemory Опрос размера памити biosprint Управление принтером Most i me Опрос времени суток
Приложение Перечень основных ошибок при программировании на Си Точка с запятой после директивы «include Обычная ошибка - добавление точки с залитой после директивы «include. В отличие от многих другцх операторов (Турбо) Си директива include ие должна оканчиваться точкой с запитой. Точка с занятой после значения макро Не оканчивайте значение макро точкой с запятой. Значение макро подставляется вместо имени макро полиостью. Если точка с запитой есть, то оиа будет подставлена вместе с литеральным выражением. Неверное задание параметров функции printf Частая ошибка - отсутствие соответствующей переменной для каждой спецификации в строке printf. Другая частая ошибка - несоответствие типа переменной спецификации. Обычно это происходит тогда, когда в строке много спецификаций и программист путает порядок требуемых переменных. Точка с запитой в конце описания прототипа функции Частая ошибка - постановка точки с запитой вслед за правой скобкой в описании функции. За правой скобкой в описании функции должна следовать открывающая фигурная скобка. Использование операции присваивании вместо операции сравнении и наоборот Обычной ошибкой для иовичхов, особенно дли тех, кто переходит с програимирования иа Паскале к (Турбо) Сн, ивлиетси использование вместо операции сравнения иа равенство == операции присваивания =. Традиционный компилятор с изыка Си ие оказывает программисту никакой помощи по отысканию такой логической ошибки. К счастью, компилятор Турбо Сн выдает предупреждающее сообщение, На которое программисту нужно отреагировать.
368 Приложение Присваивание отрицательных значений переменным целого беззнакового типа Не следует присваивать отрицательные значения целым беззнаковым переменным. Побитовые операции иад целыми знаковыми переменными Хотим предупредить читателя, что при использовании в операции сдвига вправо знахового целого операнда некоторые компиляторы с Си ие «вдвинут» нули слева, если самый левый бит операнда ие был равен нулю. Поэтому соглашении о мобильности предписывают преобразовывать операнд операции сдвига вправо в беззнаковый тип до выполнении самого сдвига. Подобная ситуация наблюдается и при использовании операции отрицании ~. Если операнд был со знаком, то результат выполнения операции будет машинозависимым. Прежде чем выполнить операцию отрицании, операнд должен быть преобразован в целый беззнаковый тип. Аналогичные проблемы совместимости возникают и при применении побитовых операций &, ~ и ! к знаковым целым значениям. Чтобы избежать этих проблем, следует перед выполнением побитовых операций перевести знаковые целые в беззнаковые. Обособленный оператор ELSE Синтаксис (Турбо) Си предписывает, что ELSE всегда относится к ближайшему оператору IF. Бесконечные циклы Потенциальной ошибкой при программировании цикла WHILE, как, впрочем, и цикла любого другого типа, ивлиется запись такого условного выражения, которое никогда не прекратит выполнение цикла. Такой цикл называетси бесконечным. Ошибки типа «лишний шаг» Человеку свойственно ошибаться. Циклы предоставляют нам обширные возможности проявить наши способности. Системы программирования на (Турбо) Си ие осуществляют проверки выхода за границу цикла во время выполнения программы. Вся ответственность здесь целиком лежит на программисте.
Перечень основных ошнбои при программировании на Си 369 Указатели Серьезная опасность возникает, если функция возвращает указатель, являющийся адресом автоматической (локальной) переменной. Автоматическая переменная описывается внутри функции. Память под иее отводится в момент активизации (вызова) функции. По завершении работы функции освобождается память для всех автоматических переменных. Поэтому возвращенный адрес может быть позже использован системой и информации, содержащаяся по этому адресу, может оказатьси замененной новой информацией. Выход из описанного положения - никогда не возвращать адреса автоматических переменных. Еще один источник ошибок - неосвобожденне памяти, запрошенной ранее с помощью функций alloc или malloc, когда указатель уже больше не нужен. Система неспособна автоматически освобождать память в куче. Возврат (освобождение) памяти в куче выполняет функция free. В качестве аргумента функции free задается указатель, ссылающийся на освобождаемую память. И еще одна ошибка - присваивание переменной-указателю адресного значения непосредственно. Это делает код программы немобильным. Выход за границу массива Наиболее серьезные ошибки, связанные с использованием массивов, - выход за границу массива. Ошибки возникают, когда в программе делается попытка записать или прочитать значение по адресу памяти, который не был указан в программе. Следствиями ошибок выхода за границу массива могут быть и неверные результаты, и общий сбой системы. Если вы забыли отвести память для массива, описанного с помощью указателя иа. какой-либо тип, то все попытки записать или прочитать данные из массива приведут к ошибке выхода за границу массива. Ошибки при использовании строк Очень важно, чтобы программист был уверен в том, что все строки оканчиваются символом '\0'. Если строки задаются в виде литералов, то компилятор добавляет нулевой символ в конец строки автоматически. Если же строка формируется символ за символом, ответственность за окончание строки лежит на самом программисте.
370 Приложение Описание многомерных массивов При описании многомерных массивов следует иметь в виду, что они требуют большого объема памити. Например, для размещения трехмерного массива, описанного как double data! 50 It 50 It 50 1; потребуется 125000*sizeof( double ) байт, или 1 000 000 байт.
Предметный указатель Абстрактные данные 100, 306 Автоматические переменные 255 Адрес памяти 142 Алгоритм - вычисления размера фиксированных выплат 131 - генерапни перестановок 263 - нумерования строк программы 62 - определение максимального и минимального значений 60 - определения фальшивой монеты 65 - сортировки 64 - суммирования подвекторов 126, 128 Аргументы см. Параметры Арифметические операции 48 над указателями 152 - - - типом float 90 типом integer 87 Бесконечный цикл 117 Библиотека - bios.h 365 - dir.h 363 - dos.h 364 Битовые поля 206 Блоки 108 - описания локальных переменных 255 Ввод и вывод нижнего уровни 311, 322 Ввод с клавиатуры 81 Вложенные - комментарии 75 - махро 356 - операторы 108 - операторы 108 - структуры 209 Вывод на терминал 35 Выражения - леводопустимые 86 - праводопустимые 86 Глобальные переменные 361 Директивы препроцессора - - define 34, 347, 355 - - else 347 - -endif 348 - - if 348 - - ifdef 348 - - ifndef 347 include 33, 347 - - line 348 - - undef 348 Двоичное дерево 277 - - поиска 268 Загрузчик 31 Зарезервированные слова 77, 359 Идентификаторы 76 Инициализации - массивов 168 - переменных 84 - структур 207 - цикла 57, 116 Инкапсуляция данных 307 Класс памяти 254 - auto 255 - extern 258 - register 256 - static 260
372 Предметный указатель Код - ассемблера 31 - исходный 32 - объектный 31 - программный 31 - ошибки MS-DOS 362 Комментарии 32, 74 - вложенные 33, 75 Компилятор 31 Компьютер DEC PDP-11 10 Константы 77 - битовые (двоичные) 206 - вещественные 79 - восьмеричные 78 - десятичные 78 - длинные целые 78 - перечислимые - символьные 79 - строковые 82 - целые 77 - шестнадцатеричиые 78 Конструкция выбора 109 Лексические структуры языка 74 Логические операции 51, 78 Макро 34 Макровызовы 374 Макроопределения 347 Массивы 58, 148 - выход за границу - многомерные 180 - описания 148 - параметров вызова 174 - свободные 190 - сортировка 60 ' - строк 169 - структур 209 - указателей на функции 242 Метка оператора 115 Микропроцессор 9 Мобильность - полей битов 206 - идентификаторов 76 - комментариев 74 - объединений 212 - текстовых потоков 313 Модели памяти 135, 350 Объединения 212 Обработка ошибок 153 Ограничители 75 Оператор 47 - ветвления SWITCH 56, ИЗ - вызова функции 47 - запятая 102 - перехода GOTO 115 - присваивания 52, 93 - пустой 108 - составной 108 - условный - - IF 56, 109 - - IF ELSE 56, 110 - - ? 11 - цикла 57, 116 - - DO WHILE 57, 121 - - FOR 57, 124 - WHILE 57,-117 BREAK 56 - CONTINUE 124 - RETURN. 187, 235 Операции 47, 75 - арифметические 87, 92 - логические 51, 87 - над массивами 148 - над объединениями 212 - над структурами 214 - обращения по адресу 139, 174, 239 - отношения 49, 94 - побитовые 87, 90, 97 - получения адреса 139
Предметный указатель 373 - постфиксные 52, 54 - префиксные 52, 54 - приоритет 90 - присваивании 52, 93 - прочие 54, 102 Операционная система - - MS-DOS И, 31, 310, 363 - OS/2 11 - UNIX 11, 311, 334, 363 Описание - внешних объектов 42 - массивов 148, 180 - области действия 235 - объединений 212 - параметров 239, 283 - перечислений 102 - полей битов 206 - структур 214 - указателей 135 Отладка 33 Очереди 219 Пакет программ 340, 352 Параметры - задаваемые в командной строке 17, 188 - макро 356 - передача 239 - - по адресу 239 - - по значению 239 - функции 239 Переменные - автоматические 255 - внешние 258 - глобальные 45, 316 - класса volatile 261 - локальные 45 - регистровые 256 - статические 260 - типа вещественного 89 - - перечислимого 102 - - символьного 88 - - целого 87 Перечисления 102 Побочные эффекты в макро 356 Подвекторы 126 - суммирование 128 Поддерево 268 - левое 268 - правое 268 Подпрограммы см. Функции Поли битов 206 Поток 311 - двоичный 313 - текстовый 313 Потоковый ввод вывод 311 Преобразование типов 104 - - при присваивании 105 - - для двухместных операций 106 - - для унарных операций 106 Препроцессор 31, 348 - директивы 348 - особенности 357 Прерывании 153 Приоритеты операций 90 Програм м и рован ие - объектно-ориентированное 307 - стиль 29, 39, 109 Прототипы функций 236 Процессор 80x86 12, 20, 313, 359 Размер данных 104 Рекурсия 261 Родовые структуры данных 286 список 286 дерево поиска 295
374 Предметный указатель Связанные структуры - - обобщенные 214 очередь 219 список 287 - - стек 215 Символ - подчеркивание 86 - точка с запятой 55, 75 - EOF 314 Символы - ограничители 75 - ASCII 313 - IBM 313 Синтаксис языка 74 Сообщение - об ошибке 237 - предупреждающее 47, 118 Спецификаторы 35, 85 Спецификации - преобразования 36, 322 - - ввода 330 - - вывода 36 / Список 223 - связанный 287 Сортировка массива методом - - - быстрой сортировки 253 - - - пузырька 64 - - - грубой силы 126 Стандарт Си - - ANSI 12, 33, 41, 77, 348 - - K&R 12, 41 Стек 215 Строки 82, 169 - конец 170 Структура программы 38, 44 Структуры - инициализация 207 - описания 312 - связанные 214 Тип данных 39, 84 - - абстрактный 306 - - вещественный- 89 - - массив 148 - - объединение 212 - - перечислимый 102 - - символьный 88 - - строковый 169 - - указатель 135 - - целый 87 Типы данных 84 - - скалярные 84 - - указатели 139 Турбо Си - система программирования 31 - особенности 12, 33, 41 43, 77, 348 - язык 12, 33, 43, 77 Узел - корневой 295 - левый потомок 295 - правый потомок 295 Указатели - модели памити 135 - на функции 242 - операции 152 - описание 135 Управляющие структуры 55, 108 Управляющие (ESC) последовательности 80 Усечение 94 Условная компиляция 350 Утилиты 61 Файл стандартный 311 Фирма - Borland 12 - Microsoft 11 Флаги 36, 322, 350
Предметный указатель 375 Функции 41 - библиотечные 310 - ввода данных 311, 332 - внешние 258 - вывода данных 311, 332 - классы памяти 254 - области видимости 235 - обращения к MS-DOS 341 - с переменным числом параметров 283 Функция - access 332 - clearerr 321 - close 333 - cprintf 321 - cscanf 329 - creat 333 - dup 334 - dup2 334 - eof 335 - fclose 315 - feof 320 - ferror 321 - fgetc 315 - fgeis 318 - filelength 335 - fflush 315 - fopen 314 - fprintf 321 - fputc 318 - fputs 319 - fread 320 - freopen 315 - fscanf 329 - fseek 317 - ftell 329 - f write 320 - getc 315 - getchar 81, 316 - getftime 335 - gets 318 - isatty 336 - Iseek 336 - main 38 - open 337 - printf 35, 321 - putc 318 - putchar 319 - puts 319 - read 338 - rename 321 - rewind 317 - scanf 38 - setbuf 331 - setftime 335 - setmode 339 - sizeof 54 - sprintf 321 - sscanf 329 - tell 339 - ungetc 316 - vfprintf 328 - vprintf 328 - write 340 Язык программирования - Ада 9 - Бейсик 9 - Модула 2 270 - Паскаль 9 - ПЛ/1 9 - Си 9 - Си++ 307 - Objective С 307
Оглавление Предисловие редактора перевода 5 Предисловие 7 От автора 8 Глава 1. Обзор и введение 9 1.1. Языки Си и Турбо Си 9 1.2. Немного об этой книге 15 1.3. Самая первая программа на Турбо Си, которая не печатает "Нривет, Мир" 16 1.4. Программа на Турбо Си для возбуждения аппетита 17 1.5. Стиль написания программы 29 Глава 2. Турбо Си: обзор возможностей 31 2.1. Препроцессор, компилятор и загрузчик 31 2.2. Комментарии 32 2.3. Директивы Include 33 2.4. Макро 34 2.5. Оператор Printf: вывод на терминал 35 2.6. Оператор Scanf: ввод с клавиатуры 38 2.7. Функция Main 38 2.8. Типы данных..-. 39 2.9. Функции 41 2.10. Логическая организация простой программы иа Турбо Си 44 2.11. Локальные и глобальные переменные 45 2.12. Операторы и операции 47 2.12.1. Арифметические операции 48 2.12.2. Операции отношения 49 2,. 12.3. Логические операции 51 2.12.4. Операции присваивания 52 2.12.5. Другие операции 54 2.13. Управляющие структуры 55 2.13.1. Операторы ветвления 56 2.13.2. Оператор цикла 57 2.14. массивы 57 2.15. Несколько прииеров программ на Турбо Си.... 59 2.15.1. Определение максимального и минимального значений 60 2.15.2. Утилита копирования файлов, использусщая переназначение 61
Оглавление 377 2.15.3. Нумерование строк программы на языке Си. 62 2.15.4. Сортировка методом пузырька 64 2.15.5. Определение фальшивой монеты 65 Глава 3. Лексические структуры языка 74 3.1. Элементы 74 3.2. Комментарии 74 3.3. Ограничители 75 3.4. Операции 75 3.5. Идентификаторы 76 3.6. Зарезервированные слова 77 3.7. Константы 77 3.7.1. Константы целого типа 77 3.7.2. Константы вещественного типа 79 3.7.3. Символьные константы 79 3.7.4. Строковые константы 82 Глава 4. Скалярные типы данных, операции, преобразования типов 84 4.1. Типы данных и элементы памяти 84 4.2. Использование объектов различных типов 86 4.3. Переменные целого типа 87 4.4. Символьные переменные 88 4.5. Переменные вещественного типа 89 4.6. Приоритет и порядок выполнения операций 90 4.7. Арифметические операции 92 4.8. Операции отношения 94 4.9. Побитовые операции 97 4.10. Прочие операции 102 4.11. Перечислимые типы 102 4.12. Приведение и преобразование типов 104 4.12.1. Преобразование при присваивании 105 4.12.2. Преобразования для унарных операций 106 4.12.3. Преобразования для двухместных операций. 106 Глава 5. Управляющие структуры 108 5.1. Блоки и составные операторы 108 5.2. Пустой оператор 108 5.3. Конструкция выбора 109 5.3.1. Оператор IF 109 5.3.2. Оператор IF ELSE 110 5.3.3. Условная операция ? 112 5.3.4. Оператор SWITCH 113
378 Оглавление 5.3.5. Оператор GOTO 115 5.4. Циклы 116 5.4.1. Цикл WHILE 117 5.4.2. Цикл DO WHILE 121 5. 4.3. Цикл FOR 124 5.5. Несколько примеров 126 5.5.1. Суммирование подвекторов: два подхода 126 5.5.2. Вычисление размера фиксированных выплат при простом процентном займе 131 Глава в. Указатели н массивы 135 6.1. Указатели и модели памяти 135 6.2. Проблемы, связанные с указателями, и их разрешение 144 6.3. Массивы 148 6.4. Арифметические действия над указателями 152 6.5. Инициализация массивов 168 6.6. Строки 169 6.7. Передача иассивов и указателей в качестве параметров функции 174 6.8. Многомерные массивы 180 6.9. Параметры, задаваемые в коиандной строке 188 6.10. Свободные массивы 190 Глава 7. Структуры, объединения и ссылочные типы данных 200 7.1. Структуры 200 7.1.1. Битовые поля 206 7.1.2. Инициализация структур 207 7.1.3. Массивы структур 209 7.2. Объединения 212 7.3. Связанные структуры 214 7.3.1. Стек : 215 7.3.2. Очереди 219 7.3.3. Связанные списки 223 Глава 8. Функции, области видимости и классы памяти. 235 8.1. Представление функций 235 8.2. Передача параметров 239 8.3. Указатели на функции 242 8.4. Классы памяти 254 8.4.1. Автоматические переменные 255
Оглавление 379 8.4.2. Регистровые переменные 256 8.4.3. Внешние переменные и функции 258 8.4.4. Статические переменные и функции 260 8.4.5. Переменные класса volatile 261 8.5- Рекурсия 261 8.6. Двоичное дерево поиска 266 8.7. Прилокения двоичных деревьев поиска 277 8.8. Функции с переменным чисяом параметров 283 Глава 9. Родовые структуры данных 286 9.1. Родовой список 286 9.2. Родовое дерево поиска 295 9.3. Абстракции данных, объектно-ориентированное программирование и разработка программного обеспечении 306 Глава 10. Ввод и вывод в Турбо Си 310 10.1. Потоковый ввод/вывод 311 10.1.1. Текстовые и двоичные потоки 313 10.1.2. Символ EOF. 314 10.1.3. Функция fopen 314 10.1.4. Функция fflush 315 10.1.5. Функция freopen 315 10.1.6. Функция fclose 315 10.1.7. Функции fgetc и getc 315 10.1.8. Функция getchar 316 10.1.9. Функция ungetc 316 10.1.10. Функция fseek 317 10.1.11. Функция rewind 317 10.1.12. Функция fgets 318 10.1.13. Функция gets 318 10.1.14. Функции fputc и putc 318 10.1.15. Функция putchar 319 10.1.16. Функция fputs 319 10.1.17. Функция puts 319 10.1.18. Функция fread 320 10.1.19. Функция fwrite 320 10.1.20. Функция feof 320 10.1.21. Функция ferror. 321 10.1.22. Функция сlearerr 321 10.1.23. Функция rename 321 10.1.24. Функции fprintf, printf, sprlntf и cprintf 321
380 Оглавление 10.1.25. Функции vfprintf или vprintf 328 10.1.26. Функция ftell 329 10.1.27. Функции scanf, fscanf, sscanf и cscanf. 329 10.1.28. Функция setbuf 331 10.2. Ввод и вывод нижнего уровня 332 10.2.1. Функция access. 332 10.2.2. Функция close... 333 10.2.3. Функция creat 333 10.2.4. Функции dup и dup2 334 10.2.5. Функция eof 335 10.2.6. Функция filelength 335 10.2.7. Функции getftlme ш setftime 335 10.2.8. Функция isattу 336 10.2.9. Функция lseek 336 10.2.10. Функция open 337 10.2.11. Функция read 338 10.2.12. Функция setmode 339 10.2.13. Функция tell 339 10.2.14.^Функция write 340 10.3. Пакет ввода/вывода нижнего уровня 340 Глава 11. Препроцессор Турбо Си 347 11.1. Команды препроцессора 347 11.2. Условная компиляция 350 11.3. Команда «define 355 11.4. Побочные эффекты в макро 356 11.5. Специальные средства препроцессора Турбо Си 357 Глава 12. Специальные средства Турбо Си 359 12.1. Программирование со смешанными моделями памяти 359 12.2. Глобальные переменные 361 12.3. Важные нестандартные библиотеки 363 12.3.1. Библиотека dir.h 363 12.3.2. Библиотека dos. h 364 12.3.3. Библиотека bios.h 365 Приложение. Перечень основных ошибок при программировании на Си 367 Предметный указатель 371
Уважаемый читатель! Ваши замечания о содержании книги, ее оформлении, качестве перевода и другие просим присылать по адресу: 129820, Москва, И-110, ГСП, 1-й Рижский пер., д. 2, изд-во «Мир».
Научное издание Ричард Уинер Язык Турбо Си Заведующий редакцией д-р техн. наук А.Л. Щерс Зам. заведующего редакцией Э.Н. Бадиков Старшин научный редактор Л.П. Якименко Художник В. И. Шаповалов Художественный редактор Н.М. Иванов Технический редактор З.И. Резник Корректор В. И. Киселева ИБ № 7369 Оригинал-макет подготовлен на персональном компьютере и отпечатан иа лазерном принтере в издательстве "Мир" Подписано к печати 7.12.90. Формат 84x108 /_„. Бумага тнп,№1. Печать высокая. Гарнитура литературная. Объем 6,0 бум.л. Усл.печ.л. 20,16. Усл. кр.-отт. 20,16. Уч.-иэд.л. 16,25. Изд. № 6/7084. Тираж 50 000 экз. Зак. № 795. Цена 4 р.20 к. Издательство "Мнр" В/О "Совэкспорткиига" Госкомпечати СССР 129820, ГСП, Москва, И-110, 1-й Рижский пер., 2. Отпечатаио в Ленинградской типографии № 2 головном предприятии ордена Хрудового Красного Знамени Ленинградского объединения "Техническая книга" нм. Евгении Соколовой Государственного комитета СССР по печати. 198052, г. Ленинград, Л-52, Измайловский проспект, 29.
Для пользователей персональных компьютеров Если Вы желаете быстро овладеть практическими знаниями о программных средствах, используемых на персональном компьютере, Вам поможет в этом серия микропамяток - малообъемных изданий карманного формата. Примерное представление о широте тематики серии Вам даст приводимый перечень выпущенных и планируемых к выпуску названий. Финогеиов К.Г. "Работаем с MS-DOS", 92 стр., 2 р. 30 к. Описаны основные правила работы на персональных компьютерах типа IBM PC в среде операционных систем MS-DOS и PC-DOS (версии 3.3). Приведены команды DOS с пояснениями и примерами использования. Рассмотрены' внутренние команды командных файлов, коды завершения команд DOS, команды строчного редактора EDLIN. Дан перечень директив файла CONFIG.SYS с краткими пояснениями. Головач В.И. "Работаем с PC Tools", 95 стр., 2 р. 20 к. Краткое руководство по некоторым утилитам известного пакета PC Tools (версия 5.1) - PC Shell, Compress, PC Backup, PC Secure и другим. Пакет содержит программные средства общего назначения, дополняющие и расширяющие функции ДОС. Потоцкий В.К- "Работаем с системой Clipper", 95 стр., 2 р. 20 к. Описаны средства компилирующей системы программирования Clipper. Обеспечивая высокое быстродействие программ и требуя существенно меньший объем оперативной памяти, чем dBASE HI Plus и dBASE IV, Clipper является мощным средством проектирования информационных систем разнообразного назначения на ПЭВМ типа IBM PC. Морозова Н.В., Куликова Е.А. "Работаем с ScanGal", 94 стр. Описывается пакет Scanning Gallery (ScanGal) и его более поздняя версия Scanning Gallery Plus (ScanGal Plus). Предназначен для ввода и обработки изображений сканером ScanJet или ScanJet Plus под управлением IBM XT/AT. Морозова Н.В., Куликова Е.А. "Работаем с Paintbrush, 63 стр., 1 р. 50 к. Описывается один из наиболее распространенных рисовальных графических пакетов Paintbrush для IBM PC или совместимых с ними. Пользуется популярностью среди художников и оформителей реклам. Белов В. А. "Работаем с dBASE III Plus", 78 сгр., 2 p. 30 к.
Памятка по изыку программирования системы управления базами данных dBASE I!! Plus - одной из наиболее популярных в мире СУБД для персональных компьютеров. Приведены полные перечни команд и функций dBASE III Plus, их синтаксис и краткое описание применения. Татарннова Л.В., Лазукова Т.Н., Онопко Д.Д. "Работаем с WORD 5.0": В 2-х выпусках, 187 стр., 4 р.(за комплект). Пакет предназначен для ввода, оформления и вывода разнообразных текстовых материалов. Даются основные сведения по работе с пакетом, описываются операции ввода, редактирования, оформления и вывода на печать документа. Рассматриваются средства оформления издания, повышения скорости работы, а также работа с иллюстрациями. Головач В.И. "Работаем с SuperCalc 5", 94 стр., 2 р. Справочный материал по использованию табличного процессора SuperCalc 5 - программы, позволяющей работать с данными (числовыми и символьными), организованными в форме таблиц (двумерных и трехмерных). SuperCalc 5 рассчитан иа широкую область применений - от научно- технических расчетов до экономических систем. Рыбаков В.Е., Азов СВ., "Работаем с Norton Commander (3.0) н Norton Integrator (4.5)", 95 стр., 2 p. Описаны основные правила работы с программами Norton Commander (3.0), значительно облегчающими работу с ПК, и программой Norton Integrator (4.5), являющейся наиболее распространенным средством диагностики компьютера, устранения выявленных дефектов. Приведены основные команды, имеется большое количество примеров, даны реальные экранные изображения. Выпуск осуществляют издательство "Мир", Всесоюзное объединение кооперативов "Воким", малое предприятие "Малин".