Диплом: Язык С - текст диплома. Скачать бесплатно.
Банк рефератов, курсовых и дипломных работ. Много и бесплатно. # | Правила оформления работ | Добавить в избранное
 
 
   
Меню Меню Меню Меню Меню
   
Napishem.com Napishem.com Napishem.com

Диплом

Язык С

Банк рефератов / Программирование

Рубрики  Рубрики реферат банка

закрыть
Категория: Дипломная работа
Язык диплома: Русский
Дата добавления:   
 
Скачать
Microsoft Word, 2363 kb, скачать бесплатно
Заказать
Узнать стоимость написания уникальной дипломной работы

Узнайте стоимость написания уникальной работы

ЯЗЫК С Аннотация. Язык “ C” (произносится “си” ) - это универсальный язык программирования , для которого характерны экономичность выражения , современный поток управления и структуры данных , богатый набор операторов . Язык “ C” не является ни языком “очень высокого уровня” , ни “большим” языком , и не предназначается для некото р ой специальной области применения . но отсутствие ограничений и общность языка делают его более удобным и эффективным для многих задач , чем языки , предположительно более мощные. Язык “ C” , первоначально предназначавшийся для написания операционной системы “ U NIX” на ЭВМ DEC PDP-11, был разработан и реализован на этой системе Деннисом Ричи . Операционная система , компилятор с языка “ C” и по существу все прикладные программы системы “ UNIX” (включая все программное обеспечение , использованное при подготовке этой к ниги ) написаны на “ C” . Коммерческие компиляторы с языка “ C” существуют также на некоторых других ЭВМ , включая IBM SYSTEM/370, HONEYWELL 6000, INTERDATA 8/32. Язык “ C” , однако , не связан с какими-либо определенными аппаратными средствами или системами , и н а нем легко писать программы , которые можно пропускать без изменений на любой ЭВМ , имеющей “ C”-компилятор. Эта книга предназначена для того , чтобы помочь читателю научиться программировать на языке “ C” . Она сдержит учебное введение , цель которого - позволит ь новым пользователям начать программировать как можно быстрее , отдельные главы по всем основным особенностям языка и справочное руководство . Обучение построено в основном на чтении , написании и разборе примеров , а не голой формулировке правил . Примеры , п р иводимые в книге , по большей части являются законченными реальными программами , а не отдельными фрагментами . Все примеры были проверены непосредственно с текста книги , где они напечатаны в виде , пригодном для ввода в машину . Кроме указаний о том , как сдел а ть использование языка более эффективным , мы также пытались , где это возможно , проиллюстрировать полезные алгоритмы и принципы хорошего стиля и разумной разработки. Настоящая книга не является вводным курсом в программирование ; она предполагает определенно е знакомство с основными понятиями программирования такими как переменные , операторы присваивания , циклы , функции . Тем не менее и новичок в программировании должен оказаться в состоянии читать подряд и освоиться с языком , хотя при этом была бы полезной по м ощь более опытного коллеги. По нашему опыту , “ C” показал себя приятным , выразительным и разносторонним языком на широком множестве разнообразных программ . Его легко выучить , и он не теряет своих качеств с ростом опыта программиста . Мы надеемся , что эта книга поможет вам хорошо его использовать. Вдумчивая критика и предложения многих наших друзей и коллег очень много добавили как для самой книги , так и для нашего удовольствия при ее написании . В частности , Майк Биапси , Джим Блю , Стью Фельдман , Доуг Мак-Ил рой , Билл Рум , Боб Розин и Ларри Рослер тщательно прочитали множество вариантов . Мы также обязаны Элю Ахо , Стиву Борну , Дэву Двораку , Чаку Хэлею , Дебби Хэлей , Мариону Харрису , Рику Холту , Стиву Джонсону , Джону Машею , Бобу Митцу , Ральфу Мьюа , Питеру Нельсо н у , Эллиоту Пинсону , Биллу Плагеру , Джерри Спиваку , Кену Томпсону и Питеру Вейнбергеру за полезные замечания на различных этапах и Майку Лоску и Джо Осанна за неоценимую помощь при печатании книги . Брайен В . Керниган Деннис М . Ричи 0.1. Введение. Язык “ C” является универсальным языком программирования . Он тесно связан с операционной системой “ UNIX” , так как был развит на этой системе и так как “ UNIX” и ее программное обеспечение написано на “ C” . Сам язык , однако , не связан с какой-либо одной операционной системой или машиной ; и хотя его называют языком системного программирования , так как он удобен для написания операционных систем , он с равным успехом использовался при написании больших вычислительных программ , программ для обработки текстов и баз данных. Язык “ C” - это язык относительно “низкого уровня” . В такой характеристике нет ничего оскорбительного ; это просто означает , что “ C” имеет дело с объектами того же вида , что и большинство ЭВМ , а именно , с символами , числами и адресами. Они могут объединятьс я и пересылаться посредством обычных арифметических и логических операций , осуществляемых реальными ЭВМ. В языке “ C” отсутствуют операции , имеющие дело непосредственно с составными объектами , такими как строки символов , множества , списки или с массивами , р ассматриваемыми как целое . Здесь , например , нет никакого аналога операциям PL/1, оперирующим с целыми массивами и строками . Язык не предоставляет никаких других возможностей распределения памяти , кроме статического определения и механизма стеков , обеспечи в аемого локальными переменных функций ; здесь нет ни “куч” (HEAP), ни “сборки мусора” , как это предусматривается в АЛГОЛЕ -68. Наконец , сам по себе “ C” не обеспечивает никаких возможностей ввода-вывода : здесь нет операторов READ или WRITE и никаких встроенных методов доступа к файлам . Все эти механизмы высокого уровня должны обеспечиваться явно вызываемыми функциями. Аналогично , язык “ C” предлагает только простые , последовательные конструкции потоков управления : проверки , циклы , группирование и подпрограммы , но не мультипрограммирование , параллельные операции , синхронизацию или сопрограммы. Хотя отсутствие некоторых из этих средств может выглядеть как удручающая неполноценность (“выходит , что я должен обращаться к функции , чтобы сравнить две строки символов ?!” ) , но удержание языка в скромных размерах дает реальные преимущества . Так как “ C” относительно мал , он не требует много места для своего описания и может быть быстро выучен. Компилятор с “ C” может быть простым и компактным . Кроме того , компиляторы легко пиш утся ; при использовании современной технологии можно ожидать написания компилятора для новой ЭВМ за пару месяцев и при этом окажется , что 80 процентов программы нового компилятора будет общей с программой для уже существующих компиляторов . Это обеспечивае т высокую степень мобильности языка . Поскольку типы данных и стуктуры управления , имеющиеся в “ C” , непосредственно поддерживаются большинством существующих ЭВМ , библиотека , необходимая во время прогона изолированных программ , оказывается очень маленькой. На PDP -11, например , она содержит только программы для 32-битового умножения и деления и для выполнения программ ввода и вывода последовательностей . Конечно , каждая реализация обеспечивает исчерпывающую , совместимую библиотеку функций для выполнения операц и й ввода-вывода , обработки строк и распределения памяти , но так как обращение к ним осуществляется только явно , можно , если необходимо , избежать их вызова ; эти функции могут быть компактно написаны на самом “ C”. Опять же из-за того , что язык “ C” отражает возможности современных компьютеров , программы на “ C” оказываются достаточно эффективными , так что не возникает побуждения писать вместо этого программы на языке ассемблера . Наиболее убедительным примером этого является сама операционная система “ UNIX” , к о торая почти полностью написана на “ C” . Из 13000 строк программы системы только около 800 строк самого низкого уровня написаны на ассемблере . Кроме того , по существу все прикладное программное обеспечение системы “ UNIX” написано на “ C” ; подавляющее большин с тво пользователей системы “ UNIX” (включая одного из авторов этой книги ) даже не знает языка ассемблера PDP-11. Хотя “ C” соответствует возможностям многих ЭВМ , он не зависит от какой-либо конкретной архитектуры машины и в силу этого без особых усилий позволя ет писать “переносимые” программы , т.е . программы , которые можно пропускать без изменений на различных аппаратных средствах . В наших кругах стал уже традицией перенос программного обеспечения , разработанного на системе “ UNIX” , на системы ЭВМ : HONEYWELL, I B M и INTERDATA. Фактически компиляторы с “ C” и программное обеспечение во время прогона программ на этих четырех системах , по-видимому , гораздо более совместимы , чем стандартные версии фортрана американского национального института стандартов (ANSI). Сама о перационная система “ UNIX” теперь работает как на PDP-11, так и на INTERDATA 8/32. За исключением программ , которые неизбежно оказываются в некоторой степени машинно-зависимыми , таких как компилятор , ассемблер и отладчик . Написанное на языке “ C” программн о е обеспечение идентично на обеих машинах . Внутри самой операционной системы 7000 строк программы , исключая математическое обеспечение языка ассемблера ЭВМ и управления операциями ввода-вывода , совпадают на 95 процентов. Программистам , знакомым с другими яз ыками , для сравнения и противопоставления может оказаться полезным упоминание нескольких исторических , технических и философских аспектов “ C”. Многие из наиболее важных идей “ C” происходят от гораздо более старого , но все еще вполне жизненного языка BCPL , разработанного Мартином Ричардсом . Косвенно язык BCPL оказал влияние на “ C” через язык “ B” , написанный Кеном Томпсоном в 1970 году для первой операционной системы “ UNIX” на ЭВМ PDP-7. Хотя язык “ C” имеет несколько общих с BCPL характерных особенностей , он никоим образом не является диалектом последнего . И BCPL и “ B” - “безтипные” языки ; единственным видом данных для них являются машинное слово , а доступ к другим объектам реализуется специальными операторами или обращением к функциям . В языке “ C” объектами основных типов данных являются символы , целые числа нескольких размеров и числа с плавающей точкой . Кроме того , имеется иерархия производных типов данных , создаваемых указателями , массивами , структурами , объединениями и функциями. Язык “ C” включает основны е конструкции потока управления , требуемые для хорошо структуированных программ : группирование операторов , принятие решений (IF), циклы с проверкой завершения в начале (WHILE, FOR) или в конце (DO) и выбор одного из множества возможных вариантов (SWITCH). (Все эти возможности обеспечивались и в BCPL, хотя и при несколько отличном синтаксисе ; этот язык предчувствовал наступившую через несколько лет моду на структурное программирование ). В языке “ C” имеются указатели и возможность адресной арифметики . Аргумен ты передаются функциям посредством копирования значения аргумента , и вызванная функция не может изменить фактический аргумент в вызывающей программе . Если желательно добиться “вызова по ссылке” , можно неявно передать указатель , и функция сможет изменить о бъект , на который этот указатель указывает . Имена массивов передаются указанием начала массивов , так что аргументы типа массивов эффективно вызываются по ссылке. К любой функции можно обращаться рекурсивно , и ее локальные переменные обычно “автоматические” , т.е . Создаются заново при каждом обращении . Описание одной функции не может содержаться внутри другой , но переменные могут описываться в соответствии с обычной блочной структурой . Функции в “ C” программе могут транслироваться отдельно . переменные по отн о шению к функции могут быть внутренними , внешними , но известными только в пределах одного исходного файла , или полностью глобальными . Внутренние переменные могут быть автоматическими или статическими . Автоматические переменные для большей эффективности мож н о помещать в регистры , но объявление регистра является только указанием для компилятора и никак не связано с конкретными машинными регистрами. Язык “ C” не является языком со строгими типами в смысле паскаля или алгола 68. Он сравнительно снисходителен к пр еобразованию данных , хотя и не будет автоматически преобразовывать типы данных с буйной непринужденностью языка PL/1. Существующие компиляторы не предусматривают никакой проверки во время выполнения программы индексов массивов , типов аргументов и т.д. В те х ситуациях , когда желательна строгая проверка типов , используется специальная версия компилятора . Эта программа называется LINT очевидно потому , она выбирает кусочки пуха из вашей программы . Программа LINT не генерирует машинного кода , а делает очень стр о гую проверку всех тех сторон программы , которые можно проконтролировать во время компиляции и загрузки . Она определяет несоответствие типов , несовместимость аргументов , неиспользованные или очевидным образом неинициализированные переменные , потенциальные т рудности переносимости и т.д . Для программ,которые благополучно проходят через LINT, гарантируется отсутствие ошибок типа примерно с той же полнотой , как и для программ , написанных , например , на АЛГОЛЕ -68. Другие возможности программы LINT будут отмечены, когда представится соответствующий случай. Наконец , язык “ C” , подобно любому другому языку , имеет свои недостатки . Некоторые операции имеют неудачное старшинство ; некоторые разделы синтаксиса могли бы быть лучше ; сушествует несколько версий языка , отличающ ихся небольшими деталями . Тем не менее язык “ C” зарекомендовал себя как исключительно эффективный и выразительный язык для широкого разнообразия применений программирования. Содержание книги организовано следующим образом . Глава 1 является учебным введение м в центральную часть языка “ C”. Цель - позволить читателю стартовать так быстро,как только возможно , так как мы твердо убеждены , что единственный способ изучить новый язык - писать на нем программы . При этом , однако , предполагается рабочее владение основ ными элементами программирования ; здесь не объясняется , что такое ЭВМ или компилятор , не поясняется смысл выражений типа N=N+1. Хотя мы и пытались , где это возможно , продемонстрировать полезную технику программирования . Эта книга не предназначается быть с п равочным руководством по структурам данных и алгоритмам ; там , где мы вынуждены были сделать выбор , мы концентрировались на языке. В главах со 2-й по 6-ю различные аспекты “ C” излагаются более детально и несколько более формально , чем в главе 1, хотя ударен ие по-прежнему делается на разборе примеров законченных , полезных программ , а не на отдельных фрагментах. В главе 2 обсуждаются основные типы данных , операторы и выражения . В главе 3 рассматриваются управляющие операторы : IF-ELSE ,WHILE ,FOR и т.д . Глава 4 охватывает функции и структуру программы - внешние переменные , правила определенных областей действия описания и т.д . В главе 5 обсуждаются указатели и адресная арифметика . Глава 6 содержит подробное описание структур и объединений. В главе 7 описываетс я стандартная библиотека ввода-вывода языка “ C” , которая обеспечивает стандартный интерфейс с операционной системой . Эта библиотека ввода-вывода поддерживается на всех машинах , на которых реализован “ C” , так что программы , использующие ее для ввода , вывод а и других системных функций , могут переноситься с одной системы на другую по существу без изменений. В главе 8 описывается интерфейс между “ C” - программами и операционной системой “ UNIX” . Упор делается на ввод-вывод , систему файлов и переносимость . Хотя н екоторые части этой главы специфичны для операционной системы “ UNIX” , программисты , не использующие “ UNIX” , все же должны найти здесь полезный материал , в том числе некоторое представление о том , как реализована одна версия стандартной библиотеки и предло ж ения для достижения переносимости программы. Приложение A содержит справочное руководство по языку “ C” . Оно является “официальным” изложением синтаксиса и семантики “ C” и (исключая чей-либо собственный компилятор ) окончательным арбитром для всех двусмыслен ностей и упущений в предыдущих главах. Так как “ C” является развивающимся языком , реализованным на множестве систем , часть материла настоящей книги может не соответствовать текущему состоянию разработки на какой-то конкретной системе . Мы старались избегать таких проблем и предостерегать о возможных трудностях . В сомнительных случаях , однако , мы обычно предпочитали описывать ситуацию для системы “ UNIX” PDP-11 , так как она является средой для большинства программирующих на языке “ C” . В приложении а также оп и саны расхождения в реализациях языка “ C” на основных системах. 1. Учебное введение. Давайте начнем с быстрого введения в язык “ C” . Наша цель - продемонстрировать существенные элементы языка на реальных программах , не увязая при этом в деталях , формальных п равилах и исключениях . В этой главе мы не пытаемся изложить язык полностью или хотя бы строго (разумеется , приводимые примеры будут корректными ). Мы хотим как можно скорее довести вас до такого уровня , на котором вы были бы в состоянии писать полезные про г раммы , и чтобы добиться этого , мы сосредотачиваемся на основном : переменных и константах , арифметике , операторах передачи управления , функциях и элементарных сведениях о вводе и выводе . Мы совершенно намеренно оставляем за пределами этой главы многие элем е нты языка “ C” , которые имеют первостепенное значение при написании больших программ , в том числе указатели , сртуктуры , большую часть из богатого набора операторов языка “ C” , несколько операторов передачи управления и несметное количество деталей. Такой под ход имеет , конечно , свои недостатки . Самым существенным является то , что полное описание любого конкретного элемента языка не излагается в одном месте , а пояснения , в силу краткости , могут привести к неправильному истолкованию . Кроме того , из-за невозможн о сти использовать всю мощь языка , примеры оказываются не столь краткими и элегантными , как они могли бы быть . И хотя мы старались свести эти недостатки к минимуму , все же имейте их ввиду. Другой недостаток состоит в том , что последующие главы будут неизбежн о повторять некоторые части этой главы . Мы надеемся , что такое повторение будет скорее помогать , чем раздражать. Во всяком случае , опытные программисты должны оказаться в состоянии проэкстраполировать материал данной главы на свои собственные программистск ие нужды . Начинающие же должны в дополнение писать аналогичные маленькие самостоятельные программы . И те , и другие могут использовать эту главу как каркас , на который будут навешиваться более подробные описания , начинающиеся с главы 2. 1.1. Hачинаем . Единс твенный способ освоить новый язык программирования - писать на нем программы . Первая программа , которая должна быть написана , - одна для всех языков : напечатать слова : HELLO, WORLD. Это - самый существенный барьер ; чтобы преодолеть его , вы должны суметь з авести где-то текст программы , успешно его скомпилировать , загрузить , прогнать и найти , где оказалась ваша выдача . Если вы научились справляться с этими техническими деталями , все остальное сравнительно просто. Программа печати “ HELLO, WORLD” на языке “ C” имеет вид : MAIN () PRINTF(“ HELLO, WORLD\ N” ); Как пропустить эту программу - зависит от используемой вами системы . В частности , на операционной системе “ UNIX” вы должны завести исходную программу в файле , имя которого оканчивается на “ .C” , например , H ELLO.C , и затем скомпилировать ее по команде CC HELLO.C Если вы не допустили какой-либо небрежности , такой как пропуск символа или неправильное написание , компиляция пройдет без сообщений и будет создан исполняемый файл с именем а .OUT . Прогон его по ко манде A.OUT приведет к выводу HELLO, WORLD На других системах эти правила будут иными ; проконсуль-тируйтесь с местным авторитетом. Упражнение 1-1. Пропустите эту программу на вашей системе . Попробуйте не включать различные части программы и посмотрите как ие сообщения об ошибках вы при этом получите. Теперь некоторые пояснения к самой программе . Любая “ C”-программа , каков бы ни был ее размер , состоит из одной или более “функций” , указывающих фактические операции компьютера , которые должны быть выполнены . Фу нкции в языке “ C” подобны функциям и подпрограммам фортрана и процедурам PL/1, паскаля и т.д . В нашем примере такой функцией является MAIN. Обычно вы можете давать функциям любые имена по вашему усмотрению , но MAIN - это особое имя ; выполнение вашей прогр а ммы начинается сначала с функции MAIN. Это означает , что каждая программа должна в каком-то месте содержать функцию с именем MAIN. Для выполнения определенных действий функция MAIN обычно обращается к другим функциям , часть из которых находится в той же с а мой программе , а часть - в библиотеках , содержащих ранее написанные функции. Одним способом обмена данными между функциями является передача посредством аргументов . Круглые скобки , следующие за именем функции , заключают в себе список аргументов ; здесь ма IN - функция без аргументов , что указывается как (). Операторы , составляющие функцию , заключаются в фигурные скобки и , которые аналогичны DO-END в PL/1 или BEGIN-END в алголе , паскале и т.д . Обращение к функции осуществляется указанием ее имени , за кото р ым следует заключенный в круглые скобки список аргументов . здесь нет никаких операторов CALL, как в фортране или PL/1. Круглые скобки должны присутствовать и в том случае , когда функция не имеет аргументов. Строка PRINTF(“ HELLO, WORLD\ N” ); является обраще нием к функции , которое вызывает функцию с именем PRINTF и аргуметом “ HELLO, WORLD\ N” . Функция PRINTF является библиотечной функцией , которая выдает выходные данные на терминал (если только не указано какое-то другое место назначения ). В данном случае печ а тается строка символов , являющаяся аргументом функции. Последовательность из любого количества символов , заключенных в удвоенные кавычки “...” , называется 'символьной строкой ' или 'строчной константой '. Пока мы будем использовать символьные строки только в качестве аргументов для PRINTF и других функций. Последовательность \ N в приведенной строке является обозначением на языке “ C” для 'символа новой строки ', который служит указанием для перехода на терминале к левому краю следующей строки . Если вы не включи те \ N (полезный эксперимент ), то обнаружите , что ваша выдача не закончится переходом терминала на новую строку . Использование последовательности \ N - единственный способ введения символа новой строки в аргумент функции PRINTF; если вы попробуете что-нибуд ь вроде PRINTF(“ HELLO, WORLD “ ); то “ C”-компилятор будет печатать злорадные диагностические сообщения о недостающих кавычках. Функция PRINTF не обеспечивает автоматического перехода на новую строку , так что многократное обращение к ней можно использовать для поэтапной сборки выходной строки . Наша первая программа , печатающая идентичную выдачу , с точно таким же успехом могла бы быть написана в виде MAIN() PRINTF(“ HELLO, “ ); PRINTF(“ WORLD” ); PRINTF(“\ N” ); Подчеркнем , что \ N представляет только один си мвол . Условные 'последовательности ', подобные \ N , дают общий и допускающий расширение механизм для представления трудных для печати или невидимых символов . Среди прочих символов в языке “ C” предусмотрены следующие : \ т - для табуляции , \ B - для возврата н а одну позицию , \ ” - для двойной кавычки и \\ для самой обратной косой черты. Упражнение 1-2. Проведите эксперименты для того , чтобы узнать что произойдет , если в строке , являющейся аргументом функции PRINTF будет содержаться \ X, где X - некоторый символ , н е входящий в вышеприведенный список. 1.2. Переменные и арифметика. Следующая программа печатает приведенную ниже таблицу температур по Фаренгейту и их эквивалентов по стоградусной шкале Цельсия , используя для перевода формулу C = (5/9)*(F-32). 0 -17.8 20 -6.7 40 4.4 60 15.6 ... ... 260 126.7 280 137.8 300 140.9 Теперь сама программа : /* PRINT FAHRENHEIT-CELSIUS TABLE FOR F = 0, 20, ..., 300 */ MAIN() INT LOWER, UPPER, STEP; FLOAT FAHR, CELSIUS; LOWER = 0; /* LOWER LIMIT OF TEMPERATURE TABLE */ UPPER =3 00; /* UPPER LIMIT */ STEP = 20; /* STEP SIZE */ FAHR = LOWER; WHILE (FAHR <= UPPER) CELSIUS = (5.0/9.0) * (FAHR -32.0); PRINTF(“ %4.0F %6.1F\ N” , FAHR, CELSIUS); FAHR = FAHR + STEP; Первые две строки /* PRINT FAHRENHEIT-CELSIUS TABLE FOR F = 0, 20, ..., 300 */ являются комментарием , который в данном случае кратко поясняет , что делает программа . Любые символы между /* и */ игнорируются компилятором ; можно свободно пользоваться комментариями для облегч е ния понимания программы . Комментарии могут появляться в любом месте , где возможен пробел или переход на новую строку. В языке “ C” все переменные должны быть описаны до их использования , обычно это делается в начале функции до первого выполняемого оператора . Если вы забудете вставить описание , то получите диагностическое сообщение от компилятора . Описание состоит из типа и списка переменных , имеющих этот тип , как в INT LOWER, UPPER, STEP; FLOAT FAHR, CELSIUS; Тип INT означает , что все переменные списка целы е ; тип FLOAT предназначен для чисел с плавающей точкой , т.е . для чисел , которые могут иметь дробную часть . Точность как INT , TAK и FLOAT зависит от конкретной машины , на которой вы работаете . На PDP-11, например , тип INT соответствует 16-битовому числу с о знаком , т.е . числу , лежащему между -32768 и +32767. Число типа FLOAT - это 32-битовое число , имеющее около семи значащих цифр и лежащее в диапазоне от 10е -38 до 10е +38. В главе 2 приводится список размеров для других машин. В языке “ C” предусмотрено неско лько других основных типов данных , кроме INT и FLOAT: CHAR символ - один байт SHORT короткое целое LONG длинное целое DOUBLE плавающее с двойной точностью Размеры этих объектов тоже машинно-независимы ; детали приведены в главе 2. Имеются также массивы , стр уктуры и объединения этих основных типов , указатели на них и функции,которые их возвращают ; со всеми ними мы встретимся в свое время. Фактически вычисления в программе перевода температур начинаются с операторов присваивания LOWER = 0; UPPER =300; STEP = 2 0; FAHR =LOWER; которые придают переменным их начальные значения . каждый отдельный оператор заканчивается точкой с запятой. Каждая строка таблицы вычисляется одинаковым образом , так что мы используем цикл , повторяющийся один раз на строку . В этом назначени е оператора WHILE: WHILE (FAHR <= UPPER) .... проверяется условие в круглых скобках . Если оно истинно (FAHR меньше или равно UPPER), то выполняется тело цикла (все операторы , заключенные в фигурные скобки и ). Затем вновь проверяется это условие и , если оно истинно , опять выполняется тело цикла . Если же условие не выполняется ( FAHR превосходит UPPER ), цикл заканчивается и происходит переход к выполнению оператора , следующего за оператором цикла . Так как в настоящей программе нет никаких последу ю щих операторов , то выполнение программы завершается. Тело оператора WHILE может состоять из одного или более операторов , заключенных в фигурные скобки , как в программе перевода температур , или из одного оператора без скобок , как , например , в WHILE (I < J) I = 2 * I; В обоих случаях операторы , управляемые оператором WHILE, сдвинуты на одну табуляцию , чтобы вы могли с первого взгляда видеть , какие операторы находятся внутри цикла . Такой сдвиг подчеркивает логическую структуру программы . Хотя в языке “ C” допу скается совершенно произвольное расположение операторов в строке , подходящий сдвиг и использование пробелов значительно облегчают чтение программ . Мы рекомендуем писать только один оператор на строке и (обычно ) оставлять пробелы вокруг операторов . Располо ж ение фигурных скобок менее существенно ; мы выбрали один из нескольких популярных стилей . Выберите подходящий для вас стиль и затем используйте его последовательно. Основная часть работы выполняется в теле цикла . Температура по Цельсию вычисляется и присваи вается переменной CELAIUS оператором CELSIUS = (5.0/9.0) * (FAHR-32.0); причина использования выражения 5.0/9.0 вместо выглядящего проще 5/9 заключается в том , что в языке “ C” , как и во многих других языках , при делении целых происходит усечение , состояще е в отбрасывании дробной части результата . Таким образом , результат операции 5/9 равен нулю , и , конечно , в этом случае все температуры оказались бы равными нулю . Десятичная точка в константе указывает , что она имеет тип с плавающей точкой , так что , как мы и хотели , 5.0/9.0 равно 0.5555... . Мы также писали 32.0 вместо 32 , несмотря на то , что так как переменная FAHR имеет тип FLOAT , целое 32 автоматически бы преобразовалось к типу FLOAT ( в 32.0) перед вычитанием. С точки зрения стиля разумно писать плаваю щие константы с явной десятичной точкой даже тогда , когда они имеют целые значения ; это подчеркивает их плавающую природу для просматривающего программу и обеспечивает то , что компилятор будет смотреть на вещи так же , как и Вы. Подробные правила о том , в к аком случае целые преобразуются к типу с плаваюшей точкой , приведены в главе 2. Сейчас же отметим , что присваивание FAHR = LOWER; проверка WHILE (FAHR <= UPPER) работают , как ожидается , - перед выполнением операций целые преобразуются в плавающую форму. Э тот же пример сообщает чуть больше о том , как работает PRINTF. Функция PRINTF фактически является универсальной функцией форматных преобразований , которая будет полностью описана в главе 7. Ее первым аргументом является строка символов , которая должна быт ь напечатана , причем каждый знак % указывает , куда должен подставляться каждый из остальных аргументов /второй , третий , .../ и в какой форме он должен печататься . Например , в операторе PRINTF(“ %4.0F %6.1F\ N” , FAHR, CELSIUS); спецификация преобразования %4. 0F говорит , что число с плавающей точкой должно быть напечатано в поле шириной по крайней мере в четыре символа без цифр после десятичной точки. спецификация %6.1F описывает другое число , которое должно занимать по крайней мере шесть позиций с одной цифрой после десятичной точки , аналогично спецификациям F6.1 в фортране или F(6,1) в PL/1. Различные части спецификации могут быть опущены : спецификация %6F говорит , что число будет шириной по крайней мере в шесть символов ; спецификация %2 требует двух позиций п осле десятичной точки , но ширина при этом не ограничивается ; спецификация %F говорит только о том , что нужно напечатать число с плавающей точкой . Функция PRINTF также распознает следующие спецификации : %D - для десятичного целого , %о - для восьмеричного ч и сла , %х - для шестнадцатиричного , %с - для символа , %S - для символьной строки и %% для самого символа %. Каждая конструкция с символом % в первом аргументе функции PRINTF сочетается с соответствующим вторым , третьим , и т.д . Аргументами ; они должны согласо вываться по числу и типу ; в противном случае вы получите бессмысленные результаты. Между прочим , функция PRINTF не является частью языка “ C” ; в самом языке “ C” не определены операции ввода-вывода. Нет ничего таинственного и в функции PRINTF ; это - просто полезная функция , являющаяся частью стандартной библиотеки подпрограмм , которая обычно доступна “ C”-программам . Чтобы сосредоточиться на самом языке , мы не будем подробно останавливаться на операциях ввода-вывода до главы 7. В частности , мы до тех пор отл о жим форматный ввод . Если вам надо ввести числа - прочитайте описание функции SCANF в главе 7, раздел 7.4. Функция SCANF во многом сходна с PRINTF , но она считывает входные данные , а не печатает выходные. Упражнение 1-3. Преобразуйте программу перевода тем ператур таким образом , чтобы она печатала заголовок к таблице. Упражнение 1-4. Напишите программы печати соответствующей таблицы перехода от градусов цельсия к градусам фаренгейта. 1.3. Оператор FOR. Как и можно было ожидать , имеется множество различных сп особов написания каждой программы . Давайте рассмотрим такой вариант программы перевода температур : MAIN() /* FAHRENHEIT-CELSIUS TABLE */ INT FAHR; FOR (FAHR = 0; FAHR <= 300; FAHR = FAHR + 20) PRINTF(“ %4D %6.1F\ N” , FAHR, (5.0/9.0)*(FAHR-32.0)); Эта п рограмма выдает те же самые результаты , но выглядит безусловно по-другому . Главное изменение - исключение большинства переменных ; осталась только переменная FAHR , причем типа INT (это сделано для того , чтобы продемонстрировать преобразование %D в функции PRINTF). Нижняя и верхняя границы и размер щага появляются только как константы в операторе FOR , который сам является новой конструкцией , а выражение , вычисляющее температуру по цельсию , входит теперь в виде третьего аргумента функции PRINTF , а не в вид е отдельного оператора присваивания. Последнее изменение является примером вполне общего правила языка “ C” - в любом контексте , в котором допускается использование значения переменной некоторого типа , вы можете использовать выражение этого типа . Так как тре тий аргумент функции PRINTF должен иметь значение с плавающей точкой , чтобы соответствовать спецификации %6.1F, то в этом месте может встретиться любое выражение плавающего типа. Сам оператор FOR - это оператор цикла , обобщающий оператор WHILE. Его функцио нирование должно стать ясным , если вы сравните его с ранее описанным оператором WHILE . Оператор FOR содержит три части , разделяемые точкой с запятой . Первая часть FAHR = 0 выполняется один раз перед входом в сам цикл . Вторая часть проверка , или условие , которое управляет циклом : FAHR <= 300 это условие проверяется и , если оно истинно , то выполняется тело цикла (в данном случае только функция PRINTF ). Затем выполняется шаг реинициализации FAHR =FAHR + 20 и условие проверяется снова . цикл завершается , ко гда это условие становится ложным . Так же , как и в случае оператора WHILE , тело цикла может состоять из одного оператора или из группы операторов , заключенных в фигурные скобки . Инициализирующая и реинициализирующая части могут быть любыми отдельными выр а жениями. Выбор между операторами WHILE и FOR произволен и основывается на том , что выглядит яснее . Оператор FOR обычно удобен для циклов , в которых инициализация и реинициализация логически связаны и каждая задается одним оператором , так как в этом случае запись более компактна , чем при использовании оператора WHILE , а операторы управления циклом сосредотачиваются вместе в одном месте. Упражнение 1-5. Модифицируйте программу перевода температур таким образом , чтобы она печатала таблицу в обратном порядке, т.е . От 300 градусов до 0. 1.4. Символические константы. Последнее замечание , прежде чем мы навсегда оставим программу перевода температур . Прятать “магические числа” , такие как 300 и 20, внутрь программы - это неудачная практика ; они дают мало информации тем , кто , возможно , должен будет разбираться в этой программе позднее , и их трудно изменять систематическим образом . К счастью в языке “ C” предусмотрен способ , позволяющий избежать таких “магических чисел” . Используя конструкцию #DEFINE , вы можете в нач а ле программы определить символическое имя или символическую константу , которая будет конкретной строкой символов . Впоследствии компилятор заменит все не заключенные в кавычки появления этого имени на соответствующую строку . Фактически это имя может быть з а менено абсолютно произвольным текстом , не обязательно цифрами #DEFINE LOWER 0/* LOWER LIMIT OF TABLE */ #DEFINE UPPER 300 /* UPPER LIMIT */ #DEFINE STEP 20 /* STEP SIZE */ MAIN () /* FAHRENHEIT-CELSIUS TABLE */ INT FAHR; FOR (FAHR =LOWER; FAHR <= UPPER; FAHR =FAHR + STEP) PRINTF(“ %4D %6.1F\ N” , FAHR, (5.0/9.0)*(FAHR-32)); величины LOWER, UPPER и STEP являются константами и поэтому они не указываются в описаниях . Символические имена обычно пишут прописными буквами , чтобы их было легко отличить от напи санных строчными буквами имен переменных . отметим , что в конце определения не ставится точка с запятой. Так как подставляется вся строка , следующая за определенным именем , то это привело бы к слишком большому числу точек с запятой в операторе FOR . 1.5. На бор полезных программ. Теперь мы собираемся рассмотреть семейство родственных программ , предназначенных для выполнения простых операций над символьными данными . В дальнейшем вы обнаружите , что многие программы являются просто расширенными версиями тех про тотипов , которые мы здесь обсуждаем. 1.5.1. Ввод и вывод символов. Стандартная библиотека включает функции для чтения и записи по одному символу за один раз . функция GETCHAR() извлекает следующий вводимый символ каждый раз , как к ней обращаются , и возвра щает этот символ в качестве своего значения. Это значит , что после C = GETCHAR() переменная 'C' содержит следующий символ из входных данных. Символы обычно поступают с терминала , но это не должно нас касаться до главы 7. Функция PUTCHAR© является дополнен ием к GETCHAR : в результате обращения PUTCHAR © содержимое переменной 'C' выдается на некоторый выходной носитель , обычно опять на терминал . Обращение к функциям PUTCHAR и PRINTF могут перемежаться ; выдача будет появляться в том порядке , в котором происх о дят обращения. Как и функция PRINTF , функции GETCHAR и PUTCHAR не содержат ничего экстраординарного . Они не входят в состав языка “ C” , но к ним всегда можно обратиться. 1.5.2. Копирование файла. Имея в своем распоряжении только функции GETCHAR и PUTCHAR вы можете , не зная ничего более об операциях ввода-вывода , написать удивительное количество полезных программ . Простейшим примером может служить программа посимвольного копирования вводного файла в выводной . Общая схема имеет вид : ввести символ WHILE (сим в ол не является признаком конца файла ) вывести только что прочитанный символ ввести новый символ программа , написанная на языке “ C” , выглядит следующим образом : MAIN() /* COPY INPUT TO OUTPUT; 1 ST VERSION */ INT C; C = GETCHAR(); WHILE (C != EOF) PUTCHAR © ; C = GETCHAR(); оператор отношения != означает “не равно”. Основная проблема заключается в том , чтобы зафиксировать конец файла ввода . Обычно , когда функция GETCHAR наталкивается на конец файла ввода , она возвращает значение , не являющееся действительным символом ; таким образом , программа может установить , что файл ввода исчерпан . Единственное осложнение , являющееся значительным неудобством , заключается в существовании двух общеупотребительных соглашений о том , какое значение фактически яв л яется признаком конца файла . Мы отсрочим решение этого вопроса , использовав символическое имя EOF для этого значения , каким бы оно ни было . На практике EOF будет либо -1, либо 0, так что для правильной работы перед программой должно стоять собственно либо #DEFINE EOF -1 либо #DEFINE EOF 0 Использовав символическую константу EOF для представления значения , возвращаемого функцией GETCHAR при выходе на конец файла , мы обеспечили , что только одна величина в программе зависит от конкретного численного значения. Мы также описали переменную 'C' как INT , а не CHAR , с тем чтобы она могла хранить значение , возвращаемое GETCHAR . как мы увидим в главе 2, эта величина действительно INT, так как она должна быть в состоянии в дополнение ко всем возможным символам предс тавлять и EOF. Программистом , имеющим опыт работы на “ C” , программа копирования была бы написана более сжато . В языке “ C” любое присваивание , такое как C = GETCHAR() может быть использовано в выражении ; его значение - просто значение , присваиваемое левой части . Если присваивание символа переменной 'C' поместить внутрь проверочной части оператора WHILE , то программа копирования файла запишется в виде : MAIN() /* COPY INPUT TO OUTPUT; 2 ND VERSION */ INT C; WHILE ((C = GETCHAR()) != EOF) PUTCHAR© ; Прог рамма извлекает символ , присваивает его переменной 'C' и затем проверяет , не является ли этот символ признаком конца файла . Если нет - выполняется тело оператора WHILE, выводящее этот символ . Затем цикл WHILE повторяется . когда , наконец , будет достигнут к онец файла ввода , оператор WHILE завершается , а вместе с ним заканчивается выполнение и функции MAIN . В этой версии централизуется ввод - в программе только одно обращение к функции GETCHAR - и ужимается программа. Вложение присваивания в проверяемое усло вие - это одно из тех мест языка “ C” , которое приводит к значительному сокращению программ . Однако , на этом пути можно увлечься и начать писать недоступные для понимания программы . Эту тенденцию мы будем пытаться сдерживать. Важно понять , что круглые скоб ки вокруг присваивания в условном выражении действительно необходимы . Старшинство операции != выше , чем операции присваивания =, а это означает , что в отсутствие круглых скобок проверка условия != будет выполнена до присваивания =. Таким образом , оператор C = GETCHAR() != EOF эквивалентен оператору C = (GETCHAR() != EOF) Это , вопреки нашему желанию , приведет к тому , что 'C' будет принимать значение 0 или 1 в зависимости от того , натолкнется или нет GETCHAR на признак конца файла . Подробнее об этом будет с к азано в главе 2/. 1.5.3. Подсчет символов. Следующая программа подсчитывает число символов ; она представляет собой небольшое развитие программы копирования. MAIN() /* COUNT CHARACTERS IN INPUT */ LONG NC; NC = 0; WHILE (GETCHAR() != EOF ) ++NC; PRINTF( “ %1D\ N” , NC); Оператор ++NC; демонстрирует новую операцию , ++, которая означает увеличение на единицу . Вы могли бы написать NC = NC + 1 , но ++NC более кратко и зачастую более эффективно . Имеется соответствующая операция— уменьшение на единицу . Операции ++ и— могут быть либо префиксными (++NC), либо постфиксными (NC++); эти две формы , как будет показано в главе 2, имеют в выражениях различные значения , но как ++NC, так и NC++ увеличивают NC. Пока мы будем придерживаться префиксных операций. Программа под счета символов накапливает их количество в переменной типа LONG, а не INT . На PDP-11 максимальное значение равно 32767, и если описать счетчик как INT , то он будет переполняться даже при сравнительно малом файле ввода ; на языке “ C” для HONEYWELL и IBM ти пы LONG и INT являются синонимами и имеют значительно больший размер . Спецификация преобразования %1D указывает PRINTF , что соответствующий аргумент является целым типа LONG . Чтобы справиться с еще большими числами , вы можете использовать тип DOUBLE / FL OAT двойной длины /. мы также используем оператор FOR вместо WHILE с тем , чтобы проиллюстрировать другой способ записи цикла. MAIN() /* COUNT CHARACTERS IN INPUT */ DOUBLE NC; FOR (NC = 0; GETCHAR() != EOF; ++NC) ; PRINTF(“ %.0F\ N” , NC); Функция PRINT F использует спецификацию %F как для FLOAT , так и для DOUBLE ; спецификация %.0F подавляет печать несуществующей дробной части. Тело оператора цикла FOR здесь пусто , так как вся работа выполняется в проверочной и реинициализационной частях. Но грамматичес кие правила языка “ C” требуют , чтобы оператор FOR имел тело . Изолированная точка с запятой , соответствуюшая пустому оператору , появляется здесь , чтобы удовлетворить этому требованию . Мы выделили ее на отдельную строку , чтобы сделать ее более заметной. Преж де чем мы распростимся с программой подсчета символов , отметим , что если файл ввода не содержит никаких символов , то условие в WHILE или FOR не выполнится при самом первом обращении к GETCHAR , и , следовательно , программа выдаст нуль , т.е . Правильный отв е т . это важное замечание . одним из приятных свойств операторов WHILE и FOR является то , что они проверяют условие в начале цикла , т.е . До выполнения тела . Если делать ничего не надо , то ничего не будет сделано , даже если это означает , что тело цикла никогд а не будет выполняться . программы должны действовать разумно , когда они обращаются с файлами типа “никаких символов” . Операторы WHILE и FOR помогают обеспечить правильное поведение программ при граничных значениях проверяемых условий. 1.5.4. Подсчет строк. Следующая программа подсчитывает количество строк в файле ввода . Предполагается , что строки ввода заканчиваются символом новой строки \ N, скрупулезно добавленным к каждой выписанной строке. MAIN() /* COUNT LINES IN INPUT */ INT C,NL; NL = 0; WHILE ((C = GETCHAR()) != EOF) IF (C =='\N') ++NL; PRINTF(“ %D\ N” , NL); Тело WHILE теперь содержит оператор IF , который в свою очередь управляет оператором увеличения ++NL. Оператор IF проверяет заключенное в круглые скобки условие и , если оно истинно, выполняет следующий за ним оператор /или группу операторов , заключенных в фигурные скобки /. Мы опять использовали сдвиг вправо , чтобы показать , что чем управляет. Удвоенный знак равенства == является обозначением в языке “ C” для “равно” /аналогично .EQ. В фортране /. Этот символ введен для того , чтобы отличать проверку на равенство от одиночного =, используемого при присваивании . Поскольку в типичных “ C” - программах знак присваивания встречается примерно в два раза чаще , чем проверка на равенство , то есте с твенно , чтобы знак оператора был вполовину короче. Любой отдельный символ может быть записан внутри одиночных кавычек , и при этом ему соответствует значение , равное численному значению этого символа в машинном наборе символов ; это называется символьной кон стантой . Так , например , 'A' - символьная константа ; ее значение в наборе символов ASCII /американский стандартный код для обмена информацией / равно 65, внутреннему представлению символа а . Конечно , 'A' предпочтительнее , чем 65: его смысл очевиден и он не з ависит от конкретного машинного набора символов. Условные последовательности , используемые в символьных строках , также занимают законное место среди символьных констант . Так в проверках и арифметических выражениях '\ N' представляет значение символа новой с троки . Вы должны твердо уяснить , что '\ N' - отдельный символ , который в выражениях эквивалентен одиночному целому ; с другой стороны “\ N” - это символьная строка , которая содержит только один символ . Вопрос о сопоставлении строк и символов обсуждается в гл а ве 2. Упражнение 1-6. Напишите программу для подсчета пробелов , табуляций и новых строк. Упражнение 1-7. Напишите программу , которая копирует ввод на вывод , заменяя при этом каждую последовательность из одного или более пробелов на один пробел. 1.5.5. Подс чет слов. Четвертая программа из нашей серии полезных программ подсчитывает количество строк , слов и символов , используя при этом весьма широкое определение , что словом является любая последовательность символов , не содержащая пробелов , табуляций или новы х строк . /Это - упрощенная версия утилиты 'WC' системы 'UNIX'/ #DEFINE YES 1 #DEFINE NO 0 MAIN() /* COUNT LINES, WORDS, CHARS IN INPUT */ INT C, NL, NW, INWORD; INWORD = NO; NL = NW = NC = 0; WHILE((C = GETCHAR()) != EOF) ++NC; IF (C == '\N') ++NL; IF (C==' ' \!\! C=='\N' \!\! C=='\T') INWORD = NO; ELSE IF (INWORD == NO) INWORD = YES; ++NW; PRINTF(“ %D %D %D\ N” , NL, NW, NC); Каждый раз , когда программа встречает первый символ слова , она увеличивает счетчик числа слов на единицу . Переменная INWORD следит за тем , находится ли программа в настоящий момент внутри слова или нет ; сначала этой переменной присваивается “ не в слове” , чему соответствует значение NO. Мы предпочитаем символические константы YES и NO литерным значениям 1 и 0, потому чт о они делают программу более удобной для чтения . Конечно , в такой крошечной программе , как эта , это не приводит к заметной разнице , но в больших программах увеличение ясности вполне стоит тех скромных дополнительных усилий , которых требует следование этом у принципу с самого начала . Вы также обнаружите , что существенные изменения гораздо легче вносить в те программы , где числа фигурируют только в качестве символьных констант. Строка NL = NW = NC = 0; полагает все три переменные равными нулю . Это не особый сл учай , а следствие того обстоятельства , что оператору присваивания соответствует некоторое значение и присваивания проводятся последовательно справа налево . Таким образом , дело обстоит так , как если бы мы написали NC = (NL = (NW = 0)); операция \!\ ! Означа ет OR , так что строка IF( C==' ' \!\! C=='\N' \!\! C=='\ T') говорит “если с - пробел , или с - символ новой строки , или с табуляция ...” ./условная последовательность \ T является изображением символа табуляции /. Имеется соответствующая операция && для AND. Выражения , связанные операциями && или \!\ ! , Рассматриваются слева на право , и при этом гарантируется , что оценивание выражений будет прекращено , как только станет ясно , является ли все выражение истинным или ложным . Так , если 'C' оказывается пробелом , т о нет никакой необходимости проверять , является ли 'C' символом новой строки или табуляции , и такие проверки действительно не делаются . В данном случае это не имеет принципиального значения , но , как мы скоро увидим , в более сложных ситуациях эта особенност ь языка весьма существенна. Этот пример также демонстрирует оператор ELSE языка “ C” , который указывает то действие , которое должно выполняться , если условие , содержащееся в операторе IF, окажется ложным. Общая форма такова : IF (выражение ) оператор -1 ELSE оп ератор -2 Выполняется один и только один из двух операторов , связанных с конструкцией IF-ELSE. Если выражение истинно , выполняется оператор -1; если нет - выполняется оператор -2. Фактически каждый оператор может быть довольно сложным . В программе подсчета с лов оператор , следующий за ELSE , является опертором IF , который управляет двумя операторами в фигурных скобках. Упражнение 1-9. Как бы вы стали проверять программу подсчета слов ? Kакие имеются ограничения ? Упражнение 1-10. Напишите программу , которая б удет печатать слова из файла ввода , причем по одному на строку. Упражнение 1-11. Переделайте программу подсчета слов , используя лучшее пределение “слова” ; считайте , например словом последовательность букв , цифр и апострофов , рачинающуюся с буквы. 1.6. Масс ивы. Давайте напишем программу подсчета числа появлений каждой цифры , символов пустых промежутков /пробел , табуляции , новая строка / и всех остальных символов . Конечно , такая задача несколько искусственна , но она позволит нам проиллюстрировать в одной прогр амме сразу несколько аспектов языка “ C”. Мы разбили вводимые символы на двенадцать категорий , и нам удобнее использовать массив для хранения числа появлений каждой цифры , а не десять отдельных переменных . Вот один из вариантов программы : MAIN() /* COUNT D IGITS, WHITE SPACE, OTHERS */ INT C, I, NWHITE, NOTHER; INT NDIGIT[10]; NWHITE = NOTHER = 0; FOR (I = 0; I < 10; ++I) NDIGIT[I] = 0; WHILE ((C = GETCHAR()) != EOF) IF (C >= '0' && C <= '9') ++NDIGIT[C-'0']; ELSE IF(C== ' ' \!\! C== '\N' \!\! C== '\ T') ++NWHITE; ELSE ++NOTHER; PRINTF(“ DIGITS =” ); FOR (I = 0; I < 10; ++I) PRINTF(“ %D” , NDIGIT[I]); PRINTF(“\ NWHITE SPACE = %D, OTHER = %D\ N” , NWHITE, NOTHER); Описание INT NDIGIT[10]; объявляет , что NDIGIT является массивом из десяти целых . в языке “ C ” индексы массива всегда начинаются с нуля /а не с 1, как в фортране или PL/1/, так что элементами массива являются NDIGIT[0], NDIGIT[1],..., NDIGIT[9]. эта особенность отражена в циклах FOR , которые инициализируют и печатают массив. Индекс может быть люб ым целым выражением , которое , конечно , может включать целые переменные , такие как I , и целые константы. Эта конкретная программа сильно опирается на свойства символьного представления цифр . Так , например , в программе проверка IF( C >= '0' && C <= '9')... определяет , является ли символ в 'C' цифрой , и если это так , то численное значение этой цифры определяется по формуле / C '0'/. Такой способ работает только в том случае , если значения символьных констант '0', '1' и т.д . Положительны , расположены в порядк е возрастания и нет ничего , кроме цифр , между константами '0' и '9'. К счастью , это верно для всех общепринятых наборов символов. По определению перед проведением арифметических операций , вовлекающих переменные типа CHAR и INT, все они преобразуются к типу INT, TAK что в арифметических выражениях переменные типа CHAR по существу идентичны переменным типа INT. Это вполне естественно и удобно ; например , C -'0'- это целое выражение со значением между 0 и 9 в соответствии с тем , какой символ от '0' до '9' хран и тся в 'C', и , следовательно , оно является подходящим индексом для массива NDIGIT. Выяснение вопроса , является ли данный символ цифрой , символом пустого промежутка или чем-либо еще , осуществляется последовательностью операторов IF (C >= '0' && C <= '9') ++ NDIGIT[C-'0']; ELSE IF(C == ' ' \!\! C == '\N' \!\! C == '\T') ++NWHITE; ELSE ++NOTHER; Конструкция IF (условие ) оператор ELSE IF (условие ) оператор ELSE оператор часто встречаются в программах как средство выражения ситуаций , в которых осуществляется выбо р одного из нескольких возможных решений. Программа просто движется сверху вниз до тех пор , пока не удовлетворится какое-нибудь условие ; тогда выполняется соответствующий 'оператор ', и вся конструкция завершается. /Конечно , 'оператор ' может состоять из нес кольких операторов , заключенных в фигурные скобки /. Если ни одно из условий не удовлетворяется , то выполняется 'оператор ', стоящий после заключительного ELSE, если оно присутствует . Если последне E ELSE и соответствующий 'оператор ' опущены (как в программе подсчета слов ), то никаких действий не производится . Между начальным IF и конечным ELSE может помещаться произвольное количество групп ELSE IF (условие ) оператор С точки зрения стиля целесообразно записывать эту конст-рукцию так , как мы показали , с тем чт обы длинные выражения не залезали за правый край страницы. Оператор SWITCH (переключатель ), который рассматривается в главе 3, представляет другую возможность для записи разветвления на несколько вариантов . этот оператор особенно удобен , когда проверяемое выражение является либо просто некоторым целым , либо символьным выражением , совпадающим с одной из некоторого набора констант . Версия этой программы , использующая оператор SWITCH, будет для сравнения приведена в главе 3. Упражнение 1-12. Напишите программ у , печатающую гистограмму длин слов из файла ввода . Самое легкое - начертить гистограмму горизонтально ; вертикальная ориентация требует больших усилий. 1.7. Функции. В языке “ C” функции эквивалентны подпрограммам или функциям в фортране или процедурам в P L/1, паскале и т.д . Функции дают удобный способ заключения некоторой части вычислений в черный ящик , который в дальнейшем можно использовать , не интересуясь его внутренним содержанием . Использование функций является фактически единственным способом справи т ься с потенциальной сложностью больших программ . Если функции организованы должным образом , то можно игнорировать то , как делается работа ; достаточно знание того , что делается . Язык “ C” разработан таким образом , чтобы сделать использование функций легким, удобным и эффективным . Вам будут часто встречаться функции длиной всего в несколько строчек , вызываемые только один раз , и они используются только потому , что это проясняет некоторую часть программы. До сих пор мы использовали только предоставленные нам фу нкции типа PRINTF, GETCHAR и PUTCHAR; теперь пора написать несколько наших собственных . так как в “ C” нет операции возведения в степень , подобной операции ** в фортране или PL/1, давайте проиллюстрируем механику определения функции на примере функции POWE R (M,N), возводящей целое м в целую положительную степень N. Так значение POWER(2,5) равно 32. Конечно , эта функция не выполняет всей работы операции **, поскольку она действует только с положительными степенями небольших чисел , но лучше не создавать дополн и тельных затруднений , смешивая несколько различных вопросов. Ниже приводится функция POWER и использующая ее основная программа , так что вы можете видеть целиком всю структуру. MAIN() /* TEST POWER FUNCTION */ INT I; FOR(I = 0; I < 10; ++I) PRINTF(“ %D %D %D\ N” ,I,POWER(2,I),POWER(-3,I)); POWER(X,N) /* RAISE X N-TH POWER; N > 0 */ INT X,N; INT I, P; P = 1; FOR (I =1; I <= N; ++I) P = P * X; RETURN (P); Все функции имеют одинаковый вид : имя (список аргументов , если они имеются ) описание аргументов , если они имеются описания операторы Эти функции могут быть записаны в любом порядке и находиться в одном или двух исходных файлах . Конечно , если исходная программа размещается в двух файлах , вам придется дать больше указаний при компиляции и загрузке , чем если бы она находилась в одном , но это дело операционной системы , а не атрибут языка . В данный момент , для того чтобы все полученные сведения о прогоне “ C” - программ , не изменились в дальнейшем , мы будем предполагать , что обе функции находятся в одн о м и том же файле. Функция POWER вызывается дважды в строке PRINTF(“ %D %D %D\ N” ,I,POWER(2,I),POWER(-3,I)); при каждом обращении функция POWER, получив два аргумента , вазвращает целое значение , которое печатается в заданном формате . В выражениях POWER(2,I) я вляется точно таким же целым , как 2 и I. /Не все функции выдают целое значение ; мы займемся этим вопросом в главе 4/. Аргументы функции POWER должны быть описаны соответствующим образом , так как их типы известны . Это сделано в строке INT X,N; которая следу ет за именем функции. Описания аргументов помещаются между списком аргументов и открывающейся левой фигурной скобкой ; каждое описание заканчивается точкой с запятой . Имена , использованные для аргументов функции POWER, являются чисто локальными и недоступны никаким другим функциям : другие процедуры могут использовать те же самые имена без возникновения конфликта. Это верно и для переменных I и P; I в функции POWER никак не связано с I в функции MAIN. Значение , вычисленное функцией POWER, передаются в MAIN с помощью оператора RETURN, точно такого же , как в PL/1. внутри круглых скобок можно написать любое выражение . Функция не обязана возвращать какое-либо значение ; оператор RETURN, не содержащий никакого выражения , приводит к такой же передаче управления , как “сваливание на конец” функции при достижении конечной правой фигурной скобки , но при этом в вызывающую функцию не возвращается никакого полезного значения. Упражнение 1-13. Апишите программу преобразования прописных букв из айла ввода в строчные , используя при этом функцию OWER© , которая возвращает значение 'C', если C'- не буква , и значение соответствующей строчной уквы , если 'C'-буква. 1.8. Аргументы - вызов по значению. Один аспект в “ C” может оказаться непривычным для программистов , которые использовал и другие языки , в частности , фортран и PL/1. в языке “ C” все аргументы функций передаются “по значению” . это означает , что вызванная функция получает значения своих аргументов с помощью временных переменных /фактически через стек /, а не их адреса . Это при в одит к некоторым особенностям , отличным от тех , с которыми мы сталкивались в языках типа фортрана и PL/1, использующих “вызов по ссылке “ , где вызванная процедура работает с адресом аргумента , а не с его значением. Главное отличие состоит в том , что в “ C” вызванная функция не может изменить переменную из вызывающей функции ; она может менять только свою собственную временную копию. Вызов по значению , однако , не помеха , а весьма ценное качество . Оно обычно приводит к более компактным программам , содержащим ме ньше не относящихся к делу переменных , потому что с аргументами можно обращаться как с удобно инициализированными локальными перемнными вызванной процедуры . Вот , например , вариант функции POWER использующей это обстоятельство POWER(X,N) /* RAISE X N-TH PO WER; N > 0; VERSION 2 */ INT X,N; INT P; FOR (P = 1; N > 0; --N) P = P * X; RETURN (P); Аргумент N используется как временная переменная ; из него вычитается единица до тех пор , пока он не станет нулем. Переменная I здесь больше не нужна . чтобы ни п роисходило с N внутри POWER это никак не влияет на аргумент , с которым первоначально обратились к функции POWER. При необходимости все же можно добиться , чтобы функция изменила переменную из вызывающей программы . Эта программа должна обеспечить установлени е адреса переменной /технически , через указатель на переменную /, а в вызываемой функции надо описать соответствующий аргумент как указатель и ссылаться к фактической переменной косвенно через него . Мы рассмотрим это подробно в главе 5. Когда в качестве арг умента выступает имя массива , то фактическим значением , передаваемым функции , является адрес начала массива . /Здесь нет никакого копирования элементов массива /. С помощью индексации и адреса начала функция может найти и изменить любой элемент массива . Это - тема следующего раздела. 1.9. Массивы символов. По-видимому самым общим типом массива в “ C” является массив символов . Чтобы проиллюстрировать использование массивов символов и обрабатывающих их функций , давайте напишем программу , которая читает набор ст рок и печатает самую длинную из них . Основная схема программы достаточно проста : WHILE (имеется еще строка ) IF (эта строка длиннее самой длинной из предыдущих ) запомнить эту строку и ее длину напечатать самую длинную строку По этой схеме ясно , что програм ма естественным образом распадается на несколько частей . Одна часть читает новую строку , другая проверяет ее , третья запоминает , а остальные части программы управляют этим процессом. Поскольку все так прекрасно делится , было бы хорошо и написать программу соответсвующим образом . Давайте сначала напишем отдельную функцию GETLINE, которая будет извлекать следующую строку из файла ввода ; это - обобщение функции GETCHAR. мы попытаемся сделать эту функцию по возможности более гибкой , чтобы она была полезной и в других ситуациях. Как минимум GETLINE должна передавать сигнал о возможном появлении конца файла ; более общий полезный вариант мог бы передавать длину строки или нуль , если встретится конец файла. нуль не может быть длиной строки , так как каждая строка сод ержит по крайней мере один символ ; даже строка , содержащая только символ новой строки , имеет длину 1. Когда мы находим строку , которая длиннее самой длинной из предыдущих , то ее надо где-то запомнить . Это наводит на мысль о другой функции , COPY , которая б удет копировать новую строку в место хранения. Наконец , нам нужна основная программа для управления функциями GETLINE и COPY . Вот результат : #DEFINE MAXLINE 1000 /* MAXIMUM INPUT LINE SIZE */ MAIN() /* FIND LONGEST LINE */ INT LEN; /* CURRENT LINE LENG TH */ INT MAX; /* MAXIMUM LENGTH SEEN SO FAR */ CHAR LINE[MAXLINE]; /* CURRENT INPUT LINE */ CHAR SAVE[MAXLINE]; /* LONGEST LINE, SAVED */ MAX = 0; WHILE ((LEN = GETLINE(LINE, MAXLINE)) > 0) IF (LEN > MAX) MAX = LEN; COPY(LINE, SAVE); IF (MAX > 0) /* THERE WAS A LINE */ PRINTF(“ %S” , SAVE); GETLINE(S,LIM) /* GET LINE INTO S,RETURN LENGTH */ CHAR S[]; INT LIM; INT C, I; FOR(I=0;I 0 ) IF ( LEN > MAX ) MAX = LEN; COPY(); IF ( MAX > 0 ) /* THERE WAS A LINE */ PRINTF( “ %S” , SAVE ); GETLINE() /* SPECIALIZED VERSION */ INT C, I; EXTERN CHAR LINE[]; FOR (I = 0; I < MAXLINE-1 && (C=GETCHAR()) !=EOF && C!='\N'; ++I) LINE[I] = C; ++I; LINE[I] = '\0' RETURN(I) COPY() /* SPECIALIZED VERSION */ INT I; EXTERN CHAR LINE[], SAVE[]; I = 0; WHILE ((SAVE[I] = LINE[I]) !='\0') ++I; Внешние переменные для функций MAIN, GETLINE и COPY определены в первых строчках приведенного выше примера , которыми указывается их тип и вызывается отведение для них памяти . синтаксически внешние описания точно такие же , как описания , которые мы использо в али ранее , но так как они расположены вне функций , соответствующие переменные являются внешними . Чтобы функция могла использовать внешнюю переменую , ей надо сообщить ее имя . Один способ сделать это включить в функцию описание EXTERN; это описание отличает с я от предыдущих только добавлением ключевого слова EXTERN. В определенных ситуациях описание EXTERN может быть опущено : если внешнее определение переменной находится в том же исходном файле , раньше ее использования в некоторой конкретной функции , то не обя зательно включать описание EXTERN для этой переменной в саму функцию . Описания EXTERN в функциях MAIN, GETLINE и COPY являются , таким образом , излишними. Фактически , обычная практика заключается в помещении определений всех внешних переменных в начале исхо дного файла и последующем опускании всех описаний EXTERN. Если программа находится в нескольких исходных файлах , и некоторая переменная определена , скажем в файле 1, а используется в файле 2, то чтобы связать эти два вхождения переменной , необходимо в файл е 2 использовать описание EXTERN. Этот вопрос подробно обсуждается в главе 4. Вы должно быть заметили , что мы в этом разделе при ссылке на внешние переменные очень аккуратно используем слова описание и определение . “Определение” относится к тому месту , где переменная фактически заводится и ей выделяется память ; “описание” относится к тем местам , где указывается природа переменной , но никакой памяти не отводится. Между прочим , существует тенденция объявлять все , что ни попадется , внешними переменными , поскол ьку кажется , что это упрощает связи , - списки аргументов становятся короче и переменные всегда присутствуют , когда бы вам они ни понадобились . Но внешние переменные присутствуют и тогда , когда вы в них не нуждаетесь . Такой стиль программирования чреват оп а сностью , так как он приводит к программам , связи данных внутри которых не вполне очевидны . Переменные при этом могут изменяться неожиданным и даже неумышленным образом , а программы становится трудно модифицировать , когда возникает такая необходимость . Вто р ая версия программы поиска самой длинной строки уступает первой отчасти по этим причинам , а отчасти потому , что она лишила универсальности две весьма полезные функции , введя в них имена переменных , с которыми они будут манипулировать. Упражнение 1-18. Пров ерка в операторе FOR функции GETLINE довольно неуклюжа . Перепишите программу таким образом , чтобы сделать эту проверку более ясной , но сохраните при этом то же самое поведение в конце файла и при переполнении буфера . Является ли это поведение самым разумн ы м ? 1.11. Резюме На данном этапе мы обсудили то , что можно бы назвать традиционным ядром языка “ C” . Имея эту горсть строительных блоков , можно писать полезные программы весьма значительного размера , и было бы вероятно неплохой идеей , если бы вы задержалис ь здесь на какое-то время и поступили таким образом : следующие ниже упражнения предлагают вам ряд программ несколько большей сложности , чем те , которые были приведены в этой главе. После того как вы овладеете этой частью “ C” , приступайте к чтению следующих нескольких глав . Усилия , которые вы при этом затратите , полностью окупятся , потому что в этих главах обсуждаются именно те стороны “ C” , где мощь и выразительность языка начинает становиться очевидной. Упражнение 1-19. Напишите программу DETAB, которая зам еняет табуляции во вводе на нужное число пробелов так , чтобы промежуток достигал следующей табуляционной остановки . Предположите фиксированный набор табуляционных остановок , например , через каждые N позиций. Упражнение 1-20. Напишите программу ENTAB, котор ая заменяет строки пробелов минимальным числом табуляций и пробелов , достигая при этом тех же самых промежутков . Используйте те же табуляционные остановки , как и в DETAB. Упражнение 1-21. Напишите программу для “сгибания” длинных вводимых строк после после днего отличного от пробела символа , стоящего до столбца N ввода , где N - параметр . убедитесь , что ваша программа делает что-то разумное с очень длинными строками и в случае , когда перед указанным столбцом нет ни табуляций , ни пробелов. Упражнение 1-22. Нап ишите программу удаления из “ C”-программы всех комментариев . Не забывайте аккуратно обращаться с “закавыченными” строками и символьными константами. Упражнение 1-23. Напишите программу проверки “ C”-программы на элементарные синтаксические ошибки , такие как несоответствие круглых , квадратных и фигурных скобок . Не забудьте о кавычках , как одиночных , так и двойных , и о комментариях . (Эта программа весьма сложна , если вы будете писать ее для самого общего случая ). 2. Типы , операции и выражения. Переменные и к онстанты являются основными объектами , с которыми оперирует программа . Описания перечисляют переменные , которые будут использоваться , указывают их тип и , возможно , их начальные значения . Операции определяют , что с ними будет сделано . выражения объединяют п еременные и константы для получения новых значений . Все это - темы настоящей главы. 2.1. Имена переменных. Хотя мы этого сразу прямо не сказали , существуют некоторые ограничения на имена переменных и символических констант . Имена составляются из букв и ц ифр ; первый символ должен быть буквой . Подчеркивание “ _” тоже считается буквой ; это полезно для удобочитаемости длинных имен переменных. Прописные и строчные буквы различаются ; традиционная практика в “с” - использовать строчные буквы для имен переменных , а прописные - для символических констант. Играют роль только первые восемь символов внутреннего имени , хотя использовать можно и больше . Для внешних имен , таких как имена функций и внешних переменных , это число может оказаться меньше восьми , так как внешни е имена используются различными ассемблерами и загрузчиками . Детали приводятся в приложении а . Кроме того , такие ключевые слова как IF, ELSE, INT, FLOAT и т.д ., зарезервированы : вы не можете использовать их в качестве имен переменных . (Они пишутся строчны м и буквами ). Конечно , разумно выбирать имена переменных таким образом , чтобы они означали нечто , относящееся к назначению переменных , и чтобы было менее вероятно спутать их при написании. 2.2. Типы и размеры данных. Языке “ C” имеется только несколько осно вных типов данных : CHAR один байт , в котором может находиться один символ из внутреннего набора символов. INT Целое , обычно соответствующее естественному размеру целых в используемой машине. FLOAT С плавающей точкой одинарной точности. DOUBLE С плавающей т очкой двойной точности. Кроме того имеется ряд квалификаторов , которые можно использовать с типом INT: SHORT (короткое ), LONG (длинное ) и UNSIGNED (без знака ). Квалификаторы SHORT и LONG указывают на различные размеры целых . Числа без знака подчиняются зак онам арифметики по модулю 2 в степени N, где N - число битов в INT; числа без знаков всегда положительны . Описания с квалификаторами имеют вид : SHORT INT X; LONG INT Y; UNSIGNED INT Z; Cлово INT в таких ситуациях может быть опущено , что обычно и делается. Количество битов , отводимых под эти объекты зависит от имеющейся машины ; в таблице ниже приведены некоторые характерные значения. Таблица 1 ! DEC PDP-11 HONEYWELL IBM 370 INTERDATA ! 6000 8/32 ! ! ASCII ASCII EBCDIC ASCII ! ! CHAR 8-BITS 9-BITS 8-BITS 8- BITS ! INT 16 36 32 32 ! SHORT 16 36 16 16 ! LONG 32 36 32 32 ! FLOAT 32 36 32 32 ! DOUBLE 64 72 64 64 ! ! Цель состоит в том , чтобы SHORT и LONG давали возможность в зависимости от практических нужд использовать различные длины целых ; тип INT отражает на иболее “естественный” размер конкретной машины . Как вы видите , каждый компилятор свободно интерпретирует SHORT и LONG в соответствии со своими аппаратными средствами . Все , на что вы можете твердо полагаться , это то , что SHORT не длиннее , чем LONG. 2.3. Ко нстанты. Константы типа INT и FLOAT мы уже рассмотрели . Отметим еще только , что как обычная 123.456е -7, так и “научная” запись 0.12е 3 для FLOAT является законной. Каждая константа с плавающей точкой считается имеющей тип DOUBLE, так что обозначение “ E” сл ужит как для FLOAT, так и для DOUBLE. Длинные константы записываются в виде 123L. Обычная целая константа , которая слишком длинна для типа INT, рассматривается как LONG. Существует система обозначений для восьмеричных и шестнадцатеричных констант : лидирующ ий 0(нуль ) в константе типа INT указывает на восьмеричную константу , а стоящие впереди 0X соответствуют шестнадцатеричной константе . Например , десятичное число 31 можно записать как 037 в восьмеричной форме и как 0X1F в шестнадцатеричной . Шестнадцатеричны е и восьмеричные константы могут также заканчиваться буквой L, что делает их относящимися к типу LONG. 2.3.1. Символьная константа. Символьная константа - это один символ , заключенный в одинарные кавычки , как , например , 'х '. Значением символьной константы является численное значение этого символа во внутреннем машинном наборе символов . Например , в наборе символов ASCII символьный нуль , или '0', имеет значение 48, а в коде EBCDIC - 240, и оба эти значения совершенно отличны от числа 0. Написание '0' вместо численного значения , такого как 48 или 240, делает программу не зависящей от конкретного численного представления этого символа в данной машине . Символьные константы точно так же участвуют в численных операциях , как и любые другие числа , хотя наиболее час т о они используются в сравнении с другими символами . Правила преобразования будут изложены позднее. Некоторые неграфические символы могут быть представлены как символьные константы с помощью условных последовательностей , как , например , \ N (новая строка ), \T (табуляция ), \ 0 (нулевой символ ), \\ (обратная косая черта ), \ ' (одинарная кавычка ) и т.д . Хотя они выглядят как два символа , на самом деле являются одним . Кроме того , можно сгенерировать произвольную последовательность двоичных знаков размером в байт , е с ли написать '\ DDD' где DDD - от одной до трех восьмеричных цифр , как в #DEFINE FORMFEED '\ 014' /* FORM FEED */ Символьная константа '\ 0', изображающая символ со значе-нием 0, часто записывается вместо целой константы 0 , чтобы подчеркнуть символьную приро ду некоторого выражения. 2.3.2. Константное выражение Константное выражение - это выражение , состоящее из одних констант . Такие выражения обрабатываются во время компиляции , а не при прогоне программы , и соответственно могут быть использованы в любом мест е , где можно использовать константу , как , например в #DEFINE MAXLINE 1000 CHAR LINE[MAXLINE+1]; или SECONDS = 60 * 60 * HOURS; 2.3.3. Строчная константа Строчная константа - это последовательность , состоящая из нуля или более символов , заключенных в дво йные кавычки , как , например, “ I AM A STRING” /* я - строка */ или “” /* NULL STRING / / нуль-строка */ Кавычки не являются частью строки , а служат только для ее ограничения . те же самые условные последовательности , которые использовались в символьных кон стантах , применяются и в строках ; символ двойной кавычки изображается как \ ”. С технической точки зрения строка представляет собой массив , элементами которого являются отдельные символы . Чтобы программам было удобно определять конец строки , компилятор авто матически помещает в конец каждой строки нуль-символ \ 0. Такое представление означает , что не накладывается конкретного ограничения на то , какую длину может иметь строка , и чтобы определить эту длину , программы должны просматривать строку полностью . При э т ом для физического хранения строки требуется на одну ячейку памяти больше , чем число заключенных в кавычки символов . Следующая функция STRLEN(S) вычисляет длину символьной строки S не считая конечный символ \0. STRLEN(S) /* RETURN LENGTH OF S */ CHAR S[]; INT I; I = 0; WHILE (S[I] != '\0') ++I; RETURN(I); Будьте внимательны и не путайте символьную константу со строкой , содержащей один символ : 'X' - это не то же самое , что “ X” . Первое - это отдельный символ , использованный с целью получения численного значения , соответствующего букве х в машинном наборе символов . Второе - символьная строка , состоящая из одного символа (буква х ) и \0. 2.4. Описания Все переменные должны быть описаны до их использования , хотя некоторые описания делаются неявно , по конте ксту . Описание состоит из спецификатора типа и следующего за ним списка переменных , имеющих этот тип , как , например, INT LOWER, UPPER, STEP; CHAR C, LINE[1000]; Переменные можно распределять по описаниям любым образом ; приведенные выше списки можно с тем же успехом записать в виде INT LOWER; INT UPPER; INT STEP; CHAR C; CHAR LINE[1000]; Такая форма занимает больше места , но она удобна для до-бавления комментария к каждому описанию и для последующих модификаций. Переменным могут быть присвоены начальные зн ачения внутри их описания , хотя здесь имеются некоторые ограничения. Если за именем переменной следуют знак равенства и константа , то эта константа служит в качестве инициализатора , как , например , в CHAR BACKSLASH = '\\'; INT I = 0; FLOAT EPS = 1.0E-5; Ес ли рассматриваемая переменная является внешней или статической , то инициализация проводится только один раз , согласно концепции до начала выполнения программы . Инициализируемым явно автоматическим переменным начальные значения присваиваются при каждом обр а щении к функции , в которой они описаны . Автоматические переменные , не инициализируемые явно , имеют неопределенные значения , (т.е . мусор ). Внешние и статические переменные по умолчанию инициализируются нулем , но , тем не менее , их явная инициализация являет с я признаком хорошего стиля. Мы продолжим обсуждение вопросов инициализации , когда будем описывать новые типы данных. 2.5. Арифметические операции. Бинарными арифметическими операциями являются +, -, *, / и операция деления по модулю %. Имеется унарная опе рация -, но не существует унарной операции +. При делении целых дробная часть отбрасывается . Выражение X % Y дает остаток от деления X на Y и , следовательно , равно нулю , когда х делится на Y точно . Например , год является високосным , если он делится на 4, н о не делится на 100, исключая то , что делящиеся на 400 годы тоже являются високосными . Поэтому IF(YEAR % 4 == 0 && YEAR % 100 != 0 \!\ ! YEAR % 400 == 0) год високосный ELSE год невисокосный Операцию % нельзя использовать с типами FLOAT или DOUBLE. Операц ии + и - имеют одинаковое старшинство , которое младше одинакового уровня старшинства операций *, / и %, которые в свою очередь младше унарного минуса . Арифметические операции группируются слева направо . (Сведения о старшинстве и ассоциативности всех опера ц ий собраны в таблице в конце этой главы ). Порядок выполнения ассоциативных и коммутативных операций типа + и - не фиксируется ; компилятор может перегруппировывать даже заключенные в круглые скобки выражения , связанные такими операциями . таким образом , а +( B +C) может быть вычислено как (A+B)+C. Это редко приводит к какому-либо расхождению , но если необходимо обеспечить строго определенный порядок , то нужно использовать явные промежуточные переменные. Действия , предпринимаемые при переполнении и антипереполнен ии (т.е . При получении слишком маленького по абсолютной величине числа ), зависят от используемой машины. 2.6. Операции отношения и логические операции Операциями отношения являются => > =< < все они имеют одинаковое старшинство . Непосредственно за ними по уровню старшинства следуют операции равенства и неравенства : == != которые тоже имеют одинаковое старшинство . операции отношения младше арифметических операций , так что выражения типа I='0' && S[I]<='9'; ++I) N = 10 * N + S[I] - '0'; RETURN(N); KAK Уже обсуждалось в главе 1, выражение S[I] - '0' имеет численное значение находящегося в S[I] символа , потому что значение символов '0', '1' и т.д . образуют возрастающую п оследовательность расположенных подряд целых положительных чисел. Другой пример преобразования CHAR в INT дает функция LOWER, преобразующая данную прописную букву в строчную . Если выступающий в качестве аргумента символ не является прописной буквой , то LOW ER возвращает его неизменным . Приводимая ниже программа справедлива только для набора символов ASCII. LOWER© /* CONVERT C TO LOWER CASE; ASCII ONLY */ INT C; IF ( C >= 'A' && C <= 'Z' ) RETURN( C + '@' - 'A'); ELSE /*@ Записано вместо 'A' строчного */ RETURN© ; Эта функция правильно работает при коде ASCII, потому что численные значения , соответствующие в этом коде прописным и строчным буквам , отличаются на постоянную величину , а каждый алфавит является сплошным - между а и Z нет ничего , кроме букв . Это последнее замечание для набора символов EBCDIC систем IBM 360/370 оказывается несправедливым , в силу чего эта программа на таких системах работает неправильно - она преобразует не только буквы. При преобразовании символьных переменных в целые возникает один тонкий момент . Дело в том , что сам язык не указывает , должны ли переменным типа CHAR соответствовать численные значения со знаком или без знака . Может ли при преобразовании CHAR в INT получиться отрицательное целое ? К сожалению , ответ на этот вопрос меняется от машины к машине , отражая расхождения в их архитектуре . На некоторых машинах (PDP-11, например ) переменная типа CHAR, крайний левый бит которой содержит 1, преобразуется в отрицательное целое (“знаковое расширение” ). На других машинах такое пре о бразование сопровождается добавлением нулей с левого края , в результате чего всегда получается положительное число. Определение языка “ C” гарантирует , что любой символ из стандартного набора символов машины никогда не даст отрицательного числа , так что эти символы можно свободно использовать в выражениях как положительные величины . Но произвольные комбинации двоичных знаков , хранящиеся как символьные переменные на некоторых машинах , могут дать отрицательные значения , а на других положительные. Наиболее типичным примером возникновения такой ситуации является сучай , когда значение 1 используется в качестве EOF. Рассмотрим программу CHAR C; C = GETCHAR(); IF ( C == EOF ) ... На машине , которая не осуществляет знакового расширения , переменная 'с ' всегда по ложительна , поскольку она описана как CHAR, а так как EOF отрицательно , то условие никогда не выполняется . Чтобы избежать такой ситуации , мы всегда предусмотрительно использовали INT вместо CHAR для любой переменной , получающей значение от GETCHAR. Основна я же причина использования INT вместо CHAR не связана с каким-либо вопросом о возможном знаковом расширении . просто функция GETCHAR должна передавать все возможные символы (чтобы ее можно было использовать для произвольного ввода ) и , кроме того , отличающе е ся значение EOF. Следовательно значение EOF не может быть представлено как CHAR, а должно храниться как INT. Другой полезной формой автоматического преобразования типов является то , что выражения отношения , подобные I>J, и логические выражения , связанные о перациями && и \!\ !, по определению имеют значение 1, если они истинны , и 0, если они ложны . Таким образом , присваивание ISDIGIT = C >= '0' && C <= '9'; полагает ISDIGIT равным 1, если с - цифра , и равным 0 в противном случае . (В проверочной части операто ров IF, WHILE, FOR и т.д . “Истинно” просто означает “не нуль” ). Неявные арифметические преобразования работают в основном , как и ожидается . В общих чертах , если операция типа + или *, которая связывает два операнда (бинарная операция ), имеет операнды разны х типов , то перед выполнением операции “низший” тип преобразуется к “высшему” и получается результат “высшего” типа . Более точно , к каждой арифметической операции применяется следующая последовательность правил преобразования. Типы CHAR и SHORT преобразуют ся в INT, а FLOAT в DOUBLE. 48 Затем , если один из операндов имеет тип DOUBLE, то другой преобразуется в DOUBLE, и результат имеет тип DOUBLE. В противном случае , если один из операндов имеет тип LONG, то другой преобразуется в LONG, и результат имеет ти п LONG. В противном случае , если один из операндов имеет тип UNSIGNED, то другой преобразуется в UNSIGNED и результат имеет тип UNSIGNED. В противном случае операнды должны быть типа INT, и результат имеет тип INT. Подчеркнем , что все переменные типа FLOAT в выражениях преобразуются в DOUBLE; в “ C” вся плавающая арифметика выполняется с двойной точностью. Преобразования возникают и при присваиваниях ; значение правой части преобразуется к типу левой , который и является типом результата . Символьные переменные преобразуются в целые либо со знаковым расширением ,либо без него , как описано выше . Обратное преобразование INT в CHAR ведет себя хорошо лишние биты высокого порядка просто отбрасываются . Таким образом INT I; CHAR C; I = C; C = I; значение 'с ' не изменя ется . Это верно независимо от того , вовлекается ли знаковое расширение или нет. Если х типа FLOAT, а I типа INT, то как х = I; так и I = х ; приводят к преобразованиям ; при этом FLOAT преобразуется в INT отбрасыванием дробной части . Тип DOUBLE преобразуетс я во FLOAT округлением . Длинные целые преобразуются в более короткие целые и в переменные типа CHAR посредством отбрасывания лишних битов высокого порядка. Так как аргумент функции является выражением , то при передаче функциям аргументов также происходит п реобразование типов : в частности , CHAR и SHORT становятся INT, а FLOAT становится DOUBLE. Именно поэтому мы описывали аргументы функций как INT и DOUBLE даже тогда , когда обращались к ним с переменными типа CHAR и FLOAT. Наконец , в любом выражении может бы ть осуществлено (“принуждено” ) явное преобразование типа с помощью конструкции , называемой перевод (CAST). В этой конструкции , имеющей вид (имя типа ) выражение Выражение преобразуется к указанному типу по правилам преобразования , изложенным выше . Факти чески точный смысл операции перевода можно описать следующим образом : выражение как бы присваивается некоторой переменной указанного типа , которая затем используется вместо всей конструкции . Например , библиотечная процедура SQRT ожидает аргумента типа DOU B LE и выдаст бессмысленный ответ , если к ней по небрежности обратятся с чем-нибудь иным . таким образом , если N целое , то выражение SQRT((DOUBLE) N) до передачи аргумента функции SQRT преобразует N к типу DOUBLE. (Отметим , что операция перевод преобразует з начение N в надлежащий тип ; фактическое содержание переменной N при этом не изменяется ). Операция перевода имрация перевода имеет тот же уровень старшинства , что и другие унарные операции , как указывается в таблице в конце этой главы. Упражнение 2-2. Соста вьте программу для функции HTOI(S), которая преобразует строку шестнадцатеричных цифр в эквивалентное ей целое значение . При этом допустимыми цифрами являются цифры от 1 до 9 и буквы от а до F. 2.8. Операции увеличения и уменьшения В языке “ C” предусмотр ены две необычные операции для увеличения и уменьшения значений переменных . Операция увеличения ++ добавляет 1 к своему операнду , а операция уменьшения— вычитает 1. Мы часто использовали операцию ++ для увеличения переменных , как , например , в IF(C == '\N') ++I; Необычный аспект заключается в том , что ++ и— можно использовать либо как префиксные операции (перед переменной , как в ++N), либо как постфиксные (после переменной : N++). Эффект в обоих случаях состоит в увеличении N. Но выражение ++N увеличивает пере менную N до использования ее значения , в то время как N++ увеличивает переменную N после того , как ее значение было использовано . Это означает , что в контексте , где используется значение переменной , а не только эффект увеличения , использование ++N и N++ п р иводит к разным результатам . Если N = 5, то х = N++; устанавливает х равным 5, а х = ++N; 50 полагает х равным 6. В обоих случаях N становится равным 6. Операции увеличения и уменьшения можно применять только к переменным ; выражения типа х =(I+J)++ являю тся незаконными. В случаях , где нужен только эффект увеличения , а само значение не используется , как , например , в IF ( C == '\N' ) NL++; выбор префиксной или постфиксной операции является делом вкуса . но встречаются ситуации , где нужно использовать именно ту или другую операцию . Рассмотрим , например , функцию SQUEEZE(S,C), которая удаляет символ 'с ' из строки S, каждый раз , как он встречается. SQUEEZE(S,C) /* DELETE ALL C FROM S */ CHAR S[]; INT C; INT I, J; FOR ( I = J = 0; S[I] != '\0'; I++) IF ( S[I] != C ) S[J++] = S[I]; S[J] = '\0'; Каждый раз , как встечается символ , отличный от 'с ', он копируется в текущую позицию J, и только после этого J увеличивается на 1, чтобы быть готовым для поступления следующего символа . Это в точности эквивалентно запи си IF ( S[I] != C ) S[J] = S[I]; J++; Другой пример подобной конструкции дает функция GETLINE, которую мы запрограммировали в главе 1, где можно заменить IF ( C == '\N' ) S[I] = C; ++I; более компактной записью IF ( C == '\N' ) S[I++] = C; В качестве третьего примера рассмотрим функцию STRCAT(S,T), которая приписывает строку т в конец строки S, образуя конкатенацию строк S и т . При этом предполагается , что в S достаточно места для хранения полученной комбинации. STRCAT(S,T) /* CONCATENATE T TO END OF S */ CHAR S[], T[]; /* S MUST BE BIG ENOUGH */ INT I, J; I = J = 0; WHILE (S[I] != '\0') / *FIND END OF S */ I++; WHILE((S[I++] = T[J++]) != '\0') /*COPY T*/ ; Tак как из T в S копируется каждый символ , то для подготовки к следующему прох ождению цикла постфиксная операция ++ применяется к обеим переменным I и J. Упражнение 2-3. Напишите другой вариант функции SQUEEZE(S1,S2), который удаляет из строки S1 каждый символ , совпадающий с каким-либо символом строки S2. Упражнение 2-4. Напишите программу для функции ANY(S1,S2), которая находит место первого появления в строке S1 какого-либо символа из строки S2 и , если строка S1 не содержит символов строки S2, возвращает значение -1. 2.9. Побитовые логические операции В языке предусмотрен ряд о пераций для работы с битами ; эти операции нельзя применять к переменным типа FLOAT или DOUBLE. & Побитовое AND \ ! Побитовое включающее OR ^ побитовое исключающее OR << сдвиг влево >> сдвиг вправо \ ^ дополнение (унарная операция ) “\” иммитирует вертикаль ную черту. Побитовая операция AND часто используется для маскирования некоторого множества битов ; например , оператор C = N & 0177 передает в 'с ' семь младших битов N , полагая остальные равными нулю . Операция 'э ' побитового OR используется для включени я битов : C = X э MASK устанавливает на единицу те биты в х , которые равны единице в MASK. Следует быть внимательным и отличать побитовые операции & и 'э ' от логических связок && и \!\ ! , Которые подразумевают вычисление значения истинности слева направо. Например , если х =1, а Y=2, то значение х &Y равно нулю , в то время как значение X&&Y равно единице ./почему ?/ Операции сдвига << и >> осуществляют соответственно сдвиг влево и вправо своего левого операнда на число битовых позиций , задаваемых правым опера н дом . Таким образом , х <<2 сдвигает х влево на две позиции , заполняя освобождающиеся биты нулями , что эквивалентно умножению на 4. Сдвиг вправо величины без знака заполняет освобождающиеся биты на некоторых машинах , таких как PDP-11, заполняются содержание м знакового бита /”арифметический сдвиг” /, а на других - нулем /”логический сдвиг” /. Унарная операция \ ^ дает дополнение к целому ; это означает , что каждый бит со значением 1 получает значение 0 и наоборот . Эта операция обычно оказывается полезной в выраже ниях типа X & \ ^077 где последние шесть битов х маскируются нулем . Подчеркнем , что выражение X&\ ^077 не зависит от длины слова и поэтому предпочтительнее , чем , например , X&0177700, где предполагается , что х занимает 16 битов . Такая переносимая форма не тр ебует никаких дополнительных затрат , поскольку \ ^077 является константным выражением и , следовательно , обрабатывается во время компиляции. Чтобы проиллюстрировать использование некоторых операций с битами , рассмотрим функцию GETBITS(X,P,N), которая возвращ ает /сдвинутыми к правому краю / начинающиеся с позиции р поле переменной х длиной N битов . мы предполагаем , что крайний правый бит имеет номер 0, и что N и р - разумно заданные положительные числа . например , GETBITS(х ,4,3) возвращает сдвинутыми к правому краю биты , занимающие позиции 4,3 и 2. GETBITS(X,P,N) /* GET N BITS FROM POSITION P */ UNSIGNED X, P, N; RETURN((X >> (P+1-N)) & \^(\^0 << N)); Операция X >> (P+1-N) сдвигает желаемое поле в правый конец слова . Описание аргумента X как UNSIGNED г арантирует , что при сдвиге вправо освобождающиеся биты будут заполняться нулями , а не содержимым знакового бита , независимо от того , на какой машине пропускается программа . Все биты константного выражения \ ^0 равны 1; сдвиг его на N позиций влево с помощь ю операции \ ^0<> & \^ \! Если е 1 и е 2 - выражения , то е 1 оп = е 2 эквивалентно е 1 = (е 1) оп (е 2) за исключением того , что выражение е 1 вычисляется только один раз . Обратите внимание на круглые скобки вокруг е 2: X *= Y + 1 то X = X * (Y + 1) не X = X * Y + 1 В качестве примера приведем функцию BITCOUNT, которая подсчитывает число равных 1 битов у ц елого аргумента. BITCOUNT(N) /* COUNT 1 BITS IN N */ UNSIGNED N; ( INT B; FOR (B = 0; N != 0; N >>= 1) IF (N & 01) B++; RETURN(B); ) Не говоря уже о краткости , такие операторы приваивания имеют то преимущество , что они лучше соответствуют образу человеч еского мышления . Мы говорим : “прибавить 2 к I” или “увеличить I на 2” , но не “взять I, прибавить 2 и поместить результат опять в I” . Итак , I += 2. Кроме того , в громоздких выражениях , подобных YYVAL[YYPV[P3+P4] + YYPV[P1+P2]] += 2 Tакая операция присваива ния облегчает понимание программы , так как читатель не должен скрупулезно проверять , являются ли два длинных выражения действительно одинаковыми , или задумываться , почему они не совпадают . Такая операция присваивания может даже помочь компилятору получить более эффективную программу. Мы уже использовали тот факт , что операция присваивания имеет некоторое значение и может входить в выражения ; самый типичный пример WHILE ((C = GETCHAR()) != EOF) присваивания , использующие другие операции присваивания (+=, -= и т.д .) также могут входить в выражения , хотя это случается реже. Типом выражения присваивания является тип его левого операнда. Упражнение 2-9. В двоичной системе счисления операция X&(X-1) обнуляет самый правый равный 1 бит переменной X.(почему ?) исп ользуйте это замечание для написания более быстрой версии функции BITCOUNT. 2.11. Условные выражения. Операторы IF (A > B) Z = A; ELSE Z = B; конечно вычисляют в Z максимум из а и в . Условное выражение , записанное с помощью тернарной операции “ ?:” , предо ставляет другую возможность для записи этой и аналогичных конструкций . В выражении е 1 ? Е 2 : е 3 сначала вычисляется выражение е 1. Если оно отлично от нуля (истинно ), то вычисляется выражение е 2, которое и становится значением условного выражения . В против ном случае вычисляется е 3, и оно становится значением условного выражения . Каждый раз вычисляется только одно из выражения е 2 и е 3. Таким образом , чтобы положить Z равным максимуму из а и в , можно написать Z = (A > B) ? A : B; /* Z = MAX(A,B) */ Следует п одчеркнуть , что условное выражение действительно является выражением и может использоваться точно так же , как любое другое выражение . Если е 2 и е 3 имеют разные типы , то тип результата определяется по правилам преобразования , рассмотренным ранее в этой гла в е . например , если F имеет тип FLOAT, а N - тип INT, то выражение (N > 0) ? F : N Имеет тип DOUBLE независимо от того , положительно ли N или нет. Так как уровень старшинства операции ?: очень низок , прямо над присваиванием , то первое выражение в условн ом выражении можно не заключать в круглые скобки . Однако , мы все же рекомендуем это делать , так как скобки делают условную часть выражения более заметной. Использование условных выражений часто приводит к коротким программам . Например , следующий ниже опера тор цикла печатает N элементов массива , по 10 в строке , разделяя каждый столбец одним пробелом и заканчивая каждую строку (включая последнюю ) одним символом перевода строки. OR (I = 0; I < N; I++) PRINTF(“ %6D%C” ,A[I],(I%10==9 \!\! I==N-1) ? '\ N' : ' ') Си мвол перевода строки записывается после каждого десятого элемента и после N-го элемента . За всеми остальными элементами следует один пробел . Хотя , возможно , это выглядит мудреным , было бы поучительным попытаться записать это , не используя условного выраже н ия. Упражнение 2-10. Перепишите программу для функции LOWER, которая переводит прописные буквы в строчные , используя вместо конструкции IF-ELSE условное выражение. 2.12. Старшинство и порядок вычисления. В приводимой ниже таблице сведены правила старшинс тва и ассоциативности всех операций , включая и те , которые мы еще не обсуждали . Операции , расположенные в одной строке , имеют один и тот же уровень старшинства ; строки расположены в порядке убывания старшинства . Так , например , операции *, / и % имеют один а ковый уровень старшинства , который выше , чем уровень операций + и -. OPERATOR ASSOCIATIVITY () [] -> . LEFT TO RIGHT ! \^ ++ -- - (TYPE) * & SIZEOF RIGHT TO LEFT * / % LEFT TO RIGHT + - LEFT TO RIGHT << >> LEFT TO RIGHT < <= > >= LEFT TO RIGHT == != LEFT TO RIGHT & LEFT TO RIGHT ^ LEFT TO RIGHT \ ! LEFT TO RIGHT && LEFT TO RIGHT \ !\! LEFT TO RIGHT ?: RIGHT TO LEFT = += -= ETC. RIGHT TO LEFT , (CHAPTER 3) LEFT TO RIGHT Операции -> и . Используются для доступа к элементам структур ; они б удут описаны в главе 6 вместе с SIZEOF (размер объекта ). В главе 5 обсуждаются операции * (косвенная адресация ) и & (адрес ). Отметим , что уровень старшинства побитовых логических операций &, ^ и э ниже уровня операций == и !=. Это приводит к тому , что осущ ествляющие побитовую проверку выражения , подобные IF ((X & MASK) == 0) ... Для получения правильных результатов должны заключаться в круглые скобки. Как уже отмечалось ранее , выражения , в которые входит одна из ассоциативных и коммутативных операций (*, + , &, ^, э ), могут перегруппировываться , даже если они заключены в круглые скобки . В большинстве случаев это не приводит к каким бы то ни было расхождениям ; в ситуациях , где такие расхождения все же возможны , для обеспечения нужного порядка вычислений можн о использовать явные промежуточные переменные. В языке “ C” , как и в большинстве языков , не фиксируется порядок вычисления операндов в операторе . Например в операторе вида X = F() + G(); сначала может быть вычислено F, а потом G, и наоборот ; поэтому , если л ибо F, либо G изменяют внешнюю переменную , от которой зависит другой операнд , то значение X может зависеть от порядка вычислений . Для обеспечения нужной последовательности промежуточные результаты можно опять запоминать во временных переменных. Подобным же образом не фиксируется порядок вычисления аргументов функции , так что оператор PRINTF(“ %D %D\ N” ,++N,POWER(2,N)); 58 может давать (и действительно дает ) на разных машинах разные результаты в зависимости от того , увеличивается ли N до или после обращения к функции POWER. Правильным решением , конечно , является запись ++N; PRINTF(“ %D %D\ N” ,N,POWER(2,N)); Обращения к функциям , вложенные операции присваивания , операции увеличения и уменьшения приводят к так называемым “побочным эффектам” - некоторые переменны е изменяются как побочный результат вычисления выражений . В любом выражении , в котором возникают побочные эффекты , могут существовать очень тонкие зависимости от порядка , в котором определяются входящие в него переменные . примером типичной неудачной ситуа ц ии является оператор A[I] = I++; Возникает вопрос , старое или новое значение I служит в качестве индекса . Компилятор может поступать разными способами и в зависимости от своей интерпретации выдавать разные результаты . Тот случай , когда происходят побочные эффекты (присваивание фактическим переменным ), - оставляется на усмотрение компилятора , так как наилучший порядок сильно зависит от архитектуры машины. Из этих рассуждений вытекает такая мораль : написание программ , зависящих от порядка вычислений , являетс я плохим методом программирования на любом языке . Конечно , необходимо знать , чего следует избегать , но если вы не в курсе , как некоторые вещи реализованы на разных машинах , это неведение может предохранить вас от неприятностей . (Отладочная программа LINT у кажет большинство мест , зависящих от порядка вычислений. 3. Поток управления Управляющие операторы языка определяют порядок вычислений . В приведенных ранее примерах мы уже встречались с наиболее употребительными управляющими конструкциями языка “ C” ; зд есь мы опишем остальные операторы управления и уточним действия операторов , обсуждавшихся ранее. 3.1. Операторы и блоки Такие выражения , как X=0, или I++, или PRINTF(...), становятся операторами , если за ними следует точка с запятой , как , например, X = 0; I++; PRINTF(...); В языке “ C” точка с запятой является признаком конца оператора , а не разделителем операторов , как в языках типа алгола. Фигурные скобки /( и /) используются для объединения описаний и операторов в составной оператор или блок , так что они оказываются синтаксически эквивалентны одному оператору. Один явный пример такого типа дают фигурные скобки , в которые заключаются операторы , составляющие функцию , другой фигурные скобки вокруг группы операторов в конструкциях IF, ELSE, WHILE и FOR .(на самом деле переменные могут быть описаны внутри любого блока ; мы поговорим об этом в главе 4). Точка с запятой никогда не ставится после первой фигурной скобки , которая завершает блок. 3.2. IF - ELSE Оператор IF - ELSE используется при необходимости сделать выбор . Формально синтаксис имеет вид IF (выражение ) оператор -1 ELSE оператор -2, Где часть ELSE является необязательной . Сначала вычисляется выражение ; если оно “истинно” /т.е . значение выражения отлично от нуля /, то выполняется оператор -1. Если о н о ложно /значение выражения равно нулю /, и если есть часть с ELSE, то вместо оператора -1 выполняется оператор -2. Так как IF просто проверяет численное значение выражения , то возможно некоторое сокращение записи . Самой очевидной возможностью является за пись IF (выражение ) вместо IF (выражение !=0) иногда такая запись является ясной и естественной , но временами она становится загадочной. То , что часть ELSE в конструкции IF - ELSE является необязательной , приводит к двусмысленности в случае , когда ELSE оп ускается во вложенной последовательности операторов IF. Эта неоднозначность разрешается обычным образом - ELSE связывается с ближайшим предыдущим IF, не содержащим ELSE. Например , в IF ( N > 0 ) IF( A > B ) Z = A; ELSE Z = B; конструкция ELSE относится к внутреннему IF, как мы и показали , сдвинув ELSE под соответствующий IF. Если это не то , что вы хотите , то для получения нужного соответствия необходимо использовать фигурные скобки : IF (N > 0) IF (A > B) Z = A; ELSE Z = B; Tакая двусмысленность особ енно пагубна в ситуациях типа IF (N > 0) FOR (I = 0; I < N; I++) IF (S[I] > 0) PRINTF(“...” ); RETURN(I); ELSE /* WRONG */ PRINTF(“ ERROR - N IS ZERO\ N” ); 61 Запись ELSE под IF ясно показывает , чего вы хотите , но компилятор не получит соответствующего указания и свяжет ELSE с внутренним IF. Ошибки такого рода очень трудно обнаруживаются. Между прочим , обратите внимание , что в IF (A > B) Z = A; ELSE Z = B; после Z=A стоит точка с запятой . Дело в том , что согласно грамматическим правилам за IF должен сле довать оператор , а выражение типа Z=A, являющееся оператором , всегда заканчивается точкой с запятой. 3.3. ELSE - IF Конструкция IF (выражение ) оператор ELSE IF (выражение ) оператор ELSE IF (выражение ) оператор ELSE оператор встречается настолько часто , ч то заслуживает отдельного краткого рассмотрения . Такая последовательность операторов IF является наиболее распространенным способом программирования выбора из нескольких возможных вариантов . выражения просматриваются последовательно ; если какое-то выражен и е оказывается истинным,то выполняется относящийся к нему оператор , и этим вся цепочка заканчивается . Каждый оператор может быть либо отдельным оператором , либо группой операторов в фигурных скобках. Последняя часть с ELSE имеет дело со случаем , когда ни од но из проверяемых условий не выполняется . Иногда при этом не надо предпринимать никаких явных действий ; в этом случае хвост ELSE оператор может быть опущен , или его можно использовать для контроля , чтобы засечь “невозможное” условие. Для иллюстрации в ыбора из трех возможных вариантов приведем программу функции , которая методом половинного деления определяет , находится ли данное значение х в отсортированном массиве V. Элементы массива V должны быть расположены в порядке возрастания . Функция возвращает н омер позиции (число между 0 и N-1), в которой значение х находится в V, и -1, если х не содержится в V. BINARY(X, V, N) /* FIND X IN V[0]...V[N-1] */ INT X, V[], N; INT LOW, HIGH, MID; LOW = 0; HIGH = N - 1; WHILE (LOW <= HIGH) MID = (LOW + HIGH) / 2 ; IF (X < V[MID]) HIGH = MID - 1; ELSE IF (X > V[MID]) LOW = MID + 1; ELSE /* FOUND MATCH */ RETURN(MID); RETURN(-1); Основной частью каждого шага алгоритма является проверка , будет ли х меньше , больше или равен среднему элементу V[MID]; использован ие конструкции ELSE - IF здесь вполне естественно. 3.4. Переключатель Оператор SWITCH дает специальный способ выбора одного из многих вариантов , который заключается в проверке совпадения значения данного выражения с одной из заданных констант и соответст вующем ветвлении . В главе 1 мы привели программу подсчета числа вхождений каждой цифры , символов пустых промежутков и всех остальных символов , использующую последовательность IF...ELSE IF...ELSE. Вот та же самая программа с переключателем. MAIN() /* CO UNT DIGITS,WHITE SPACE, OTHERS */ INT C, I, NWHITE, NOTHER, NDIGIT[10]; NWHITE = NOTHER = 0; FOR (I = 0; I < 10; I++) NDIGIT[I] = 0; WHILE ((C = GETCHAR()) != EOF) SWITCH © CASE '0': CASE '1': CASE '2': CASE '3': CASE '4': CASE '5': CASE '6': CASE '7': CASE '8': CASE '9': NDIGIT[C-'0']++; BREAK; CASE ' ': CASE '\N': CASE '\T': NWHITE++; BREAK; DEFAULT : NOTHER++; BREAK; PRINTF(“ DIGITS =” ); FOR (I = 0; I < 10; I++) PRINTF(“ %D” , NDIGIT[I]); PRINTF(“\ NWHITE SPACE = %D, OTHER = %D\ N” , NWHITE, NOTHER); Переключатель вычисляет целое выражение в круглых скобках (в данной программе - значение символа с ) и сравнивает его значение со всеми случаями (CASE). Каждый случай должен быть помечен либо целым , либо символьной константой , либо константным выражением . Е сли значение константного выражения , стоящего после вариантного префикса CASE, совпадает со значением целого выражения , то выполнение начинается с этого случая . Если ни один из случаев не подходит , то выполняется оператор после префикса DEFAULT. Префикс D E FAULT является необязательным ,если его нет , и ни один из случаев не подходит , то вообще никакие действия не выполняются . Случаи и выбор по умолчанию могут располагаться в любом порядке . Все случаи должны быть различными. Оператор BREAK приводит к неме дленному выходу из переключателя . Поскольку случаи служат только в качестве меток , то если вы не предпримите явных действий после выполнения операторов , соответствующих одному случаю , вы провалитесь на следующий случай . Операторы BREAK и RETURN являются с а мым обычным способом выхода из переключателя . Как мы обсудим позже в этой главе , оператор BREAк можно использовать и для немедленного выхода из операторов цикла WHILE, FOR и DO. Проваливание сквозь случаи имеет как свои достоинства , так и недостатки . К пол ожительным качествам можно отнести то , что оно позволяет связать несколько случаев с одним действием , как было с пробелом , табуляцией и новой строкой в нашем примере . Но в то же время оно обычно приводит к необходимости заканчивать каждый случай операторо м BREAK, чтобы избежать перехода к следующему случаю . Проваливание с одного случая на другой обычно бывает неустойчивым , так как оно склонно к расщеплению при модификации программы . За исключением , когда одному вычислению соответствуют несколько меток , про в аливание следует использовать умеренно. Заведите привычку ставить оператор BREAK после последнего случая (в данном примере после DEFAULT), даже если это не является логически необходимым . В один прекрасный день , когда вы добавите в конец еще один случай , э та маленькая мера предосторожности избавит вас от неприятностей. Упражнение 3-1. Напишите программу для функции EXPAND(S, T), которая копирует строку S в т , заменяя при этом символы табуляции и новой строки на видимые условные последовательности , как \ N и \ т . используйте переключатель. 3.5. Циклы - WHILE и FOR Мы уже сталкивались с операторами цикла WHILE и FOR. В конструкции WHILE (выражение ) оператор вычисляется выражение . Если его значение отлично от нуля , то выполняется оператор и выражение вычисляетс я снова . Этот цикл продолжается до тех пор , пока значение выражения не станет нулем , после чего выполнение программы продолжается с места после оператора. Оператор FOR (выражение 1; выражение 2; выражение 3) оператор 65 эквивалентен последовательности вы ражение 1; WHILE (выражение 2) оператор выражение 3; Грамматически все три компонента в FOR являются выражениями. наиболее распространенным является случай , когда выражение 1 и выражение 3 являются присваиваниями или обращениями к функциям , а выражен ие 2 - условным выражением . любая из трех частей может быть опущена , хотя точки с запятой при этом должны оставаться . Если отсутствует выражение 1 или выражение 3, то оно просто выпадает из расширения . Если же отсутствует проверка , выражение 2, то считает с я , как будто оно всегда истинно , так что FOR (;;) ... является бесконечным циклом , о котором предполагается , что он будет прерван другими средствами (такими как BREAK или RETURN). Использовать ли WHILE или FOR - это , в основном дело вкуса . Например в WHILE ((C = GETCHAR()) == ' ' \!\! C == '\N' \!\! C == '\T') ; /* SKIP WHITE SPACE CHARACTERS */ нет ни инициализации , ни реинициализации , так что цикл WHILе выглядит самым естественным. Цикл FOR, очевидно , предпочтительнее там , где имеется простая ини циализация и реинициализация , поскольку при этом управляющие циклом операторы наглядным образом оказываются вместе в начале цикла . Это наиболее очевидно в конструкции FOR (I = 0; I < N; I++) которая является идиомой языка “ C” для обработки первых N элемен тов массива , аналогичной оператору цикла DO в фортране и PL/1. Аналогия , однако , не полная , так как границы цикла могут быть изменены внутри цикла , а управляющая переменная сохраняет свое значение после выхода из цикла , какова бы ни была причина этого выхо да . Поскольку компонентами FOR могут быть произвольные выражения , они не ограничиваются только арифметическими прогрессиями . Тем не менее является плохим стилем включать в FOR вычисления , которые не относятся к управлению циклом , лучше поместить их в управ ляемые циклом операторы. В качестве большего по размеру примера приведем другой вариант функции ATOI, преобразующей строку в ее численный эквивалент . Этот вариант является более общим ; он допускает присутствие в начале символов пустых промежутков и зна ка + или -. (В главе 4 приведена функция ATOF, которая выполняет то же самое преобразование для чисел с плавающей точкой ). Общая схема программы отражает форму поступающих данных : пропустить пустой промежуток , если он имеется извлечь знак , если он имеется извлечь целую часть и преобразовать ее Каждый шаг выполняет свою часть работы и оставляет все в подготовленном состоянии для следующей части . Весь процесс заканчивается на первом символе , который не может быть частью числа. ATOI(S) /* CONVERT S TO INTEGER */ CHAR S[]; INT I, N, SIGN; FOR(I=0;S[I]==' ' \!\! S[I]=='\N' \!\! S[I]=='\T';I++) ; /* SKIP WHITE SPACE */ SIGN = 1; IF(S[I] == '+' \!\! S[I] == '-') /* SIGN */ SIGN = (S[I++]=='+') ? 1 : - 1; FOR( N = 0; S[I] >= '0' && S[I] <= '9'; I++) N = 10 * N + S[I] - '0'; RETURN(SIGN * N); Преимущества централизации управления циклом становятся еще более очевидными , когда имеется несколько вложенных циклов . Следующая функция сортирует массив целых чисел по методу шелла . основн ая идея сортировки по шеллу заключается в том , что сначала сравниваются удаленные элементы , а не смежные , как в обычном методе сортировки . Это приводит к быстрому устранению большой части неупорядоченности и сокращает последующую работу . Интервал между эл е ментами постепенно сокращается до единицы , когда сортировка фактически превращается в метод перестановки соседних элементов. SHELL(V, N) /* SORT V[0]...V[N-1] INTO INCREASING ORDER */ INT V[], N; INT GAP, I, J, TEMP; FOR (GAP = N/2; GAP > 0; GAP /= 2) FOR (I = GAP; I < N; I++) FOR (J=I-GAP; J>=0 && V[J]>V[J+GAP]; J-=GAP) TEMP = V[J]; V[J] = V[J+GAP]; V[J+GAP] = TEMP; Здесь имеются три вложенных цикла . Самый внешний цикл управляет интервалом между сравниваемыми элементами , уменьшая его от N/2 вдвое при каждом проходе , пока он не станет равным нулю . Средний цикл сравнивает каждую пару элементов , разделенных на величину интервала ; самый внутренний цикл переставляет любую неупорядоченную пару . Так как интервал в конце концов сводится к единице , в се элементы в результате упорядочиваются правильно . Отметим , что в силу общности конструкции FOR внешний цикл укладывается в ту же самую форму , что и остальные , хотя он и не является арифметической прогрессией. Последней операцией языка “ C” является запята я “,” , которая чаще всего используется в операторе FOR. Два выражения , разделенные запятой , вычисляются слева направо , причем типом и значением результата являются тип и значение правого операнда . Таким образом , в различные части оператора FOR можно включ и ть несколько выражений , например , для параллельного изменения двух индексов . Это иллюстрируется функцией REVERSE(S), которая располагает строку S в обратном порядке на том же месте. REVERSE(S) /* REVERSE STRING S IN PLACE */ CHAR S[]; INT C, I, J; FOR( I = 0, J = STRLEN(S) - 1; I < J; I++, J--) C = S[I]; S[I] = S[J]; S[J] = C; Запятые , которые разделяют аргументы функций , переменные в описаниях и т.д ., не имеют отношения к операции запятая и не обеспечивают вычислений слева направо. Упражнен ие 3-2. Составьте программу для функции EXPAND(S1,S2), которая расширяет сокращенные обозначения вида а -Z из строки S1 в эквивалентный полный список авс ...XYZ в S2. Допускаются сокращения для строчных и прописных букв и цифр . Будьте готовы иметь дело со сл учаями типа а-в-с , а -Z0-9 и -а -Z. (Полезное соглашение состоит в том , что символ -, стоящий в начале или конце , воспринимается буквально ). 3.6. Цикл DO - WHILE Как уже отмечалось в главе 1, циклы WHILE и FOR обладают тем приятным свойством , что в них про верка окончания осуществляется в начале , а не в конце цикла . Третий оператор цикла языка “ C” , DO-WHILE, проверяет условие окончания в конце , после каждого прохода через тело цикла ; тело цикла всегда выполняется по крайней мере один раз . Синтаксис этого оп е ратора имеет вид : DO оператор WHILE (выражение ) Сначала выполняется оператор , затем вычисляется выражение. Если оно истинно , то оператор выполняется снова и т.д . Если выражение становится ложным , цикл заканчивается. Как и можно было ожидать , цикл DO-WHIL E используется значительно реже , чем WHILE и FOR, составляя примерно пять процентов от всех циклов . Тем не менее , иногда он оказывается полезным , как , например , в следующей функции ITOA, которая преобразует число в символьную строку (обратная функции ATOI ) . Эта задача оказывается несколько более сложной , чем может показаться сначала . Дело в том , что простые методы выделения цифр генерируют их в неправильном порядке . Мы предпочли получить строку в обратном порядке , а затем обратить ее. ITOA(N,S) /*CONVE RT N TO CHARACTERS IN S */ CHAR S[]; INT N; INT I, SIGN; IF ((SIGN = N) < 0) /* RECORD SIGN */ N = -N; /* MAKE N POSITIVE */ I = 0; DO /* GENERATE DIGITS IN REVERSE ORDER */ S[I++] = N % 10 + '0';/* GET NEXT DIGIT */ WHILE ((N /=10) > 0); /* DELETE IT */ IF (SIGN < 0) S[I++] = '-' S[I] = '\0'; REVERSE(S); Цикл DO-WHILE здесь необходим , или по крайней мере удобен, поскольку , каково бы ни было значение N, массив S должен со держать хотя бы один символ . Мы заключили в фигурные скобки один оператор , составляющий тело DO-WHILе , хотя это и не обязательно , для того , чтобы торопливый читатель не принял часть WHILE за начало оператора цикла WHILE. Упражнение 3-3. ------------- При представлении чисел в двоичном дополнительном коде наш вариант ITOA не с правляется с наибольшим отрицательным числом , т.е . Со значением N р Aвным -2 в степени м -1, где м размер слова . объясните почему . Измените программу так , что бы она правильно печатала это значение на любой машине. Упражнение 3-4. ------------- Напишите а налогичную функцию ITOB(N,S), которая преобра зует целое без знака N в его двоичное символьное представле ние в S. Запрограммируйте функцию ITOH, которая преобразует целое в шестнадцатеричное представление. Упражнение 3-5. -------------- Напишите вариант Iтоа , который имеет три , а не два аргу мента . Третий аргумент - минимальная ширина поля ; преобразо ванное число должно , если это необходимо , дополняться слева пробелами , так чтобы оно имело достаточную ширину. 3.7. Оператор BREAK Иногда бывает удобны м иметь возможность управлять выходом из цикла иначе , чем проверкой условия в начале или в конце . Оператор BRеак позволяет выйти из операторов FOR, WHILE и DO до окончания цикла точно так же , как и из переключателя . Оператор BRеак приводит к немедленному в ыходу из самого внутреннего охватывающего его цикла (или переключателя ). Следующая программа удаляет хвостовые пробелы и табуляции из конца каждой строки файла ввода . Она использует оператор BRеак для выхода из цикла , когда найден крайний правый отличный о т пробела и табуляции символ. #DEFINE MAXLINE 1000 MAIN() /* REMOVE TRAILING BLANKS AND TABS */ INT N; CHAR LINE[MAXLINE]; WHILE ((N = GETLINE(LINE,MAXLINE)) > 0) WHILE (--N >= 0) IF (LINE[N] != ' ' && LINE[N] != '\T' && LINE[N] != '\N') BREAK; LINE[N+1] = '\0'; PRINTF(“ %S\ N” ,LINE); Функция GETLINE возвращает длину строки . Внутренний цикл начинается с последнего символа LINE (напомним , что— N уменьшает N до использования его значения ) и движется в обратном направлении в поиске первого символ а , который отличен от пробела , табуляции или новой строки . Цикл прерывается , когда либо найден такой символ , либо N становится отрицательным (т.е ., когда просмотрена вся строка ). Советуем вам убедиться , что такое поведение правильно и в том случае , когда строка состоит только из символов пустых промежутков. В качестве альтернативы к BRеак можно ввести проверку в сам цикл : WHILE ((N = GETLINE(LINE,MAXLINE)) > 0) WHILE (--N >= 0 && (LINE[N] == ' ' \!\! LINE[N] == '\T' \!\! LINE[N] == '\N')) ; ... Эт о уступает предыдущему варианту , так как проверка становится труднее для понимания . Проверок , которые требуют переплетения &&, \!\ !, ! И круглых скобок , по возможности следует избегать. 3.8. Оператор CONTINUE Оператор CONTINUE родственен оператору BRеак, но используется реже ; он приводит к началу следующей итерации охватывающего цикла (FOR, WHILE, DO ). В циклах WHILE и DO это означает непосредственный переход к выполнению проверочной части ; в цикле FOR управление передается на шаг реинициализации . (Опер а тор CONTINUE применяется только в циклах , но не в переключателях . Оператор CONTINUE внутри переключателя внутри цикла вызывает выполнение следующей итерации цикла ). В качестве примера приведем фрагмент , который обрабатывает только положительные элементы ма ссива а ; отрицательные значения пропускаются. FOR (I = 0; I < N; I++) IF (A[I] < 0) /* SKIP NEGATIVE ELEMENTS */ CONTINUE; ... /* DO POSITIVE ELEMENTS */ Оператор CONTINUE часто используется , когда последующая часть цикла оказывается слишком сложной , так что рассмотрение условия , обратного проверяемому , приводит к слишком глубокому уровню вложенности программы. Упражнение 3-6. Напишите программу копирования ввода на вывод , с тем исключением , что из каждой группы последовательных одинаковых строк выво дится только одна . (Это простой вариант утилиты UNIQ систем UNIX). 3.9. Оператор GOTO и метки В языке “ C” предусмотрен и оператор GOTO, которым бесконечно злоупотребляют , и метки для ветвления . С формальной точки зрения оператор GOTO никогда не является необходимым , и на практике почти всегда можно обойтись без него . Мы не использовали GOTO в этой книге. Тем не менее , мы укажем несколько ситуаций , где оператор GOTO может найти свое место . Наиболее характерным является его использование тогда , когда нужно прервать выполнение в некоторой глубоко вложенной структуре , например , выйти сразу из двух циклов . Здесь нельзя непосредственно использовать оператор BRеак , так как он прерывает только самый внутренний цикл . Поэтому : FOR ( ... ) FOR ( ... ) ... IF (DI SASTER) GOTO ERROR; ... ERROR: CLEAN UP THE MESS Если программа обработки ошибок нетривиальна и ошибки могут возникать в нескольких местах , то такая организация оказывается удобной . Метка имеет такую же форму , что и имя переменной , и за ней всегда след ует двоеточие . Метка может быть приписана к любому оператору той же функции , в которой находится оператор GOTO. В качестве другого примера рассмотрим задачу нахождения первого отрицательного элемента в двумерном массиве . (Многомерные массивы рассматриваютс я в главе 5). Вот одна из возможностей : FOR (I = 0; I < N; I++) FOR (J = 0; J < M; J++) IF (V[I][J] < 0) GOTO FOUND; /* DIDN'T FIND */ ... FOUND: /* FOUND ONE AT POSITION I, J */ ... Программа , использующая оператор GOTO, всегда может быть написана без н его , хотя , возможно , за счет повторения некоторых проверок и введения дополнительных переменных . Например , программа поиска в массиве примет вид : FOUND = 0; FOR (I = 0; I < N && !FOUND; I++) FOR (J = 0; J < M && !FOUND; J++) FOUND = V[I][J] < 0; IF (FOUND ) /* IT WAS AT I-1, J-1 */ ... ELSE /* NOT FOUND */ ... Хотя мы не являемся в этом вопросе догматиками , нам все же кажется , что если и нужно использовать оператор GOTO, то весьма умеренно. 73 4. Функции и структура программ. Функции разбивают большие вычи слительные задачи на маленькие подзадачи и позволяют использовать в работе то , что уже сделано другими , а не начинать каждый раз с пустого места . Соответствующие функции часто могут скрывать в себе детали проводимых в разных частях программы операций , зна т ь которые нет необходимости , проясняя тем самым всю программу , как целое , и облегчая мучения при внесении изменений. Язык “ C” разрабатывался со стремлением сделать функции эффективными и удобными для использования ; “ C”-программы обычно состоят из большого числа маленьких функций , а не из нескольких больших . Программа может размещаться в одном или нескольких исходных файлах любым удобным образом ; исходные файлы могут компилироваться отдельно и загружаться вместе наряду со скомпилированными ранее функциями и з библиотек . Мы здесь не будем вдаваться в детали этого процесса , поскольку они зависят от используемой системы. Большинство программистов хорошо знакомы с “библиотечными” функциями для ввода и вывода /GETCHAR , PUTCHAR/ и для численных расчетов /SIN, COS, SQRT/. В этой главе мы сообщим больше о написании новых функций. 4.1. Основные сведения. Для начала давайте разработаем и составим программу печати каждой строки ввода , которая содержит определенную комбинацию символов . /Это - специальный случай утилиты GREP системы “ UNIX” /. Например , при поиске комбинации “ THE” в наборе строк NOW IS THE TIME FOR ALL GOOD MEN TO COME TO THE AID OF THEIR PARTY в качестве выхода получим NOW IS THE TIME MEN TO COME TO THE AID OF THEIR PARTY основная схема выполнения задан ия четко разделяется на три части : WHILE (имеется еще строка ) IF (строка содержит нужную комбинацию ) вывод этой строки Конечно , возможно запрограммировать все действия в виде одной основной процедуры , но лучше использовать естественную структуру задачи и представить каждую часть в виде отдельной функции . С тремя маленькими кусками легче иметь дело , чем с одним большим , потому что отдельные не относящиеся к существу дела детали можно включить в функции и уменьшить возможность нежелательных взаимодействи й . Кроме того , эти куски могут оказаться полезными сами по себе. “Пока имеется еще строка” - это GETLINE, функция , которую мы запрограммировали в главе 1, а “вывод этой строки” это функция PRINTF, которую уже кто-то подготовил для нас. Это значит , что нам осталось только написать процедуру для определения , содержит ли строка данную комбинацию символов или нет . Мы можем решить эту проблему , позаимствовав разработку из PL/1: функция INDEX(S,т ) возвращает позицию , или индекс , строки S, где начинается строка T, и -1, если S не содержит т . В качестве начальной позиции мы используем 0, а не 1, потому что в языке “ C” массивы начинаются с позиции нуль . Когда нам в дальнейшем понадобится проверять на совпадение более сложные конструкции , нам придется заменить тольк о функцию INDEX; остальная часть программы останется той же самой. После того , как мы потратили столько усилий на разработку , написание программы в деталях не представляет затруднений . ниже приводится целиком вся программа , так что вы можете видеть , как сое диняются вместе отдельные части . Комбинация символов , по которой производится поиск , выступает пока в качестве символьной строки в аргументе функции INDEX, что не является самым общим механизмом . Мы скоро вернемся к обсуждению вопроса об инициализации сим в ольных массивов и в главе 5 покажем , как сделать комбинацию символов параметром , которому присваивается значение в ходе выполнения программы. Программа также содержит новый вариант функции GETLINE; вам может оказаться полезным сравнить его с вариантом из г лавы 1. #DEFINE MAXLINE 1000 MAIN() /* FIND ALL LINES MATCHING A PATTERN */ CHAR LINE[MAXLINE]; WHILE (GETLINE(LINE, MAXLINE) > 0) IF (INDEX(LINE, “ THE” ) >= 0) PRINTF(“ %S” , LINE); GETLINE(S, LIM) /* GET LINE INTO S, RETURN LENGTH * CHAR S[]; INT LIM; INT C, I; I = 0; WHILE(--LIM>0 && (C=GETCHAR()) != EOF && C != '\N') S[I++] = C; IF (C == '\N') S[I++] = C; S[I] = '\0'; RETURN(I); INDEX(S,T) /* RETURN INDEX OF T IN S,-1 IF NONE */ CHAR S[], T[]; INT I, J, K; FOR (I = 0; S[I] != '\ 0'; I++) FOR(J=I, K=0; T[K] !='\0' && S[J] == T[K]; J++; K++) ; IF (T[K] == '\0') RETURN(I); RETURN(-1); Каждая функция имеет вид имя (список аргументов , если они имеются ) описания аргументов , если они имеются описания и операторы , если они им еются Как и указывается , некоторые части могут отсутствовать ; минимальной функцией является DUMMY () которая не совершает никаких действий. /Такая ничего не делающая функция иногда оказывается удобной для сохранения места для дальнейшего развития программы /. если функция возвращает что-либо отличное от целого значения , то перед ее именем может стоять указатель типа ; этот вопрос обсуждается в следующем разделе. Программой является просто набор определений отдельных функций . Связь между функциями осуществляется через аргументы и возвращаемые функциями значения /в этом случае /; ее можно также осуществлять через внешние переменные . Функции могут располагаться в исходном файле в любом порядке , а сама исходная программа может размещаться на нескольки х файлах , но так , чтобы ни одна функция не расщеплялась. Оператор RETURN служит механизмом для возвращения значения из вызванной функции в функцию , которая к ней обратилась . За RETURN может следовать любое выражение : RETURN (выражение ) Вызывающая функция мо жет игнорировать возвращаемое значение , если она этого пожелает . Более того , после RETURN может не быть вообще никакого выражения ; в этом случае в вызывающую программу не передается никакого значения . Управление также возвращется в вызывающую программу бе з передачи какого-либо значения и в том случае , когда при выполнении мы “проваливаемся” на конец функции , достигая закрывающейся правой фигурной скобки . EСли функция возвращает значение из одного места и не возвращает никакого значения из другого места , эт о не является незаконным , но может быть признаком каких-то неприятностей . В любом случае “значением” функции , которая не возвращает значения , несомненно будет мусор . Отладочная программа LINT проверяет такие ошибки. Механика компиляции и загрузки “ C”-програ мм , расположенных в нескольких исходных файлах , меняется от системы к системе . В системе “ UNIX” , например , эту работу выполняет команда 'CC', упомянутая в главе 1. Предположим , что три функции находятся в трех различных файлах с именами MAIN.с , GETLINE.C и INDEX.с . Тогда команда CC MAIN.C GETLINE.C INDEX.C компилирует эти три файла , помещает полученный настраиваемый объектный код в файлы MAIN.O, GETLINE.O и INDEX.O и загружа-ет их всех в выполняемый файл , называемый A.OUT . Если имеется какая-то ошибка , с кажем в MAIN.C, то этот файл можно перекомпилировать отдельно и загрузить вместе с предыдущими объектными файлами по команде CC MAIN.C GETLIN.O INDEX.O Команда 'CC' использует соглашение о наименовании с “.с” и “.о” для того , чтобы отличить исходные файлы от объектных. Упражнение 4-1. Составьте программу для функции RINDEX(S,T), которая возвращает позицию самого правого вхождения т в S и -1, если S не содержит T. 77 4.2. Функции , возвращающие нецелые значения. До сих пор ни одна из наших программ не сод ержала какого-либо описания типа функции . Дело в том , что по умолчанию функция неявно описывается своим появлением в выражении или операторе , как , например , в WHILE (GETLINE(LINE, MAXLINE) > 0) Если некоторое имя , которое не было описано ранее , появляется в выражении и за ним следует левая круглая скобка , то оно по контексту считается именем некоторой функции . Кроме того , по умолчанию предполагается , что эта функция возвращает значение типа INT. Так как в выражениях CHAR преобразуется в INT, то нет необхо д имости описывать функции , возвращающие CHAR. Эти предположения покрывают большинство случаев , включая все приведенные до сих пор примеры. Но что происходит , если функция должна возвратить значение какого-то другого типа ? Многие численные функции , такие ка к SQRT, SIN и COS возвращают DOUBLE; другие специальные функции возвращают значения других типов . Чтобы показать , как поступать в этом случае , давайте напишем и используем функцию ATо F(S), которая преобразует строку S в эквивалентное ей плавающее число дв о йной точности . Функция ATо F является расширением ато I, варианты которой мы написали в главах 2 и 3; она обрабатывает необязательно знак и десятичную точку , а также целую и дробную часть , каждая из которых может как присутствовать , так и отсутствовать ./эта процедура преобразования ввода не очень высокого качества ; иначе она бы заняла больше места , чем нам хотелось бы /. Во-первых , сама ATо F должна описывать тип возвращаемого ею значения , поскольку он отличен от INT. Так как в выражениях тип FLOAT преобразуетс я в DOUBLE, то нет никакого смысла в том , чтобы ATOF возвращала FLOAT; мы можем с равным успехом воспользоваться дополнительной точностью , так что мы полагаем , что возвращаемое значение типа DOUBLE. Имя типа должно стоять перед именем функции , как показыв а ется ниже : DOUBLE ATOF(S) /* CONVERT STRING S TO DOUBLE */ CHAR S[]; DOUBLE VAL, POWER; INT I, SIGN; 78 FOR(I=0; S[I]==' ' \!\! S[I]=='\N' \!\! S[I]=='\T'; I++) ; /* SKIP WHITE SPACE */ SIGN = 1; IF (S[I] == '+' \!\! S[I] == '-') /* SIGN */ SIGN = ( S[I++] == '+') ? 1 : -1; FOR (VAL = 0; S[I] >= '0' && S[I] <= '9'; I++) VAL = 10 * VAL + S[I] - '0'; IF (S[I] == '.') I++; FOR (POWER = 1; S[I] >= '0' && S[I] <= '9'; I++) VAL = 10 * VAL + S[I] - '0'; POWER *= 10; RETURN(SIGN * VAL / POWER); Вторы м , но столь же важным , является то , что вызывающая функция должна объявить о том , что ATOF возвращает значение , отличное от INT типа . Такое объявление демонстрируется на примере следующего примитивного настольного калькулятора /едва пригодного для подведе н ия баланса в чековой книжке /, который считывает по одному числу на строку , причем это число может иметь знак , и складывает все числа , печатая сумму после каждого ввода. #DEFINE MAXLINE 100 MAIN() /* RUDIMENTARY DESK CALKULATOR */ DOUBLE SUM, ATOF(); CHA R LINE[MAXLINE]; SUM = 0; WHILE (GETLINE(LINE, MAXLINE) > 0) PRINTF(“\ T%.2F\ N” ,SUM+=ATOF(LINE)); Оисание DOUBLE SUM, ATOF(); говорит , что SUM является переменной типа DOUBLE , и что ATOF является функцией , возвращающей значение типа DOUBLE . Эта мнемоник а означает , что значениями как SUM, так и ATOF(...) являются плавающие числа двойной точности. Если функция ATOF не будет описана явно в обоих местах , то в “ C” предполагается , что она возвращает целое значение , и вы получите бессмысленный ответ . Если с ама ATOF и обращение к ней в MAIN имеют несовместимые типы и находятся в одном и том же файле , то это будет обнаружено компилятором . Но если ATOF была скомпилирована отдельно /что более вероятно /, то это несоответствие не будет зафиксировано , так что ATOF будет возвращать значения типа DOUBLE, с которым MAIN будет обращаться , как с INT , что приведет к бессмысленным результатам . /Программа LINT вылавливает эту ошибку /. Имея ATOF, мы , в принципе , могли бы с ее помощью написать ATOI (преобразование строки в I NT): ATOI(S) /* CONVERT STRING S TO INTEGER */ CHAR S[]; DOUBLE ATOF(); RETURN(ATOF(S)); Обратите внимание на структуру описаний и оператор RETURN. Значение выражения в RETURN (выражение ) всегда преобразуется к типу функции перед выполнением самог о возвращения . Поэтому при появлении в операторе RETURN значение функции ато F, имеющее тип DOUBLE, автоматически преобразуется в INT, поскольку функция ATOI возвращает INT. (Как обсуждалось в главе 2, преобразование значения с плавающей точкой к типу INT о существляется посредством отбрасывания дробной части ). Упражнение 4-2. Расширьте ATOF таким образом , чтобы она могла работать с числами вида 123.45е -6 где за числом с плавающей точкой может следовать 'E' и показатель экспоненты , возможно со знаком. 4.3. Е ще об аргументах функций. В главе 1 мы уже обсуждали тот факт , что аргументы функций передаются по значению , т.е . вызванная функция получает свою временную копию каждого аргумента , а не его адрес . это означает , что вызванная функция не может воздействова ть на исходный аргумент в вызывающей функции . Внутри функции каждый аргумент по существу является локальной переменной , которая инициализируется тем значением , с которым к этой функции обратились. Если в качестве аргумента функции выступает имя массива , то передается адрес начала этого массива ; сами элементы не копируются . Функция может изменять элементы массива , используя индексацию и адрес начала . Таким образом , массив передается по ссылке . В главе 5 мы обсудим , как использование указателей позволяет функциям воздействовать на отличные от массивов переменные в вызывающих функциях. Между прочим , несуществует полностью удовлетворительного способа написания переносимой функции с переменным числом аргументов . Дело в том , что нет переносимого способа , с пом ощью которого вызванная функция могла бы определить , сколько аргументов было фактически передано ей в данном обращении . Таким образом , вы , например , не можете написать действительно переносимую функцию , которая будет вычислять максимум от произвольного чи с ла аргументов , как делают встроенные функции MAX в фортране и PL/1. Обычно со случаем переменного числа аргументов безопасно иметь дело , если вызванная функция не использует аргументов , которые ей на самом деле не были переданы , и если типы согласуются . Са мая распространенная в языке “ C” функция с переменным числом - PRINTF . Она получает из первого аргумента информацию , позволяющую определить количество остальных аргументов и их типы . Функция PRINTF работает совершенно неправильно , если вызывающая функция передает ей недостаточное количество аргументов , или если их типы не согласуются с типами , указанными в первом аргументе . Эта функция не является переносимой и должна модифицироваться при использовании в различных условиях. Если же типы аргументов известны , то конец списка аргументов можно отметить , используя какое-то соглашение ; например , считая , что некоторое специальное значение аргумента (часто нуль ) является признаком конца аргументов. 4.4. Внешние переменные. Программа на языке “ C” состоит из набора внешних объектов , которые являются либо переменными , либо функциями . Термин “внешний” используется главным образом в противопоставление термину “внутренний” , которым описываются аргументы и автоматические переменные , определенные внурти функций. Внешние п еременные определены вне какой-либо функции и , таким образом , потенциально доступны для многих функций . Сами функции всегда являются внешними , потому что правила языка “ C” не разрешают определять одни функции внутри других . По умолчанию внешние переменные являются также и “глобальными” , так что все ссылки на такую переменную , использующие одно и то же имя (даже из функций , скомпилированных независимо ), будут ссылками на одно и то же . В этом смысле внешние переменные аналогичны переменным COм MON в фортране и EXTERNAL в PL/1. Позднее мы покажем , как определить внешние переменные и функции таким образом , чтобы они были доступны не глобально , а только в пределах одного исходного файла. В силу своей глобальной доступности внешние переменные предоставляют друг ую , отличную от аргументов и возвращаемых значений , возможность для обмена данными между функциями. Если имя внешней переменной каким-либо образом описано , то любая функция имеет доступ к этой переменной , ссылаясь к ней по этому имени. В случаях , когда свя зь между функциями осуществляется с помощью большого числа переменных , внешние переменные оказываются более удобными и эффективными , чем использование длинных списков аргументов . Как , однако , отмечалось в главе 1, это соображение следует использовать с оп р еделенной осторожностью , так как оно может плохо отразиться на структуре программ и приводить к программам с большим числом связей по данным между функциями. Вторая причина использования внешних переменных связана с инициализацией . В частности , внешние мас сивы могут быть инициализированы а автоматические нет . Мы рассмотрим вопрос об инициализации в конце этой главы. Третья причина использования внешних переменных обусловлена их областью действия и временем существования . Автоматические переменные являются в нутренними по отношению к функциям ; они возникают при входе в функцию и исчезают при выходе из нее . Внешние переменные , напротив , существуют постоянно . Они не появляютя и не исчезают , так что могут сохранять свои значения в период от одного обращения к фу н кции до другого . В силу этого , если две функции используют некоторые общие данные , причем ни одна из них не обращается к другой , то часто наиболее удобным оказывается хранить эти общие данные в виде внешних переменных , а не передавать их в функцию и обра т но с помощью аргументов. Давайте продолжим обсуждение этого вопроса на большом примере . Задача будет состоять в написании другой программы для калькулятора , лучшей,чем предыдущая . Здесь допускаются операции +,-,*,/ и знак = (для выдачи ответа ).вместо инфик сного представления калькулятор будет использовать обратную польскую нотацию,поскольку ее несколько легче реализовать.в обратной польской нотации знак следует за операндами ; инфиксное выражение типа (1-2)*(4+5)= записывается в виде 12-45+*= круглые скобки при этом не нужны Реализация оказывается весьма простой.каждый операнд помещается в стек ; когда поступает знак операции,нужное число операндов (два для бинарных операций ) вынимается,к ним применяется операция и результат направляется обратно в стек.так в приведенном выше примере 1 и 2 помещаются в стек и затем заменяются их разностью , -1.после этого 4 и 5 вводятся в стек и затем заменяются своей суммой ,9.далее числа 1 и 9 заменяются в стеке на их произведение,равное -9.опе-рация = печатает верхний элем е нт стека , не удаляя его (так что промежуточные вычисления могут быть проверены ). Сами операции помещения чисел в стек и их извлечения очень просты,но , в связи с включением в настоящую программу обнаружения ошибок и восстановления,они оказываются достаточно длинными . Поэтому лучше оформить их в виде отдельных функций,чем повторять соответствующий текст повсюду в программе . Кроме того , нужна отдельная функция для выборки из ввода следующей операции или операнда . Таким образом , структура программы имеет вид : WHILE( поступает операция или операнд , а не конец IF ( число ) поместить его в стек е LSE IF ( операция ) вынуть операнды из стека выполнить операцию поместить результат в стек ELSE ошибка Основной вопрос , который еще не был обсужден , заключается в том,где поместить стек , т . Е . Какие процедуры смогут обращаться к нему непосредственно . Одна из таких возможностей состоит в помещении стека в MAIN и передачи самого стека и текущей позиции в стеке функциям , работающим со стеком . Но функции MAIN нет необходимости иметь дело с переменными , управляющими стеком ; ей естественно рассуждать в терминах помещения чисел в стек и извлечения их оттуда . В силу этого мы решили сделать стек и связанную с ним информацию внешними переменными , доступными функциям PUSH (помещение в стек ) и POP (извлечение из стека ), но не MAIN. Перевод этой схемы в программу достаточно прост . Ведущая программа является по существу большим переключателем по типу операции или операнду ; это , по-видимому , более характерное применеие переключателя , чем т о , которое было продемонстрировано в главе 3. #DEFINE MAXOP 20 /* MAX SIZE OF OPERAND, OPERА TOR * #DEFINE NUMBER '0' /* SIGNAL THAT NUMBER FOUND */ #DEFINE TOOBIG '9' /* SIGNAL THAT STRING IS TOO BIG * MAIN() /* REVERSE POLISH DESK CALCULATOR */ /( INT TUPE; CHAR S[MAXOP]; DOUBLE OP2,ATOF(),POP(),PUSH(); WHILE ((TUPE=GETOP(S,MAXOP)) !=EOF); SWITCH(TUPE) /( CASE NUMBER: PUSH(ATOF(S)); BREAK; CASE '+': PUSH(POP()+POP()); BREAK; CASE '*': PUSH(POP()*POP()); BREAK; CASE '-': OP2=POP(); PUSH(POP()-OP2) ; BREAK; CASE '/': OP2=POP(); IF (OP2 != 0.0) PUSH(POP()/OP2); ELSE PRINTF(“ ZERO DIVISOR POPPED\ N” ); BREAK; CASE '=': PRINTF(“\ T%F\ N” ,PUSH(POP())); BREAK; CASE 'C': CLEAR(); BREAK; CASE TOOBIG: PRINTF(“ %.20S ... IS TOO LONG\ N” ,S) BREAK; /) /) #DEFINE MAXV AL 100 /* MAXIMUM DEPTH OF VAL STACK */ 84 INT SP = 0; /* STACK POINTER */ DOUBLE VAL[MAXVAL]; /*VALUE STACK */ DOUBLE PUSH(F) /* PUSH F ONTO VALUE STACK */ DOUBLE F; /( IF (SP < MAXVAL) RETURN(VAL[SP++] =F); ELSE /( PRINTF(“ ERROR: STACK FULL\ N” ); CLEAR(); RETURN(0); /) /) DOUBLE POP() /* POP TOP VALUE FROM STEACK */ /( IF (SP > 0) RETURN(VAL[--SP]); ELSE /( PRINTF(“ ERROR: STACK EMPTY\ N” ); CLEAR(); RETURN(0); /) /) CLEAR() /* CLEAR STACK */ /( SP=0; /) Команда C очищает стек с помощью фун кции CLEAR, которая также используется в случае ошибки функциями PUSH и POP. к функции GETOP мы очень скоро вернемся. Как уже говорилось в главе 1, переменная является внешней , если она определена вне тела какой бы то ни было функции . Поэтому стек и указат ель стека , которые должны использоваться функциями PUSH, POP и CLEAR, определены вне этих трех функций . Но сама функция MAIN не ссылается ни к стеку , ни к указателю стека - их участие тщательно замаскировано . В силу этого часть программы , соответствующая о перации = , использует конструкцию PUSH(POP()); для того , чтобы проанализировать верхний элемент стека , не изменяя его. Отметим также , что так как операции + и * коммутативны , порядок , в котором объединяются извлеченные операнды , несущественен , но в случ ае операций - и / необходимо различать левый и правый операнды. Упражнение 4-3. Приведенная основная схема допускает непосредственное расширение возможностей калькулятора . Включите операцию деления по модулю /%/ и унарный минус . Включите команду “стере ть” , которая удаляет верхний элемент стека . Введите команды для работы с переменными . /Это просто , если имена переменных будут состоять из одной буквы из имеющихся двадцати шести букв /. 4.5. Правила , определяющие область действия. Функции и внешние перем енные , входящие в состав “ C”-программы , не обязаны компилироваться одновременно ; программа на исходном языке может располагаться в нескольких файлах , и ранее скомпилированные процедуры могут загружаться из библиотек . Два вопроса представляют интерес : Как с ледует составлять описания , чтобы переменные правильно воспринимались во время компиляции ? Как следует составлять описания , чтобы обеспечить правильную связь частей программы при загрузке ? 4.5.1. Область действия. Областью действия имени является та час ть программы , в которой это имя определено . Для автоматической переменной , описанной в начале функции , областью действия является та функция , в которой описано имя этой переменной , а переменные из разных функций , имеющие одинаковое имя , считаются не относ я щимися друг к другу . Это же справедливо и для аргументов функций. Область действия внешней переменной простирается от точки , в которой она объявлена в исходном файле , до конца этого файла . Например , если VAL, SP, PUSH, POP и CLEAR определены в одном файле в порядке , указанном выше , а именно : INT SP = 0; DOUBLE VAL[MAXVAL]; DOUBLE PUSH(F) ... DOUBLE POP() ... CLEAR() ... то переменные VAL и SP можно использовать в PUSH, POP и CLEAR прямо по имени ; никакие дополнительные описания не нужны. С другой сто роны , если нужно сослаться на внешнюю переменную до ее определения , или если такая переменная определена в файле , отличном от того , в котором она используется , то необходимо описание EXTERN. Важно различать описание внешней переменной и ее определение. описание указывает свойства переменной /ее тип , размер и т.д ./; определение же вызывает еще и отведение памяти. Если вне какой бы то ни было функции появляются строчки INT SP; DOUBLE VAL[MAXVAL]; то они определяют внешние переменные SP и VAL, вызывают от ведение памяти для них и служат в качестве описания для остальной части этого исходного файла . В то же время строчки EXTERN INT SP; EXTERN DOUBLE VAL[]; описывают в остальной части этого исходного файла переменную SP как INT, а VAL как массив типа DOUBLE /размер которого указан в другом месте /, но не создают переменных и не отводят им места в памяти. Во всех файлах , составляющих исходную программу , должно содержаться только одно определение внешней переменной ; другие файлы могут содержать описания EXTERN д ля доступа к ней. /Описание EXTERN может иметься и в том файле , где находится определение /. Любая инициализация внешней переменной проводится только в определении . В определении должны указываться размеры массивов , а в описании EXTERN этого можно не делать. Хотя подобная организация приведенной выше программы и маловероятна , но VAL и SP могли бы быть определены и инициализированы в одном файле , а функция PUSH, POP и CLEAR определены в другом . В этом случае для связи были бы необходимы следующие определения и описания : в файле 1: INT SP = 0; /* STACK POINTER */ DOUBLE VAL[MAXVAL]; /* VALUE STACK */ в файле 2: EXTERN INT SP; EXTERN DOUBLE VAL[]; DOUBLE PUSH(F) ... DOUBLE POP() ... CLEAR() ... так как описания EXTERN 'в файле 1' находятся выше и вне трех указанных функций , они относятся ко всем ним ; одного набора описаний достаточно для всего 'файла 2'. Для программ большого размера обсуждаемая позже в этой главе возможность включения файлов , #INCLUDE, позволяет иметь во всей программе только одн у копию описаний EXTERN и вставлять ее в каждый исходный файл во время его компиляции. Обратимся теперь к функции GETOP, выбирающей из файла ввода следующую операцию или операнд . Основная задача проста : пропустить пробелы , знаки табуляции и новые строки . Е сли следующий символ отличен от цифры и десятичной точки , то возвратить его . В противном случае собрать строку цифр /она может включать десятичную точку / и возвратить NUMBER как сигнал о том , что выбрано число. Процедура существенно усложняется , если стрем иться правильно обрабатывать ситуацию , когда вводимое число оказывается слишком длинным . Функция GETOP считывает цифры подряд /возможно с десятичной точкой / и запоминает их , пока последовательность не прерывается . Если при этом не происходит переполнения, то функция возвращает NUMBER и строку цифр. Если же число оказывается слишком длинным , то GETOP отбрасывает остальную часть строки из файла ввода , так что пользователь может просто перепечатать эту строку с места ошибки ; функция возвращает TOOBIG как сигна л о переполнении. GETOP(S, LIM) /* GET NEXT OPRERATOR OR OPERAND */ CHAR S[]; INT LIM; INT I, C; WHILE((C=GETCH())==' '\!\! C=='\T' \!\! C=='\N') ; IF (C != '.' && (C < '0' \!\ ! C > '9')) RETURN© ; S[0] = C; FOR(I=1; (C=GETCHAR()) >='0' && C <= '9'; I++ ) IF (I < LIM) S[I] = C; IF (C == '.') /* COLLECT FRACTION */ IF (I < LIM) S[I] = C; FOR(I++;(C=GETCHAR()) >='0' && C<='9';I++) IF (I < LIM) S[I] =C; IF (I < LIM) /* NUMBER IS OK */ UNGETCH© ; S[I] = '\0'; RETURN (NUMBER); ELSE /* IT'S TOO BIG; SKIP REST OF LINE */ WHILE (C != '\N' && C != EOF) C = GETCHAR(); S[LIM-1] = '\0'; RETURN (TOOBIG); Что же представляют из себя функции 'GETCH' и 'UNGETCH'? Часто так бывает , что программа , считывающая входные данные , не может опр еделить , что она прочла уже достаточно , пока она не прочтет слишком много . Одним из примеров является выбор символов , составляющих число : пока не появится символ , отличный от цифры , число не закончено . Но при этом программа считывает один лишний символ , с и мвол , для которого она еще не подготовлена. Эта проблема была бы решена , если бы было бы возможно “прочесть обратно” нежелательный символ . Тогда каждый раз , прочитав лишний символ , программа могла бы поместить его обратно в файл ввода таким образом , что ос тальная часть программы могла бы вести себя так , словно этот символ никогда не считывался . к счастью , такое неполучение символа легко иммитировать , написав пару действующих совместно функций . Функция GETCH доставляет следующий символ ввода , подлежащий рас с мотрению ; функция UNGETCH помещает символ назад во ввод , так что при следующем обращении к GETCH он будет возвращен. То , как эти функции совместно работают , весьма просто. Функция UNGETCH помещает возвращаемые назад символы в совместно используемый буфер , являющийся символьным массивом. Функция GETCH читает из этого буфера , если в нем что-либо имеется ; если же буфер пуст , она обращается к GETCHAR. При этом также нужна индексирующая переменная , которая будет фиксировать позицию текущего символа в буфере. Так как буфер и его индекс совместно используются функциями GETCH и UNGETCH и должны сохранять свои значения в период между обращениями , они должны быть внешними для обеих функций . Таким образом , мы можем написать GETCH, UNGETCH и эти переменные как : #DEFINE BUFSIZE 100 CHAR BUF[BUFSIZE]; /* BUFFER FOR UNGETCH */ INT BUFP = 0; /* NEXT FREE POSITION IN BUF */ GETCH() /* GET A (POSSIBLY PUSHED BACK) CHARACTER */ RETURN((BUFP > 0) ? BUF[--BUFP] : GETCHAR()); UNGETCH© /* PUSH CHARACTER BACK ON INPUT */ INT C; IF (BUFP > BUFSIZE) PRINTF(“ UNGETCH: TOO MANY CHARACTERS\ N” ); ELSE BUF [BUFP++] = C; Мы использовали для хранения возвращаемых символов массив , а не отдельный символ , потому что такая общность может пригодиться в дальнейшем. Упражнение 4-4. Напишите функцию UNGETS(S) , которая будет возвращать во ввод целую строку . Должна ли UNGETS иметь дело с BUF и BUFP или она может просто использовать UNGETCH ? Упражнение 4-5. Предположите , что может возвращаться только один символ . Измените GETCH и UNGETCH соответствующим образом. Упражнение 4-6. Наши функции GETCH и UNGETCH не обеспечивают обработку возвращенного символа EOF переносимым образом . Решите , каким свойством должны обладать эти функции , если возвращается EOF, и реализуйте ваши выводы. 4 .6. Статические переменные. Статические переменные представляют собой третий класс памяти , в дополнении к автоматическим переменным и EXTERN, с которыми мы уже встречались. Статические переменные могут быть либо внутренними , либо внешними . Внутренние стат ические переменные точно так же , как и автоматические , являются локальными для некоторой функции , но , в отличие от автоматических , они остаются существовать , а не появляются и исчезают вместе с обращением к этой функции . это означает , что внутренние стати ч еские переменные обеспечивают постоянное , недоступное извне хранение внутри функции . Символьные строки , появляющиеся внутри функции , как , например , аргументы PRINTF , являются внутренними статическими. Внешние статические переменные определены в остальной части того исходного файла , в котором они описаны , но не в каком-либо другом файле . Таким образом , они дают способ скрывать имена , подобные BUF и BUFP в комбинации GETCH-UNGETCH, которые в силу их совместного использования должны быть внешними , но все же н е доступными для пользователей GETCH и UNGETCH , чтобы исключалась возможность конфликта . Если эти две функции и две переменные объеденить в одном файле следующим образом STATIC CHAR BUF[BUFSIZE]; /* BUFFER FOR UNGETCH */ STATIC INT BUFP=0; /*NEXT FREE PO SITION IN BUF */ GETCH() ... UNGETCH() ... то никакая другая функция не будет в состоянии обратиться к BUF и BUFP; фактически , они не будут вступать в конфликт с такими же именами из других файлов той же самой программы. Статическая память , как внутрен няя , так и внешняя , специфицируется словом STATIC , стоящим перед обычным описанием . Переменная является внешней , если она описана вне какой бы то ни было функции , и внутренней , если она описана внутри некоторой функции. Нормально функции являются внеш ними объектами ; их имена известны глобально . возможно , однако , объявить функцию как STATIC ; тогда ее имя становится неизвестным вне файла , в котором оно описано. В языке “ C” “ STATIC” отражает не только постоянство , но и степень того , что можно назвать “пр иватностью” . Внутренние статические объекты определены только внутри одной функции ; внешние статические объекты /переменные или функции / определены только внутри того исходного файла , где они появляются , и их имена не вступают в конфликт с такими же именам и переменных и функций из других файлов. Внешние статические переменные и функции предоставляют способ организовывать данные и работающие с ними внутренние процедуры таким образом , что другие процедуры и данные не могут прийти с ними в конфликт даже по нед оразумению . Например , функции GETCH и UNGETCH образуют “модуль” для ввода и возвращения символов ; BUF и BUFP должны быть статическими , чтобы они не были доступны извне . Точно так же функции PUSH, POP и CLEAR формируют модуль обработки стека ; VAR и SP тоже должны быть внешними статическими. 4.7. Регистровые переменные. Четвертый и последний класс памяти называется регистровым . Описание REGISTER указывает компилятору , что данная переменная будет часто использоваться . Когда это возможно , переменные , описанны е как REGISTER, располагаются в машинных регистрах , что может привести к меньшим по размеру и более быстрым программам . Описание REGISTER выглядит как REGISTER INT X; REGISTER CHAR C; и т.д .; часть INT может быть опущена . Описание REGISTER можно использов ать только для автоматических переменных и формальных параметров функций . В этом последнем случае описания выглядят следующим образом : F(C,N) REGISTER INT C,N; REGISTER INT I; ... На практике возникают некоторые ограничения на регистровые переме нные , отражающие реальные возможности имеющихся аппаратных средств . В регистры можно поместить только несколько переменных в каждой функции , причем только определенных типов . В случае превышения возможного числа или использования неразрешенных типов слово REGISTER игнорируется. Кроме того невозможно извлечь адрес регистровой переменной (этот вопрос обсуждается в главе 5). Эти специфические ограничения варьируются от машины к машине . Так , например , на PDP-11 эффективными являются только первые три описания R EGISTER в функции , а в качестве типов допускаются INT, CHAR или указатель. 4.8. Блочная структура. Язык “ C” не является языком с блочной структурой в смысле PL/1 или алгола ; в нем нельзя описывать одни функции внутри других. Переменные же , с другой сторо ны , могут определяться по методу блочного структурирования . Описания переменных (включая инициализацию ) могут следовать за левой фигурной скобкой,открывающей любой оператор , а не только за той , с которой начинается тело функции . Переменные , описанные таки м образом , вытесняют любые переменные из внешних блоков , имеющие такие же имена , и остаются определенными до соответствующей правой фигурной скобки . Например в IF (N > 0) INT I; /* DECLARE A NEW I */ FOR (I = 0; I < N; I++) ... Областью действия пере менной I является “истинная” ветвь IF; это I никак не связано ни с какими другими I в программе. Блочная структура влияет и на область действия внешних переменных . Если даны описания INT X; F() DOUBLE X; ... То появление X внутри функции F относится к внутренней переменной типа DOUBLE, а вне F - к внешней целой переменной. это же справедливо в отношении имен формальных параметров : INT X; F(X) DOUBLE X; ... Внутри функции F имя X относится к формальному параметру , а не к внешней переменной. 4.9. Инициализация. Мы до сих пор уже много раз упоминали инициализацию , но всегда мимоходом , среди других вопросов . Теперь , после того как мы обсудили различные классы памяти , мы в этом разделе просуммируем некоторые правила , относящиеся к инициализации. Если явная инициализация отсутствует , то внешним и статическим переменным присваивается значение нуль ; автоматические и регистровые переменные имеют в этом случае неопределенные значения (мусор ). Простые переменные (не массивы или структуры ) можно ин ициализировать при их описании , добавляя вслед за именем знак равенства и константное выражение : INT X = 1; CHAR SQUOTE = '\ ” ; LONG DAY = 60 * 24; /* MINUTES IN A DAY */ Для внешних и статических переменных инициализация выполняется только один раз , на эта пе компиляции . Автоматические и регистровые переменные инициализируются каждый раз при входе в функцию или блок. В случае автоматических и регистровых переменных инициализатор не обязан быть константой : на самом деле он может быть любым значимым выражением , которое может включать определенные ранее величины и даже обращения к функциям . Например , инициализация в программе бинарного поиска из главы 3 могла бы быть записана в виде BINARY(X, V, N) INT X, V[], N; INT LOW = 0; INT HIGH = N - 1; INT MID; ... вместо BINARY(X, V, N) INT X, V[], N; INT LOW, HIGH, MID; LOW = 0; HIGH = N - 1; ... По своему результату , инициализации автоматических переменных являются сокращенной записью операторов присваивания. Какую форму предпочесть - в основном дело вкуса . мы обычно используем явные присваивания , потому что инициализация в описаниях менее заметна. Автоматические массивы не могут быть инициализированы . Внешние и статические массивы можно инициализировать , помещая вслед за описанием заключенный в фигурн ые скобки список начальных значений , разделенных запятыми . Например программа подсчета символов из главы 1, которая начиналась с MAIN() /* COUNT DIGITS, WHITE SPACE, OTHERS */ ( INT C, I, NWHITE, NOTHER; INT NDIGIT[10]; NWHITE = NOTHER = 0; FOR (I = 0; I < 10; I++) NDIGIT[I] = 0; ... ) Ожет быть переписана в виде INT NWHITE = 0; INT NOTHER = 0; INT NDIGIT[10] = 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 ; MAIN() /* COUNT DIGITS, WHITE SPACE, OTHERS */ ( INT C, I; ... ) Эти инициализации фактически не нужны , так как все присваиваемые значения равны нулю , но хороший стиль - сделать их явными . Если количество начальных значений меньше , чем указанный размер массива , то остальные элементы заполняются нулями . Перечисление слишком большого числа начальны х значений является ошибкой . К сожалению , не предусмотрена возможность указания , что некоторое начальное значение повторяется , и нельзя инициализировать элемент в середине массива без перечисления всех предыдущих. Для символьных массивов существует специаль ный способ инициализации ; вместо фигурных скобок и запятых можно использовать строку : CHAR PATTERN[] = “ THE” ; Это сокращение более длинной , но эквивалентной записи : CHAR PATTERN[] = 'T', 'H', 'E', '\0' ; Если размер массива любого типа опущен , то компил ятор определяет его длину , подсчитывая число начальных значений . В этом конкретном случае размер равен четырем (три символа плюс конечное \0). 4.10. Рекурсия. В языке “ C” функции могут использоваться рекурсивно ; это означает , что функция может прямо и ли косвенно обращаться к себе самой . Традиционным примером является печать числа в виде строки символов . как мы уже ранее отмечали , цифры генерируются не в том порядке : цифры младших разрядов появляются раньше цифр из старших разрядов , но печататься они д о лжны в обратном порядке. Эту проблему можно решить двумя способами . Первый способ , которым мы воспользовались в главе 3 в функции ITOA, заключается в запоминании цифр в некотором массиве по мере их поступления и последующем их печатании в обратном порядке. Первый вариант функции PRINTD следует этой схеме. PRINTD(N) /* PRINT N IN DECIMAL */ INT N; CHAR S[10]; INT I; IF (N < 0) PUTCHAR('-'); N = -N; I = 0; DO S[I++] = N % 10 + '0'; /* GET NEXT CHAR */ WHILE ((N /= 10) > 0); /* DISCARD IT */ WHILE (--I >= 0) PUTCHAR(S[I]); Альтернативой этому способу является рекурсивное решение , когда при каждом вызове функция PRINTD сначала снова обращается к себе , чтобы скопировать лидирующие цифры , а затем печа тает последнюю цифру. PRINTD(N) /* PRINT N IN DECIMAL (RECURSIVE)*/ INT N; ( INT I; IF (N < 0) PUTCHAR('-'); N = -N; IF ((I = N/10) != 0) PRINTD(I); PUTCHAR(N % 10 + '0'); ) Когда функция вызывает себя рекурсивно , при каждом обращении образуется новый набор всех автоматических переменных , совершенно не зависящий от предыдущего набора . Таким образом , в PRINTD(123) первая функция PRINTD имеет N = 123. Она передает 12 второй PRINTD, а когда та возвращает управление ей , печатает 3. Точно так же втор а я PRINTD передает 1 третьей (которая эту единицу печатает ), а затем печатает 2. Рекурсия обычно не дает никакой экономиии памяти , поскольку приходится где-то создавать стек для обрабатываемых значений . Не приводит она и к созданию более быстрых программ . Н о рекурсивные программы более компактны , и они зачастую становятся более легкими для понимания и написания . Рекурсия особенно удобна при работе с рекурсивно определяемыми структурами данных , например , с деревьями ; хороший пример будет приведен в главе 6. У пражнение 4-7. Приспособьте идеи , использованные в PRINTD для рекурсивного написания ITOA; т.е . Преобразуйте целое в строку с помощью рекурсивной процедуры. Упражнение 4-8. Напишите рекурсивный вариант функции REVERSE(S), которая располагает в обратном пор ядке строку S. 4.11. Препроцессор языка “ C”. В языке “с” предусмотрены определенные расширения языка с помощью простого макропредпроцессора . одним из самых распространенных таких расширений , которое мы уже использовали , является конструкция #DEFINE; други м расширением является возможность включать во время компиляции содержимое других файлов. 4.11.1. Включение файлов Для облегчения работы с наборами конструкций #DEFINE и описаний (среди прочих средств ) в языке “с” предусмотрена возможность включения файл ов . Любая строка вида #INCLUDE “ FILENAME” заменяется содержимым файла с именем FILENAME. (Кавычки обязательны ). Часто одна или две строки такого вида появляются в начале каждого исходного файла , для того чтобы включить общие конструкции #DEFINE и описания EXTERN для глобальных переменных . Допускается вложенность конструкций #INCLUDE. Конструкция #INCLUDE является предпочтительным способом связи описаний в больших программах . Этот способ гарантирует , что все исходные файлы будут снабжены одинаковыми определ ениями и описаниями переменных , и , следовательно , исключает особенно неприятный сорт ошибок . Естественно , когда какой -TO включаемый файл изменяется , все зависящие от него файлы должны быть перекомпилированы. 4.11.2. Макроподстановка Определение вида # DEFINE TES 1 приводит к макроподстановке самого простого вида - замене имени на строку символов . Имена в #DEFINE имеют ту же самую форму , что и идентификаторы в “с” ; заменяющий текст совершенно произволен . Нормально заменяющим текстом является остальная ч а сть строки ; длинное определение можно продолжить , поместив \ в конец продолжаемой строки . “Область действия” имени , определенного в #DEFINE, простирается от точки определения до конца исходного файла . имена могут быть переопределены , и определения могут и с пользовать определения , сделанные ранее . Внутри заключенных в кавычки строк подстановки не производятся , так что если , например , YES - определенное имя , то в PRINTF(“ YES” ) не будет сделано никакой подстановки. Так как реализация #DEFINE является частью раб оты ма Kропредпроцессора , а не собственно компилятора , имеется очень мало грамматических ограничений на то , что может быть определено . Так , например , любители алгола могут объявить #DEFINE THEN #DEFINE BEGIN #DEFINE END ; и затем написать IF (I > 0) TH EN BEGIN A = 1; B = 2 END Имеется также возможность определения макроса с аргументами , так что заменяющий текст будет зависеть от вида обращения к макросу . Определим , например , макрос с именем MAX следующим образом : #DEFINE MAX(A, B) ((A) > (B) ? (A) : (B )) когда строка X = MAX(P+Q, R+S); будет заменена строкой X = ((P+Q) > (R+S) ? (P+Q) : (R+S)); Такая возможность обеспечивает “функцию максимума” , которая расширяется в последовательный код , а не в обращение к функции . При правильном обращении с аргументам и такой макрос будет работать с любыми типами данных ; здесь нет необходимости в различных видах MAX для данных разных типов , как это было бы с функциями. Конечно , если вы тщательно рассмотрите приведенное выше расширение MAX, вы заметите определенные н едостатки . Выражения вычисляются дважды ; это плохо , если они влекут за собой побочные эффекты , вызванные , например , обращениями к функциям или использованием операций увеличения . Нужно позаботиться о правильном использовании круглых скобок , чтобы гарантир о вать сохранение требуемого порядка вычислений . (Рассмотрите макрос #DEFINE SQUARE(X) X * X при обращении к ней , как SQUARE(Z+1)). Здесь возникают даже некоторые чисто лексические проблемы : между именем макро и левой круглой скобкой , открывающей список ее аргументов , не должно быть никаких пробелов. Тем не менее аппарат макросов является весьма ценным. Один практический пример дает описываемая в главе 7 стандартная библиотека ввода-вывода , в которой GETCHAR и PUTCHAR определены как макросы (очевидно PUTCHAR должна иметь аргумент ), что позволяет избежать затрат на обращение к функции при обработке каждого символа. Другие возможности макропроцессора описаны в приложении А. Упражнение 4-9. Определите макрос SWAP(X, Y), который обменивает значениями два своих ар гумента типа INT. (В этом случае поможет блочная структура ). 98 5.Указатели и массивы Указатель - это переменная , содержащая адрес другой переменной . указатели очень широко используются в языке “ C”. Это происходит отчасти потому , что иногда они дают единс твенную возможность выразить нужное действие , а отчасти потому , что они обычно ведут к более компактным и эффективным программам , чем те , которые могут быть получены другими способами. Указатели обычно смешивают в одну кучу с операторами GOTO, характеризуя их как чудесный способ написания программ , которые невозможно понять . Это безусловно спр Aведливо , если указатели используются беззаботно ; очень просто ввести указатели , которые указывают на что-то совершенно неожиданное . Однако , при определенной дисципли н е , использование указателей помогает достичь ясности и простоты . Именно этот аспект мы попытаемся здесь проиллюстрировать. 5.1. Указатели и адреса Так как указатель содержит адрес объекта , это дает возможность “косвенного” доступа к этому объекту через у казатель . Предположим , что х - переменная , например , типа INT, а рх - указатель , созданный неким еще не указанным способом. Унарная операция & выдает адрес объекта , так что оператор рх = &
1Архитектура и строительство
2Астрономия, авиация, космонавтика
 
3Безопасность жизнедеятельности
4Биология
 
5Военная кафедра, гражданская оборона
 
6География, экономическая география
7Геология и геодезия
8Государственное регулирование и налоги
 
9Естествознание
 
10Журналистика
 
11Законодательство и право
12Адвокатура
13Административное право
14Арбитражное процессуальное право
15Банковское право
16Государство и право
17Гражданское право и процесс
18Жилищное право
19Законодательство зарубежных стран
20Земельное право
21Конституционное право
22Конституционное право зарубежных стран
23Международное право
24Муниципальное право
25Налоговое право
26Римское право
27Семейное право
28Таможенное право
29Трудовое право
30Уголовное право и процесс
31Финансовое право
32Хозяйственное право
33Экологическое право
34Юриспруденция
 
35Иностранные языки
36Информатика, информационные технологии
37Базы данных
38Компьютерные сети
39Программирование
40Искусство и культура
41Краеведение
42Культурология
43Музыка
44История
45Биографии
46Историческая личность
47Литература
 
48Маркетинг и реклама
49Математика
50Медицина и здоровье
51Менеджмент
52Антикризисное управление
53Делопроизводство и документооборот
54Логистика
 
55Педагогика
56Политология
57Правоохранительные органы
58Криминалистика и криминология
59Прочее
60Психология
61Юридическая психология
 
62Радиоэлектроника
63Религия
 
64Сельское хозяйство и землепользование
65Социология
66Страхование
 
67Технологии
68Материаловедение
69Машиностроение
70Металлургия
71Транспорт
72Туризм
 
73Физика
74Физкультура и спорт
75Философия
 
76Химия
 
77Экология, охрана природы
78Экономика и финансы
79Анализ хозяйственной деятельности
80Банковское дело и кредитование
81Биржевое дело
82Бухгалтерский учет и аудит
83История экономических учений
84Международные отношения
85Предпринимательство, бизнес, микроэкономика
86Финансы
87Ценные бумаги и фондовый рынок
88Экономика предприятия
89Экономико-математическое моделирование
90Экономическая теория

 Анекдоты - это почти как рефераты, только короткие и смешные Следующий
- Ты заметил, что я с тобой второй день не разговариваю?
- Тише ты, я, может, продлевать буду.
Anekdot.ru

Узнайте стоимость курсовой, диплома, реферата на заказ.

Обратите внимание, диплом по программированию "Язык С", также как и все другие рефераты, курсовые, дипломные и другие работы вы можете скачать бесплатно.

Смотрите также:


Банк рефератов - РефератБанк.ру
© РефератБанк, 2002 - 2016
Рейтинг@Mail.ru