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

Б.8. К<'рниган, Д.М.

Ричи

Язык программирования С
2-е и з,цани (', и слравл(' нно е

Керни га н Б.В.
Ричи Д.М.

НационаJlЬНЫЙ OffiРЫ1ЫЙ Университет" ИНТУИТ"

20 16

2
Б.8. К<'рниган, Д. М. Ричи ЯЗЫК "JIOграм""ирования С

Язык nрограммирования CJ Б . В. Кf'рниган, Д.М . Ри ч и - М . : Национальный D1xРЫ1ЪJЙ


Университет" ИНТУИТ", 2016
Классический у чебник по языку С.

(с) 000 " И НТУ ИТ.РУ", 2tXJ6.-2016


(с) Кf'рниган Б.В . , Ричи Д.М., 2006-2016

з
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Аннотация и введение

Введение в язык С. Краткое содержание вопросов, рассматриваемых в


лекциях.

Аннотация

Язык 'С"(произнос и тся "си') это универсальный язык


программирования, для которого характерны экономичность

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


богатый набор операторов. Я з ык 'С" не является ни яз ыком "очень
высокого уровня", ни ''БОЛЫl1ИМ'' языком, И не предназначается для
некоторой специальной области применения. Но отсyrствие
ограничений и общность языка делают его более удобным и
э фрективным для многих задач, чем ЯЗЫЮ1, предположительно более
мощные.

Язык "С", первоначально предназначавlШ1ЙСЯ для написания


операционной системы 'UNIX" на ЭВМ DEC PDP-ll, был разработан и
реализо ван на этой системе Деннисом Ричи. Операционная система,
компилятор с яз ыка ' С " и по C)ll..ЦeCТBY все прикладные программы
системы 'UN IX" (включая все программное обеспечен и е,
использованное при подготовке этой книги) написаны на "С".
Коммерческие компиляторы с языка 'С" С.YJ.Lf'ствуют также на некоторых
други х ЭВМ, включая lВM SYSTEМl370, HONEYWELL 6000,
INТERDAТA 8132. Я з ык 'С", однако, не связан с каЮ1ми -л и бо
определенным и аппаратными средствами или системами, и на нем

легко писать программы , которые можно пропускать без изменений на


любо й ЭВМ, имеющей ' С "- компилятор.

Эта книга предназначена для того, чтобы помочь читателю научиться


программировать на языке 'С". Она содержит учебное введение, цель
которого - позволить новым пользователям начать программировать как

можно быстрее, отдельные ле кции по всем основным особенностям


языка и справочное руководство. Обучение построено в основном на
чтении , написании и разборе примеров , а не голой фоРМ)m.и ровке
прави л. При меры, привqдимые в книге , по БОЛЫllей части являются
законченными реальными программами, а не отдельными cWагментами.

,
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

Все примеры были проверены непосредственно с текста книrn, где они


напечатаны в виде, приroдном для ввода в маll.lИНу. Кроме указаний о
том, как сделать использование языка более эфtx:>ктивным, мы также
пытались, где это возможно , проиллюстрировать полезные алгоритмы

и принципы )[)рошего стиля и разум ной раз работки.

Настоящзя книга не является вводным курсом в программирование; она


предполагает определенное знакомство с основными понятиями

программирования такими как переменные, операторы присваивания,

циклы, фующии. Тем не менее и новичок в программировании должен


оказаться в состоянии читать подряд и освоиться с языком, )[)тя при

этом была бы полезной помощь более опытного коллеги.

По Hal.1..leMY опыту, "С" показал себя приятным, выразительным и


разносторонним языком на Il.IИроком множестве разно образных
программ. Его ле[1(l) выучить, и он не теряет своих качеств с ростом
опыта программиста. Мы надеемся , что эта книга поможет вам .хороuю
его использов ать.

Вдумчивая критика и предложения многих наll.lИХ друзей и коллег очень


много добави л и как для самой книrn, так и для Hal.1..lerO у,цо вол ьствия
при ее написании. В частности, Майк Биапси , Джим Блю, Стью
Фел ьдман , Доут Мак- Илрой , Билл Рум, Боб Розин и Ларри Росл ер
тщзтельно прочитал и множество вариантов. Мы также обязаны Элю
Ахо, Стиву Борн у, Дэву Двораку, Чаку Хэлею, Дебби Хэлей, Мариону
Харрису, Рику Холту, Стиву Джонсону, Джону Машею, Бобу Митцу,
Ральфу Мьюа, Питеру Нельсон у, Эллиоту Пинсону, Биллу Плагеру,
Джерри С пиваку, Кену Томпсону и Питеру Вейнбергеру за полезные
замечания на различных этапах и Майку Лоску и Джо Осанна за
неоценимую помощь при печатании книrn.

Брайен В. Керниган

Деннис М. Ричи

0.1 . Введение

Язык 'С" яв ляется универсальным языком программирования. Он тесно


связан с операционной системой 'UNIX" , так как был раз вит на этой

5
Б.8. К <'рниган, Д. М. Р ич и Я з ык "JIOграм""ирования С

системе и так как 'UN IX" и ее программное обес печение написано на


'С". Сам язык, однако, не связан с какой-либо одной операционной
системой или маll.В1НОЙ; и хотя его называют языком системного
программирования, так как он у,цобен для написания операционных
систем, он с равным успехом использовался при написании болыl.в1x
вычисл ительных программ, программ для обработки текстов и баз
данных.

Язык "С " это язык относительно ''н изкого уровня". В такой
характеристике нет ничего оскорбительного; это просто означает, что
'С" и меет дело с объектами того же вида, что и болыl.в1ствоo ЭВМ, а
именно, с символами, числами и адресами. Они могут объединяться и
пересылаться посредством обычных арифvtетических и ЛОЛ1ческих
операций, осyrцeствляемых реальными Э ВМ.

в языке "С" отсутствуют операции , имеющие дело непосредственно с


составными объектами, такими как строки символов, м ножества ,
списки или с массивами, рассматриваемыми как целое. Здесь,
например , нет никакого аналога операциям PUl, оперирующим с
целыми массивами и строками. Язык не предостав ляет никаких других
во зм ожностей распределения памяти , кроме статического определения
и механизма стеков, обеспечиваемого локальными переменны м и
функций ; здесь нет ни ''куч''(НЕАР), ни "сборки мусора", как это
преАУсмат ривается в АЛГОЛЕ-б8. Наконец , сам по себе 'С" не
обеспечивает никаких возможностей ввода-вывода: здесь нет
операторов read и ли wr i te и никаких встроенных методов до ступа к
фай лам. Все эт и механизмы высокого уровня должны обеспечиваться
явно вы зываемыми функциями.

АналоП1ЧНО, язык 'С" предлагает только простые, по следовательные


конструкции потоков управления: проверки, циклы, группирование и

подпрограммы, но не мультипрограммирование, параллельные

операции, си нхронизацию или сопрограммы.

Хотя отсутствие некоторых из эт их с редств может выглядеть как


удручаю щая непол ноценность (''выходит, что я должен обращаться к
функции , чтобы с равнить две строки символов?! ' ), но у,цержание языка
в скромных размерах дает реальные преимущества. Так как 'С"
относительно мал, он не требует много места для своего описания и

6
Б.8. К <'рниган, Д. М. Р ич и Я з ык "JIOграм""ирования С

может быть быстро выучен. Компилятор с 'С" может быть простым и


компактным. Кроме того, компиляторы легко пишугся; при
использовании современной технолоЛ1И можно ожи дать написания
компилятора для новой ЭВМ за пару месяцев и при этом окажется, что
80 процентов программы нового компилятора будет общей с
программой для уже существующих компиляторов. Это обеспечивает
высокую степень мобильности языка. Поскольку типы данных и
структуры управления, имеющиеся в 'С", непосредственно
пqцдерживаются болы.LIИН СТВОМ существующих ЭВМ, библиотека,
необходи мая во время прогона изол ированных программ, оказывается
очень маленькой. На PDP -11, например , она содержит только
программы для 32-битового умножения и делен ия и для выпол нения
программ ввода и вывода посл едовательностей. Конечно, каждая
реализация обеспеч и вает исчерпывающую, совмести мую б и блиотеку
функций для выполнен ия операций ввода-вывода, обработки строк и
распределе ния памяти , но так как обращение к ним осуществляется
только явно, можно, если необходимо , и збежать их вызова; эти функц ии
мосут быть компактно написаны на самом 'С".

Опять же из-за того, что язык 'С" отражает возможности современных


компьютеров, программ ы на 'С" оказываются достаточно
э фрективными, так что не возникает побуждения писать вместо этого
программы на языке ассемблера. Наиболее убедительным примером
этого является сама операционная система 'UN IX", которая по чти
полно стью напи сана на ' С ". Из 13000 строк программы системы только
около 800 строк самого низкого уровня н а пис а ны на ассембле ре. Кроме
того, по существу все прикладное программное обеспечение системы"
UN IX " написано на 'С"; подаВ ЛЯЮ l.Lf'е болы.LIИН СТВО пользователей
системы ' UN IХ"(включ ая одного из авторов этой книги) даже не знает
языка ассемблера PDP-11 .

Хотя ' С " соответствует возможностям мноЛ1Х Э ВМ, он не зав исит от


какой-либо конкретной архитектуры маlШ1НЫ и в силу этого без особых
усилий позволяет писать ''п ереносимые'' программы , Т.е. программы,
которые можно проп ускать без изменений на разл ичных аппаратных
средствах. В наl.LlИХ кругах стал уже традицией перенос программнога
обеспечения, разработан ного на системе" UNIX ", на системы ЭВМ:
HONEYWELL, 18М и INТERDAТA. Фактически компиляторы с "С" и
программное обеспечен и е во время прогона программ на этих четырех

7
Б.8. К <'рниган, Д. М. Р ич и Я з ык "JIOграм""ирования С

системах, по-видимому, гораздо более совместимы, чем стандартные


версии фортрана американского национального института стандартов (
ANSI ). Сама операционная система 'UNIX" теперь работает как на
РDР-ll, так и на INТERDAТA 8/32. За исключением программ, которые
неизбежно оказываются в некоторой степени маlШ1нно-зависимыми,
таких как компилятор, ассемблер и отладчик Написанное на языке 'С"
программное обеспечение идентично на обеих маlШ1нах. Внутри самой
операционной системы 7000 строк программы, исключая
математическое обеспечение языка ассемблера ЭВМ и управления
операциями ввода-вывода, совпадают на 95 процентов.

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


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

нескольких исторических, технических и философ::ких аспектов ' С".

Мноrnе из наиболее важных идей 'С" происходят от гораздо более


старого, но в се еще впол не жизненного языка BCPL , разработанного
Мартином Ричардсом. Косвенно язык BCPL оказал влияние на ' С " через
язык ''В '', написанный Кеном Томпсонам в 1970 пщу для первой
операционной системы 'UN IX" на ЭВМ РDР-7.

Хотя язы к 'С" имеет нескол ько общих с BCPL характерных


особенностей, он никоим образом не является диалектом по сл еднего. И
BCPL и 'Б" - "безтипные" языки; единственным видом дан ных для них
являются маlШ1нное слово, а доступ к другим объектам реализуется
специальными операторами или обращением к функциям. В языке 'С"
объектами основных типов данных являются символы, цел ые числа
нескольки х размеров и числа с плаваю l.Lf'Й точкой. Кром е того, имеется
иерархия произв qд,ных типов данных, создаваемых указателями,

массивами, структурами, объединениями и функциями.

Я зык 'С" включает основные конструкции потока управления,


требуемые для хороll.Ю структурированных программ: группирование
операторов, принятие решений ( if ), циклы с проверкой завершения в
н ачале ( while , for ) или в конце( do ) и выбор одного из множества
во зм ожных вариантов ( swi tch ). (Все эти возможности
обеспечивались и в BCPL, )[)тя и при несколько отличном синтаксисе;
этот язык предчувствов ал наступившую через несколько лет моду на

структурное программирование).

8
Б.8. К <'рниган, Д. М. Р ич и Я з ык "JIOграм""ирования С

в языке 'С" имеются указатели и возможность адресной арифvtетики.


Аргументы передаются функциям посредством копирования значения
аргумента, и вызванная функция не может изменить фзктический
аргумент в вы з ываюl.Цf'Й программе. Если желательно добиться" вызова
по ссылке ", можно неявно передать указатель, и функция сможет
изменить объект, на который этот указатель указывает. Имена массивов
передаются указанием начала массивов , так что аргументы типа

массивов э Ф1х"ктивно вызываются по ссылке.

к любо й Функции можно обращаться рекурсивно , и ее л окальные


переменные обычно "автоматические" , Т.е. создаются заново при
каждом обращении. Описание одной функции не может содержаться
внутри д ругой, но переменные могут описываться в соответствии с
обычной блочной структурой. функции в 'С" - программе могут
транслироваться отдельно. Переменные по отношению к функции могут
быть внутренними, внешними, но известными только в пределах
одного исходного фзй ла, или пол ностью глобальными. Внутренние
переменные могут быть автоматическими или статическими.
Автоматические переменные для БОЛЫl.lеЙ эфJx:'кт ивности можно
помещать в реЛ1СТРЫ , но объявление регистра яв ляется только
указанием для компилятора и никак не связано с конкретными

маl.LIИННЫМИ реЛ1страм и.

Язык 'С" не является языком со строгими типами в смысле паскаля и л и


алгола 68. Он сравнительно снисходителен к преобразованию данных,
хотя и не будет автоматически преобразов ывать типы данных с буйной
непринужденностью языка PUl. С уществующие компиляторы не
предусмат ривают никакой проверки во время выполнения программы
индексов массивов, типов аргументов и Т.д.

В тех ситуациях, когда желательна строгая проверка типов, используется


специальная версия компилятора. Эта программ а называется 1i nt
очевидно потому. она выбирает кусочки пуха из вашей программы.
Программа 1i nt не генерирует маlШ1ННОГО кода, а делает очень строгую
проверку всех тех сто рон программы, которые можно

проконтроли ровать во время компиляции и загрузки. Она определяет


несоответствие типов, несовместимость а ргументов , неиспользованные

или о чев и дным образом неинициал и з ированные переменные,


потенциальные тру,цности переносимости и Т.д. Для программ , которые

9
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

благополучно проходят через lint , гарантируется отсугствие ошибок


типа примерно с той же полнотой, как и для программ, написанных,
например, на АЛГОЛЕ-б8. Другие возможности программы lint будуг
отмечены, когда представится соответствующий случай.

Наконец , язык "С", подобно любому друтому языку, имеет свои


недостатки. Некоторые операции имеют неу,цачное старшинство;
некоторые разделы синтаксиса могли бы быть лучше; существует
несколько версий языка, отличающи.xrя небольшими деталям и. Тем не
менее язык "С" зарекомендовал себя как исключительно эt1xJx:'ктивный и
выразительный язык ДJlЯ uшрокого раз нообразия применений
программирования.

Соде ржание книги организовано следующим образом. Лекция No 1:


является учебным введением в центральную часть языка "С". Цель -
позволить читателю стартовать так быстро, как только возможно, так
как мы твердо убеждены, что единственный способ изучить новый язык
- писать на нем программы. При ЭТОМ, однако, предполагается рабочее
владение основными элементами программирования ; здесь не

объясняется, что такое ЭВМ или компилятор, не поясняется смысл


выражений типа n=n+l. Хотя мы и пытались, где это возможно,
продемонстрировать полезную технику программирования. Эта книга
не предназначается быть справочным руководством по структурам
данных и алгоритмам; там, где мы вынуждены были сделать выбор, мы
концентрировались на языке.

в лекциях NQ 2, NQ 3, NQ 4, NQ 5 и NQ б различные аспекты "С" излагаются


более детально и несколько более фJрмально, чем в лекции No 1, хотя
уда рение по-прежнему делается на разборе примеров законченных,
полезн ых программ , а не на отдельных cWarMeHTax.

в лекции No 2 обсуждаются основные типы данных, операторы и


выражения. В лекции No 3 рассматриваются управляющие операторы:
if - e l se , while, for и Т.д. Лекция NQ 4 охватывает функции и
структуру программы - внеllПiИе переменные, правила определе ных

областей действия описания и Т.Д. В лекции NQ 5 обсуждаются указатели


и адресная арифvtетика. Лекция No б содержит подробное описание
структур и объединений.
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

В лещии No 7 описывается стандартная библиотека ввода-вывода языка


'С", которая обеспечивает стандартный интерф:>йс с операционной
системой. Эта библиотека ввода-вывода пqццерживается на всех
маJ.LП.1Нах, на которых реализован "С", так что программы, использующие
ее для ввода, вывода и APyrnx системных функций, MOryr переноситься
с одной системы на другую по существу без изменений.

в лещи и N2 8 описывается интерф:>йс между 'С" - программами и


операционной системой 'UNIX". Упор делается на ввод-вывод, систему
файлов и переносимость. Хотя некоторые части этой лекции
специ<l:ичны для операционной системы 'UNIX", программисты, не
использующие 'UN lX", все же должны найти здесь полезный материал,
в том числе некоторое представление о том, как реализована одна

версия стандартной библиотеки и предложения для достижения


переносимости программы.

Приложение А: содержит справочное руководств о по языку "С". Оно


является "оф1циальным" и зл ожением синтаксиса и семантики "С " и
(исключая чей-либо собственный компилятор ) ОЮJнчательным
арбитром для всех двусмысленностей и упущений в предьщущих
лекциях.

Так как 'С" является разв ивающимся ЯЗЫЮJм, реализованным на


множестве систем, часть материала настоящей книги может не
соответствовать текущему состоянию разработки на каЮJЙ-ТО
ЮJнкретной системе. Мы старались избегать таких проблем и
предостерегать о возможных тру,цностях. В сомнительных случаях,
однако, мы обычно предпочитали описывать ситуацию для системы
'UNIX" PDP-ll , так как она является средой для БОЛЬJ.LП.1нства
программирующих на языке "С". В приложении А также описаны
расхождения в реализациях языка 'С" на основных системах.
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Учебное введение

в этой часги описываются основные элементы языка С , сосгавл яющие


ядро языка.

1. Учебное введение

Давайте начнем с бысгрого введения в язык "С". Наша цел ь -


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

программах, не увязая при этом в деталях, qюрмальных правилах и


исключениях. В этой л екции мы не пытаемся изложить язык полносгью
или )[Этя бы сгрого (разумеется , приводимые примеры будуг
корректными). Мы )[Этим как можно скорее довесги вас до такого
уровня , на котором вы бы л и бы в сосгоянии писать полезные
программы, и чтобы добиться этого, мы сосредотачиваемся на
основном: переменных и консгантах. ариф\1етике , операторах передачи
ynравления , функциях и элементарных сведениях о вводе и выводе. Мы
совершенно намеренно осгавляем за пределами этой лекции мноrnе
элементы яз ыка 'С", которые имеют первосгепенное значение при
написании больших программ, в том числе указатели , сгруктуры,
большую часгь из богатого набора операторов языка "С", несколько
операторов передачи ynравления и несметное количесгво деталей.

Такой подщц имеет, конечно, свои недосгатки. Самым c.YJ..Цf'CГBeHHЫM


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

привесги к неправи л ьному ИСГОЛКDванию. Кроме того, и з-за


нево з можносги использовать всю мощь языка, примеры оказываются

не сголь краткими и эл егантными, как они могл и бы быть. И )[Этя мы


сгарались свести эти недосгатки к минимуму, все же имейте их ввиду.

Другой недосгаток сосгоит в том, что посл едующие лекции будуг


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

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


сосгоянии про з ксграполировать материал данной л екции на свои
собсгвенные программисгские нужды. Начинающие же должны в
дополнение писать аналогичные маленькие самостоятел ьные
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

программы. И те, и другие могут использовать э ту лекцию как каркас, на


который будуг навеl.шшаться более подробные описания, начинающиеся
с л екции NQ 2.

1.1. Начинаем

gцинственный способ освоить новый язык программирования - писать


на нем программы. Первая программа, которая должна быть написана , -
одна для всех языков: напечатать сл ова: HELLO, WORLD.

Это - самый С)'l.ЦE'ственныЙ барьер; чтобы преодолеть его , вы должны


суметь завести где-то текст программы, успеll.lНО его скомпилировать,

загрузить, прогнать и найти, где оказалась ваша выдача. Если вы


научились справляться с э тими техническими деталями , все остальное

сравнительно просто.

Программа печати "HELLO, WORLD" на языке 'С" имеет вид:

#include <s(dio.h>
int main О
{
print~'hello , worklln');
гeturn О ;
}

Как запустить эту программу - зависит от используемой вами системы .


В частности, на операционной системе 'UNIX" вы должны завести
исходную программу в файле, имя которого оканчивается на . с ",
например, h e 11 о . С , И затем скомпилировать ее по команде

се hello.c

Если вы не допустили какой-либо небрежности , такой как пропуск


символа или неправильное написание, компи л яция пройдет без
сообщений и будет создан исполняемый файл с именем а . ои t . Прогон
его по команде

а . ОШ
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

приведет к выво.цу

hello, world

На друтих системах эти прави л а БWJ.YТ иными; проконсультирyi1тесь с


местным авторитетом.

Упражнение 1-1

Пропустите эту программу на вашей системе . Попробуйте не включать


различные части программы и посмотрите какие сообщения об Оll.D1бках
вы при этом получите .

Теперь некоторые пояснения к самой программе. Любая 'С"- программа,


каЮJВ бы ни был ее размер, состоит из одной или более " функций ",
указ ывающих фактические операции компьютера, ЮJторые должны быть
выполнены. функции в языке 'С" подобны функциям и подпрограммам
ф:Jртрана и процедурам РИ1, паскаля и Т.д. В HaJJ.JeM примере такой
функцией яв л яется main . Обычно вы можете давать функциям любые
имена по BaJJ.JeMY усмотрению, но та i n - это особое имя; выпол нение
вашей программы начинается сначала с функции main . Это означает,
что каждая программа должна в каком-то месте содержать функцию с
именем main . Для выпол нения определенных действий функция main
обычно обраl.Lflется к друтим функциям , часть из которых находится в
той же самой программе, а часть - в библиотеках, содержащих ранее
написанные функции.

Одним способом обмена данными между функциями является передача


посредством аргументов. Крутлые скобки , слеАУЮщие за именем
функции, заключают в себе список аргументов; здесь main - функция
без аргументов, что указывается как () . операторы , составл яющие
функцию , заключаются в фигурные скобки { и }, ЮJторые аналогичны
do - end в РИ1 или begin - end в алгол е, паскале и Т.д. Обращение к
функции осуществляется указанием ее имени , за ЮJторым следует
заключенный вкрутлые СЮJбки список аргументов. Здесь нет никаких
операторов cal l , как в ф:Jртране или РИ1. Круглые скобки должны
присyrствовать и в том случае, когда функция не имеет аргументов.
С трока
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

pгint~'hello , woгkl\n');

яв л яется обраl.lf'нием к функции, ЮJТорое вызывает функцию с именем


printf и аргументом " hel l o , world\n ". функция printf
является библиотечной функцией, которая выдает выходные данные на
терминал (если только не указано какое-то друтое место назначения). В
данном случае печатается строка символов, являющзяся аргументом

функции.

Последовател ьность из любого количества символов, заключенных в


удвоенные кавычки "... ", называется 'символьной строкой' или 'строчной
константой'. Пока мы будем использовать символьные строки только в
качестве аргументов для pr in tf и других функций.

Последовател ьность \n в приведенной строке является обозначением н<


языке "С" для 'символа новой строки', который служит указанием для
перехода на терминале к левому краю следующей строки. Если вы не
включите \n (полезный экспе римент), то обнаружите, что ваша выдача
не зако нчится переxqд,ом терминала на новую строку. Использование
посл едовательности \n - единственный способ введения символа
новой строки в аргумент функции printf ; если вы попробуете что­
нибудь вроде

pгintf{"hello, woгld
');

то 'С"- компилятор будет печатать злорадные диагностические


сообщения о недостающих кавычках

Функция printf не обеспечивает автоматического перехода на новую


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

mainО
{
pгintf('hello , ');
pгintf( "wo гld');
pгintf('\n') ;
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Подчеркнем, что \n представляет тол ько один символ . . Условные


посл едовательности " подобные \n , дают общий и допускающий
раСLШ1рение механизм для представления тру,цных для печати или

невидимых символов. С реди прочих символ ов в языке 'С"


предусмотрены слеАУЮщие:

• \ t - для табуnяции ,
• \Ь - для возврата на одн у позицию,
• \ " - для двойной кавычки,
• \ \ - для самой обратной косой черты.

Упражнение 1-2

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


строке, ЯВЛЯЮl.Цf'йся аргументом функции pr i n t f бу,цет содержаться \х
где Х - некоторый символ, не входящий в ВЫl..L.IeприведенныЙ список

1.2. Переменные и арифметика

С леАУЮщая программа печатает приведенную ниже таблицу температур


по Фаренгейту и их эквивалентов по стоградусной J..LIКaЛе Цельсия,
используя для перевода ф:Jрмулу

с = (5/9)*(f-32).
О - 17.8
20 -6.7
40 4.4
60 15.6

260 126. 7
280 137.8
300 148.9

Теперь сама программа:

/* ргint fahгenhe it- celsius {а Ые


Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

for f = О , 20, "" 300 */


#include <stdlo,h>
iлt maiлО
{
int lower, upper, step;
float fahr, celsius;
lower = О ; /* lower limit of temperature
<а ы е *!
upper =300; /* upper limit */
step = 20; /* step size */
fahr = lower;
while (fahr <= upper) {
celsius = (5.0/9.0) * (fahr -32.0);
рriлt~' %4 . 0 f%6. 1 1\n", fahr, celsius);
fa hr = fahr + step;
}
Ге(ШП О;
}

Первые две строки

/* ргint fahrenheit- celsius table


for f = О, 20, .. ,,300 */

яв л яются комментарием, который в данном случае кратко поясняет, что


делает программа . Любые символы между /* и */ игнорируются
компилятором ; можно свободно пользоваться комментариями для
обле гчения пони мания программы. Комментарии MOryr появляться в
любом месте , где возможен пробел и л и пере.юд на новую строку.

в яз ыке "С" все переменные должны быть описаны до их


использования, обычно это делается в начале ФУНIO.\ИИ дО первого
выполняемого оператора, Если вы забудете вставить описание, то
получите диагностическое сообщение от компилятора, описание
состоит из типа и списка переменных. имеющих этот тип, как в

int lower, upper, step;


float fahr, celsius;

тип int означает, что все переменные списка целые ; тип float
"
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

предназначен для чисел с плавающей точкой, Т.е. для чисел, которые


мосут иметь дробную часть. Точность как int , так и float зависит от
конкретной машины, на которой вы работаете. На PDP-ll, например,
тип i nt соответствует 16-битовому числу со знаком , Т.е. числ у,
лежащему между -32768 и +32767. Число типа f lоа t - это 32-битовое
числ о , имеющее около семи значащих циcW и л ежащее в диапазоне от
10е-38 до 10е + 38. В ле кц ии No 2 приводится список размеров для других
машин.

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


кроме in t и f l oat :

Таблица 1.1.
char символ - один байт

short короткое цел ое

l ong длинное цел ое

double плавающее с двойной точностью

Раз меры э тих объектов машинно-независимы; детали приведены в


ле кци и No 2. Имеются также массивы , структуры и объединения этих
основных типов , указател и на них и функции , которые их возвращают;
со всеми ними мы встретимся в свое время.

Фактически вычисл ения в программе перевода температур начинаются


с операторов присваивания

Iower = О;
upper =300;
step = 20;
fahr = Iower;

которые придают переменным их начальные значения. каждый


отдельный оператор заканчивается точкой с запятой.

Каждая строка таблицы вычисляется одинаковым обра зом , так что мы


используем цикл, повторяющийся один раз на строку. В этом
наз начение оператора while :

while (fahr <= up per) {


Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

проверяется усл овие вкрутлых скобках. Если оно истинно ( fa h r


меныие или равно upper ), то выполняется тело цикла (все операторы ,
заключенные в 4игурные скобки { и } ). Затем вновь проверяется это
условие и , если оно истинно, опять выполняется тел о цикла. Если же
условие не выполняется ( fahr превосXDДИТ upper ), цикл
заканчивается и происходит переход к выполнению оператора,

слеАУЮщего за оператором цикла. Так как в настоящей программе нет


никаких послеАУЮЩИХ операторов , то выполнение программы

завершается.

Тел о оператора while может состоять из одного или более операторов,


заключенных в 4игурные скобки, как в программе перевода температур,
или из одного оператора без скобок. как. например, в

while (i<j)
i = 2 '" ~

в обоих сл учаях операторы , управляемые оператором whi l e, сдвинyrы


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

подходящий сдви г и испол ьзование пробел ов з начительно облегчают


чтение программ. Мы рекомендуем писать только один оператор на
строке и (обычно) оставлять пробелы вокруг операторов. Расположение
4игурных скобок менее существенно ; мы выбрал и один из нескол ьких
популярных сти л ей. Выберите подходящий для вас стиль и затем
используйте его последовательно.

Основная часть работы выполняется в тел е цикла. Температура по


Цельсию вычисляется и присваивается переменной ce l sius
оператором

cel<;ius = (5.0/9.0) • (fa hr- З2.0) ;

причина испол ьзования выражения 5 . 0/9 . О вместо выглядящего


Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

проще 5/ 9 заключается в том , что в языке 'С", как и во многих других


языках, при делении целых происходит усечение, состоящее в

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


операции 5/9 равен нулю, и, конечно , в этом случае все температуры
оказались бы равными н ул ю. Десятичная точка в константе указывает,
что она имеет тип с плавающей точкой, так что, как мы и .хотели,
5 . 0/9 . О равно О . 5555 ....

Мы также писали 32 . О
32 , несмотря на то, что так как
вместо
переменная fahr имеет тип float , цел ое 32 автоматически бы
преобразовалось к типу float (в 32 . О ) перед вычитанием. С точки
зрения сти л я разумно писать плавающие константы с явной десятичной
точкой даже то гда, когда они имеют целые з начения; это подчеркивает
их плавающую приро.цу для просматривающеro программу и

обеспечивает то , что компилятор бу,цет смотреть на вещи так же , как и


Вы.

Подробные правила о том , в каком случае целые преобразуются к типу с


плавающей точкой, приведены в л екции NQ 2. Сейчас же отметим, что
присваивание

fahr = Iower;

проверка

while (fahr <= upper)

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


преобразуются в плавающую фJрму.

Этот же при мер сообщает чуть бол ьше о том , как работает printf .
функция printf фактически является универсальной функцией
фJрматных преобразований, которая бу,цет полностью описана в л екции
NQ 7. Ее первым аргументом является строка символов, которая должна
быть напечатана, причем каждый знак % указывает, ку,ца должен
подставляться каждый из остальных аргументов /второй, третий, .. ./ и в
какой фJрме он должен печататься. Например, в операторе

p rint~·%4 .0f%6. 1 tln··, fahr, cel<;iu<;);

20
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

С пециф1кация преобразования %4 . О f говорит, что число с плавающей


точкой должно быть напечатано в поле uмриной по крайней мере в
четыре символа без циcW после десятичной точки. специф1кация
%6 . 1 f описывает другое число, которое должно занимать по крайней
мере шесть позиций с одной циcWой после десят ичной точки,
аналогично спецификациям f6 . 1 В фJртране или f (6, 1) в РИl .
Различные части спецификации могуг быть опyrцeны: спецификация
%6 f говорит, что число бу,цет uмриной по крайней мере в шесть
символов; cnециф1кация %. 2 f требует двух позиций после десятичной
точки, но uмрина при этом не ограничивается; спецификация %f
говорит ТОЛЬЮJ О том, что нужно напечатать число с плавающей ТОЧЮJй.
функция р r i n t f также распознает следующие cnециф1кации:

• %d - для десятичного целого ,


• %0 - для восьмеричного числа,
• % х - для шестнадцатеричного ,
• %с - для символа,
• %s - для символьной строки
• %%- для самого символа %.

Каждая ЮJнструкция с символом % в первом аргументе функции


printf сочетается с соответствующим вторым, третьим, и Т.д.
аргументами ; они должны согласовываться по числу и типу ; в

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

Между прочим , функция pr in t f не яв ляется частью языка "С"; в самом


языке 'С" не определены операции. Нет ничего таинственного и в
функции printf ; это - просто пол езная функция, являющзяся частью
стандартной библиотеки подпрограмм, ЮJторая обычно доступна "С"­
программам. Чтобы сосредоточиться на самом языке, мы не бу,цем
подробно останавливаться на операциях ввода-вывода до л екции No 7.
В частности , мы до тех пор отложим фJрматный ввод. Если вам надо
ввести числа - прочитайте описание функции scanf в л екции NQ 7,
раздел 7.4. функция scanf во многом сходна с printf , но она
считывает в)[)Дные данные, а не печатает вы)[)Дные.

Упражнение 1-3
Б.8. К<'рниган, Д. М . Ричи Я ЗЫ К "JIOграм""ирования С

Преобразyi1те программу перевода температур таким образом, чтобы


она печатала заголовок к табл ице.

Упражнение 1-4

НаПИlШ1те программы печати соответствующей таблицы переxqд,а от


градусов Цел ьсия к градусам по JJ.IКaЛе Фаренгейта.

1.3 . Оператор FOR

Как и можно было ожидать, имеется множество различных способов


написания каждой программы. Давайте рассмотрим такой вариант
программы перевода температур:

int mainО /* fahrenheit-ce1sius table */


{
int fahr;
for (fahг = О; (ahг <= 300; (ahг = (ahг + 20)
p rint~'o/04 d %6. 1 f\л· ·, (ahг, ( 5. 0/9.0 ) *(fahг-32.0));
return О ;
}

Эта программа выдает те же самые резул ьтаты, но выглядит безусловно


по-друтому. Главное изменение - исключение Болыш1стваa переменных
; осталась тол ько переменная fa hr, причем типа int (это сделано для
того , чтобы продемонстрировать преобразование %d в функции
pr intf ). Нижняя и верхняя границы и размер шага появляются только
как константы в операторе f о r , который сам является новой
конструкцией, а выражение, вычисляющее температуру по цельсию,
входит теперь в виде третьего аргумента функции р r i nt f, а не в виде
отдельного оператора присваивания.

Последнее изменение является примером вполне общего правила языка


'С" - в любом контексте, в котором доп ускается использование значения
переменной некоторого типа, вы можете испол ьзовать выражение этого
типа. Так как третий аргумент фующии pr i n t f должен иметь значение
с плавающей точкой, чтобы соответствовать спецификации %6 . 1 f , то в
этом месте может встретиться любое выражение плавающего типа.

22
Б.8. К<'рниган, Д. М . Ричи Я ЗЫ К "JIOграм""ирования С

С ам оператор for - это оператор цикла, обобщающий оператор while .


Его функционирование должно стать ясным , если вы сравните его с
ранее описанным оператором while . оператор for содержит три чаСТ I
разделяемые точкой с запятой. Первая часть

fahr = о

выполняется один раз перед входом в сам цикл. Вторая часть -


проверка, или условие , которое управляет циклом:

fahr <= 300

это условие проверяется и, есл и оно истинно , то выпол няется тело

цикла (в данном сл учае только функция printf ). 3атем выполняется


шаг реинициал изации

fahr =fahr + 20

и условие проверяется снова . Цикл завершается, когда это усл овие


становится ложным. Так же , как и в случае оператора whi le , тело
цикла может состоять из одного оператора или из группы операторов,

заключенных в ф1гурные скобки. Инициализирующая и


реинициализирующая части Moryr быть любыми отдел ьными
выражениями.

Выбор между операторами while и for произволен и основывается


на том , что выгл ядит яснее. Оператор for обычно удобен для циклов , в
которых инициализация и реинициализация логически связаны и

каждая задается одним оператором , так как в этом случае запись бол ее
компактна , чем при использовании оператора while , а операторы
управления циклом сосредотачиваются вместе в одном месте.

Упражнение 1-5

Модиф1цируйте программу перевода температур таким образом , чтобы


она печатала таблицу в обратном порядке , Т.е . От 300 градусов до О.

1.4. СИМВОIШческие константы


Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Последнее замечание, прежде чем мы навсегда оставим программу


перевода температур. Прятать ''маrnческие числа", такие как 300 и 20,
внутрь программы это неу,цачная практика ; они дают мало

ишlюрмации тем, кто, возможно , должен бу,цет разбираться в этой


программе позднее, и их трудно изменять систематическим образом. К
счастью в языке "С " предусмотрен способ, позволяющий избежать таких
''маrnческих чисел". Используя конструкцию #define, вы можете в
начале программы определить символическое имя или символическую

константу, которая будет конкретной строкой символов. Впосл едствии


компилятор заменит все не заключенные в кавычки появления этого

имени на соответствующую строку. Фактически это имя может быть


заменено абсолютно произвольным текстом, не обязательно ци<wами

#define LOWER 0/'" Iower limit of table "'/


#define UPPER 300 /* upper limit * /
#define SТEP 20 /'" step size "'/
irn main О /* Гahrenhert-cel<;iu<; table */
{
int fahr;
for (Гahr =LOWER; Гahr <= UPPER; Гahr = Гahr + SТEP)
print~' %4d %6. 1tln", Гahr, (5.0/9.0)*(Гahr-32));
return О;
}

вел ичины LOWER, UPPER и STEP являются константами и поэтому

они не указываются в описаниях. С имволические имена обычно пишуг


прописными буквами, чтобы и х было легко отличить от написанных
строчными буквами имен переменных. Отметим, что в конце
определения не ставится точка с запятой. Так как пqд,ставляется вся
строка, следующая за определеным именем, то это привело бы к
СЛИIl.lКDМ болыlЮМУ ЧИСЛУ точек с запятой в операторе f or .

1.5 . Набор полезных программ

Теперь мы собираемся рассмотреть семейство родственных программ,


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

"
Б.8. К<'рниган, Д. М. Ричи Язык "JIOграм""ирования С

здесь обсуждаем.

1.5. 1. Ввод и ВЫВОД символов

Станда ртная библиотека включает функции для чтения и записи по


одНОМУ СИМВОЛУ за один раз. функция getchar () извлекает
следующий вводимый символ каждый раз, как к ней обращзются, и
во зв ращзет этот символ в качестве своего значе ния. Это значит, что
после

с = gelchar()

переменная ' с ' содержит следующий символ из входных данных.


С имволы обычно поступают с терминала, но это не должно нас
касаться до лекции No 7.

функция putchar (с) яв ляется дополнением к getchar : в результате


обращения

pUlchar (с)

содержимое переменной ' с ' выдается на некоторый выходной


носитель, обычно опять на терминал. Обращение к функциям putchar
и printf MOryr перемежаться; выдача будет появ ляться в том порядке,
в котором происходят обращения.

Как и функция printf, функции getchar и putchar не содержат


ничего э кстраординарного. Они не входят в состав языка "С", но к ним
все гда можно обратиться.

1.5.2. Копирование файла

Имея в своем распоряжении только функции getchar и putchar вы


можете, не зная ничего более об операциях ввода-вывода, написать
удивительное коли чество полез ных программ. ПростеЙl1D1М примером
может служить программа посимвольного копирования вводного файла
в выводной. Общзя схема имеет вид:

25
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

вв ести с им в ол

while (си м в ол не яв ляется п р изнаком конц а фай л а )


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

программа, написанная на языке "С", выглядит следующим образом:

mainО /* со ру input to output; 1st version */


{
int с;

с = getcharO;
while (с != EOF) {
рше оог (с);
с = ge,ehar();
}

оператор отношения != означает ''не равно".

Основная пробл ема заключается в том, чтобы заф1ксировать конец


файла ввода. Обычно, когда функция getcha r наталкивается на конец
файла ввода, она во з вращает значение, не являющееся действительным
символом ; таким образом, программа может установить, что файл ввода
исчерпан. gцинственное осложнение, являющееся значительным
неудобством , заключается в существовании двух общеynотребительных
соглашений о том, какое з начение фактически является признаком конца
файла. Мы отсрочим решение этого вопроса , использовав
символическое имя EOF для э того значения , каким бы оно ни было. На
практике EOF бу,цет либо -1 , либо О , так что для правильной работы
перед программой должно стоять собственно либо

#dеfiлe EOF -)

либо

#dеfiлe EOF О

Использовав символ ическую константу EOF для представ л ения


значения, возвращаемого функцией g e tchar при выходе на конец
26
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

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


от конкретного численного значения.

Мы также описали переменную ' с ' как in t , а не char , с тем чтобы


она могла хранить значение, возвращаемое ge tcha r . Как мы увидим в
лекции No 2, эта величина действительно in t , так как она должна быть
в состоянии в дополнение ко всем возможным символам представлять

и EOF .

Программистом, имеющим опыт работы на 'С", программа копирования


была бы написана более сжато. В языке "С" л юбое присваивание , такое
как

е = getehar()

может быть использовано в выражении; его значение - просто


значение, присваиваемое левой части. Если при сваи вани е символа
переменной ' с ' поместить внутрь проверочной части оператора
whi le , то программа копирования фай ла запишется в виде:

mainО /* сору input to ourput; 2пd version */


{
int с;

while ((е = getehar()) != EOF)


putehar(e);
}

Программа извлекает символ, присваивает его переменной ' с ' и затем


проверяет, не является ли этот символ признаком конца фзЙла. Если нет
- выполняется тело оператора whi l e , выводящее этот с имвол. Затем
цикл while повторяется. Когда, наконец, бу,цет достигнуг конец фзйла
ввода , оператор whi l e завершается, а вместе с ним заканчивается
выпол нение и функции main .

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


обращение к фующии getchar - и ужимается программа. Вложение
присваивания в проверяемое услов ие - это одно из тех мест языка 'С",
которое приводит к з начительному сокращению программ. Однако, на

27
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

этом пути можно увлечься и начать писать недоступные для понимания

программы. Эту тенденцию мы бmем пытаться сдерживать.

Важно понять, что кругл ые скобки вокруг присваивания в условном


выражении действительно необходимы . CTapl..lМHCТBO операции !=
выше, чем операции присваивания =, а это означает, что в отсутствие

круглых скобок проверка усл овия! = бmет выпол нена до присваивания


=. Таким образом , оператор

с = getcharO != ЕОР

эквивале нтен оператору

с = (getchar() ! = ЕО Р)

Это, вопреки нашему желанию, приведет к тому, что 1 С 1 будет


принимать значение О или 1 в зависимости от того, натолкнется или
нет getc h ar на признак конца файла. Подробнее об этом будет сказано
в лекции No 2.

1.5.3. Подсчет символов

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


собой небольuюе развитие программы копирования.

mainО /* CO Lll1t characters in input */


(
Iong пс;

пс = О;
while (getchar() != ЕО Р)
++nc;
prinrf('%ld\n" , ос);

оператор

++ пс;

28
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

демонстрирует новую операцию, ++, которая означает увеличение на


единицу. Вы могли бы написать nc = nc + 1, но ++nc более кратко
и зачастую более эt1xJx:'ктивно. Имеется соответствуюJ..ЦЗЯ операция --
уменьшение на един иц у. Операции ++ и могут быть л ибо
префиксными (+ +nc) , либо постф.iксными (nc ++); эти две ф:эрмы,
как бу,цет показано в лекции No 2, имеют в выражен иях различные
значения, но как ++ пс, так и пс ++ увеличивают nc . Пока мы бу,цем
придерживаться преф.iксных операций.

Программа подсчета символов накапливает их количество в


переменной типа long, а не int . На РDР-ll макс и мальное значение int
равно 32767, и если описать счетч и к как int, то он бу,цет
переполняться даже при сравнительно малом файле ввода; на языке 'С"
для HONEYWELL и lВM типы l ong и int являются синон имами и
имеют знач и тельно БОЛЫllИ.Й размер. Спец иф.i кац ия преобразования
%l d указывает printf , что соответствующий аргумент является
целым типа l ong.

Чтобы справ и ться с еще БОЛЫlШМ И числами, вы можете использовать


тип double / f10at двойной ДЛ ин ы/. Мы также исп ользуем оператор
for вместо whi1e с тем, чтобы проиллюстрировать другой способ
записи ЦИКJJа.

mainО /* CO Lll1t characters in input */


{
double nc;

for (nc = О; ge,eharO != EOF; ++ пе)

prinrf('%.Ol\n", nc);

функция printf использует спец иф.i кацию %f как для f l oat, так и
для double ; спецификация %. О f подавляет печать несуществующей
дробной части.

Тело оператора ЦИКJJа for здесь пусто, так как вся работа выпол няется в
проверочной и реинициализационной частях. Но граммати ческие
правила языка "С" требуют, чтобы оператор f оr имел тело.
29
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Изолированная точка с запятой , соответствуюJ..Ц3Я пустому оператору,


появляется здесь, чтобы mов л етворить э тому требованию. Мы
выделили ее на отдельную строку, чтобы сделать ее более заметной.

Прежде чем мы распростимся с программой подсчета символов,


отметим, что если файл ввода не содержит никаких символов, то
условие вwhile или for не выполнится при самом первом
обращении к getchar, и , следовательно , программа выдаст нуль.
Одним из приятных свойств операторов whi l e и for яв л яется то , что
они проверяют условие в начале цикла , Т.е . До выполнения тела. Если
делать ничего не надо , то ничего не бу,цет сделано , даже есл и это
означает, что тело цикла нико гда не будет выполняться. программы
должны действовать разумно, когда они обращзются с файлами типа
''никаких символов". операторы while и for помогают обеспечить
правильное поведение про грамм при граничных значениях

проверяемых условий.

1.5.4. Подсчет строк

С ледующая программа подсчитывает количество строк в файле ввода.


Предполагается, что строки ввода заканчиваются символ ом новой
строки \n , скрупулез но добавленным к каждой выписанной строке.

mainО /* CO Lll1t lines in input */


{
int с , nJ;

nI = О;
while «с = getchar()) != EO F)
if (с =='In')
++nI;
printf('%dln", nI);

Тело while теперь содержит оператор if , который в свою очередь


управляет оператором увеличения ++nl . Оператор if проверяет
заключенное в крутлые скобки усл овие и, если оно истинно, выпол няет
следующий за ним оператор /или группу операторов, заключенных в

зо
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

4иrypные скобки /. Мы опять использовали сдвиг вправо, чтобы


показать, что чем управ л яет.

Удвоенный знак равенства == яв л яется обозначением в языке 'С" для


''равно'' /аналогично . eq. В фортране/. Этот символ введен для того,
чтобы отличать проверку на равенство от одиночного =, используемого
при присваивании. Поскольку в типичных 'С" - программах знак
присваивания встречается примерно в два раза чаще, чем проверка на

равенство, то естественно, чтобы знак оператора был впол овину


короче.

Любой отдельный символ может быть записан внутри одиночных


кавычек, и при этом ему соответствует значение, равное числ енному

значению этого символ а в Mal..lМHHOM наборе символ ов; это называется


символьной константой. Так. например, t а t - символ ьная константа;

ее значение в наборе символов ASClI /американский стандартный код


дл я обмена информацией / равно 65 , внутреннему представлению
символа а. Конечно , t а t предпочтительнее, чем 65 : его смысл

очевиден и он не зависит от конкретного ма1.LlИННОГО набора символов.

Условные последовательности, используемые в символьных строках,


также занимают законное место среди символьных констант. Так в
проверках и ариф\1етических выражениях' \п ' представляет значение
символа новой строки. Вы должны твердо уяснить, что '\n'
отдельный символ , который в выражениях э квивалентен одиночному
целому; с другой стороны "\ n" - это символ ьная строка , которая
содержит только один символ. Вопрос о сопоставлении строк и
символов обсуждается в л екции N22.

Упражнение 1-6

НаПИ1.LlИте программу для подсчета пробелов, табул яций и новых строк.

Упражнение 1-7

Напишите программу, которая копирует ввод на вывод, заменяя при


этом каждую последовательность из одного или бол ее пробел ов на один
пробел .
Б.8. К<'рниган, Д. М. Ричи Язык "JIOграм""ирования С

1.5.5. Подсчет слов

Четвертая программа из нашей серии полезных программ подсчитывает


количество строк, слов и символов, используя при этом весьма Il.IИрокое

определение, что словом является любая посл едовательность символов,


не содержащая пробелов, табуляций или новых строк. JЭто
ynрощенная версия утилиты 'WC' системы' UNIX '!

#dеfiлe yes 1
#dеfiлe rю О

mainО /* CO Lll1t lines, words, chars in input */


(
int с, пс, nI. nw, inword;
inword = 00;
nI = nw = nc = О;
while((c = gelcharO) != EOF)
++nc;
if(c == '\n' )
++nI;
if(c==' ' 11 c=='\n' 11C== '\l')
inword = по;
ебе if (inword == по)
inword = yes;
++nw;
}
}
printf('%d %d %d\n", nI. nw, пс);
}

Каждый раз, когда программа встречает первый символ слова, она


увеличивает счетчик числа слов на единицу. переменная i n wor d
следит за тем, находится ли программа в настоящий момент внутри
слова или нет; сначала этой переменной присваивается " не в слове",
чему соответствует значение по . МЫ предпочитаем символические
константы у е з и п о литерным значениям 1 и О , потому что они делаЮ1
программ у более у,цобной для чтения. Конечно, в такой крошечной
программе, как эта, это не приво.цит к заметной разнице, но в болыl.lиx
з2
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

программах увеличение ясности впол не стоит тех скромных

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


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

С трока

nI = пw = nc = О ;
полагает все три переменные равными н уnю. Это не особый случай , а
следствие того обстоятельства , что оператору присваивания
соответствует неЮJторое значение и присваивания проводятся

последовательно справа налево. Таким образом, дело обстоит так, как


если бы мы написали

пс = (n! = (пw = О ));

операция 1 1 Означает о r, так что строка

if( с == ' , 11 c=='\n' 11 C=='\I')

говорит "если с - пробел , или с - символ новой строки , или с -


табynяция ... "./ условная последовател ьность \ t яв л яется изображением
символа табyn яции/.

Имеется соответствующзя операция && для and. Выражения,


связанные операциями & & или 1 1, рассматриваются слева направо, и

при этом гарантируется, что оценивание выражений бу,цет прекращено,


как ТОЛЬЮJ станет ясно, является ли все выражение истинным или

ложным. Так, если ' с ' оказывается пробелом , то нет никаЮJЙ


необходимости проверять, является л и ' с ' символом новой строки и л и
табynяции, и такие проверки действительно не делаются. В данном
случае э то не имеет принципиального значения, но, как мы СЮJро

увидим, в более сложных ситуациях э та особенность яз ыка весьма


существенна.

Этот при мер также демонстрирует оператор el s e языка "С", ЮJторый


указ ывает то действие , ЮJторое ДОЛЖНО выполняться, если условие ,
содержащееся в операторе i f, окажется ложным.
зз
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Общая qюрма такова:

if (выражение)
о п е рато р-1
е Е е о пер ато р- 2

Выполняется один и только один из двух операторов, связанных с


конструкцией if - else . Есл и выражение истинно, выполняется
оператор- l ; если нет - выполняется оператор-2 . Фактически
каждый оператор может быть довольно сл ожным. В программе
подсчета сл ов оператор , сл едующий за else, является оператором if ,
который управляет двумя операторами в ф1гурных скобках.

Упражнение 1-9

Как бы вы стали проверять программу подсчета слов? Какие имеются


ограничения?

Упражнение 1-10

НаПИlШ1те программу, которая будет печатать сл ова из файла ввода,


причем по одному на строку.

Упражнение 1-11

Переделайте программу подсчета слов, используя лучшее определ ение


"слова"; считайте, например сл овом посл едовательность букв, ци@ и
апостроqюв , начинающуюся с буквы.

1.6. Массивы

Давайте напишем программу подсчета числа появ л ении каждой ци@ы,


символов пустых промежyrков/пробел, табуляции , новая строка/ и всех
остальных символов. Конечно, такая задача несколько искусственна, но
она позволит нам прои ллюстрировать в одной программе сразу
несколько аспектов языка 'С".

Мы раз били вводимые символы на двенадцать категорий, и нам


удобнее использовать массив для хранения числа появлений каждой

"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

циtП>ы , а не десять отдельных переменных. Вот один из вариантов


программы:

mainО /* COLll1t digits, white space, others */


{
int с, i, nwhite, поtПeг;
iлt ndigit[lO);

nwhite =rюthег = О;
for (i = О; i < 10; ++i)
ndigit[i) = О;

while ((е
= getehar()) != EOF)
if(e >= 'о' && е <= '9')
++ndigit[e-'O');
else if(e== " 11 е == '\n' 11 е == '\1')
++nwhite;
else
++пошег;

printf("digits =');
for (i = О; i < 10; ++ i)
printf(" 0/0<1", ndigit[i));
prinrf('\nwhite space = o/od, оtПeг = %d\n",
nwhite, rюthег);

описание

iлt ndigit[lO);

объявляет, что ndigi t является массивом и з десяти цел ы х. В языке 'С"


индексы массива все гда начин аются с н уля, а не с 1, как в фортране и л и
РИl , так что элем ентами массива являются ndigi t (О ] ,
ndigi t [ 1] , ... , ndigi t (9 ]. эта о собенность отражена в циклах
for , которые инициализируют и п е чатают массив.

Индекс может быть любым целым выражением , которое, конечно,


может включать целые переменные, такие как i, и целые константы.

35
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Эта конкретная программа сильно опирается на свойства символьного


представления циtW. Так, например, в программе проверка

if( с >= 'о' && с <= '9') ...

определяет, является ли символ в т с ' циtWой, и если это так, то


числ енное значение это й цифры определяется по qюрмуле / с
1 О 1 / . Такой способ работает только в том случае, если значения
символьных констант ' о Т, '1' и Т.д. Пол ожительны, расположены в
порядке возрастания и нет ничего, кроме цифр, между константами' О '
и ' 9 '. к счастью, это верно для всех общепринятых наборов символов.

По определению перед проведением ари~етических операций,


вов лекающих переменные типа char и in t , все они преобразуются к
типу in t , так что в ариф\1етических выражениях переменные типа cha
по существу идентичны переменным типа int . Это вполне естественно
и удобно; например, с - ' о т - это целое выражение со значением
между О и 9 в соответствии с тем, какой символ от 1 О ' до ' 91
хранится в ' с 1 , и, следовательно, оно является подходящим индексом

для массива ndig i t .

Выяснение вопроса , является ли данный символ цифрой, символом


пустого промежугка или чем- л ибо еще, осуществляется
посл едовательностью операторов

if (с >= 'о' && с <= '9')


++ndjgit[c- 'O'];
еЕе if(c == " 11 с == '\n' 11 с == '\l')
++nwhite;
еЕе
++nother;

конструкция

if (условие)
оператор

еЕе if (условие)
оператор

еЕе

з6
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

оператор

часто встречаются в программах как средство выражения ситуаций, в


которых ОС)'l.Lf'ствляется выбор одного из нескольких возможных
решений.

Программа просто движется сверху вниз до тех пор, пока не


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

4иrypные скобки 1. Если ни одно из условий не у,цовлетворяется, то


выполняется' оператор " стоящий после заключительного else, если
оно присyrствует. Если посл еднее else и соответствующий' оператор'
опущены (как в программе подсчета слов), то никаких действий не
производится. Между начальным if и конечным else может
помещаться произвольное количество групп

еЕе if (условие)
оператор

С точки з рения стиля целесообразно записывать эту конструкцию так,


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

Оператор swi tch (переключатель), который рассматривается в л екции


NQ 3, представ л яет дрyryю возможность для записи раз ветвления на
несколько вариантов. этот оператор особенно у,цобен, когда проверяемое
выражение является л ибо просто некоторым целым, л ибо символьным
выражением , совпадающим с одной из некоторого набора констант.
Версия этой программы , использующая оператор swi tch , бу,цет для
сравнения приведена в лекции NQ З.

Упражнение 1-12

Напишите программу, печатающую гистограмму длин сл ов из файла


ввода. Самое легкое начертить гистограмму горизонтально;
вертикальная ориентация требует больших усил ий.

1.7. Функции
з7
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

в языке 'С" функции эквивалентны подпрограммам или функциям в


ф:Jртране или процеАУРам в PUl, паскале и Т.д. функции дают у,цобный
способ заключения некоторой части вычислений в черный ящик,
который в дальнейшем можно использовать, не интересуясь его
внутренним содержанием. Использование функций является фзктически
единственным способом справиться с потенциальной сложностью
больших программ. Если функции организованы должным образом , то
можно игнорировать то, как делается работа; достаточно з нание того,
что делается. Язык 'С" разработан таким образом, чтобы сделать
использование функций легким, удобным и эq$:>ктивным. Вам будуг
часто встречаться функции длиной всего в несКDЛЬКD строчек,
вызываемые ТОЛЬКD оДИН раз, и они используются только потому, что

это проясняет неКDТОРУЮ часть программы.

До сих пор мы использовали только предоставленные нам функции тиП<


printf, getchar и putchar ; теперь пора написать несКDЛЬКО
наших собственных. так как в 'С" нет операции возведения в степень,
подобной операции ** в фJртране или PUl, давайте проиллюстрируем
механику определения функции на примере функции powe r (m, n) ,
во зводящей целое m в целую положительную степень n . Так значение
power (2, 5) равно 32. Конечно, эта функция не выполняет всей
работы операции * *, ПОСКDЛЫ<У она действует ТОЛЬКD с
положительными степенями неБОЛЫ1ШХ чисел , но лучше не создавать
до полнительных затру,цнений, смешивая несКDЛЬКD различных

вопросов.

Ниже приводится функция power и использующая ее основная


программа, так что вы можете видеть целиКDМ всю структуру.

mainО /* test power ftmction */


{
int~

fOl\i = О; i < 10; ++i)


prinrf('%d %d %d\n", ~power(2 , i) ,power(-3 , i) ;

power(x,n) /* rai5e х n-th power; п > О */


int х,п;
за
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

int i, р;
Р = 1;
for (i = l ; i<= п; ++i)
Р = р '" х;
return ( р );
}
Вс е функции имеют одинако в ы й ви д :
имя ( сп и сок а р гуме н тов, есл и о ни и ме ются)
о пи сани е а р гуме н то в , если о ни име ются

{
о пи сани я

о п е р ато ры

Эти функции MOfyr быть записаны в любом порядке и находиться в


одном или двух исходных фаЙлах. Конечно, если исходная программа
размещзется в двух файлах, вам придется дать БОЛЫllе указаний при
компиляции и загрузке , ч ем есл и бы она находи лась в одном , НО это
дело операц и онной системы, а не атрибут языка. В данный момент, для
того чтобы все полученные сведен ия о прогоне "С " - программ, не
изменились в дальнейшем , мы бу,цем предполагать, что обе функц ии
находятся в одном и том же файле .

функция powe r вызывается дважды в строке

printf{'"%d %d %d\n", i,роwег(2, i), р оwег(- З, i)) ;

при каждом обращении функция powe r , получив два аргумента,


во з вращзет целое значение, которое печатается в заданном ф:Jрмате . В
выражениях powe r (2 , i) является точно таким же целым, как 2 и i./
Не все функц ии выдают цел ое значение; мы займемся э тим вопросом в
ле кци и No 4/.

Аргументы функции powe r ДОЛЖНЫ быть описаны соответствующим


образом , так как их типы известны. Это сделано в строке

int х,п;

которая слеАУет за именем функции.


з9
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

описания аргументов помещаются между списюм аргументов и

открывающейся л евой фигурной сюбюй каждое описание


заканч ив ается то чкой с запятой. Имена , использованные для аргументов
функции powe r , являются чисто локальными и недоступны никаким
друтим функциям: друтие процедуры MOryr использовать те же самые
имена без возникновения конфликта. Это верно и для переменных i и р
;i в функции power никак не связано с i в функции main .

Значение, вычисленное функцией powe r , передаются в та i n с


помощью оператора return , точно такого же, как в РИ1 . Внутри
круглых скобок можно написать любое выражение. функция не обязана
во зв ращать какое-либо з начение ; оператор return , не соде ржащий
никакого выражения, приводит к такой же передаче управления, как
"свал и вание на юнец" функции при достижении коне чн ой правой
4игурной скобки, но при этом в вызывающую функцию не возвращается
никакого пол ез ного значения.

Упражнение 1-13

НаП ИlШ1 те программу преобразования прописных букв из файла ввода в


строчные , используя при этом функцию powe r (с) , юторая возвращает
значение ' с ', если ' с ' - не буква, и значение соответствующей
строчной буквы, если ' с ' - буква.

1.8. Аргументы - вызов по знач ению

Один аспект в 'С" может оказаться непривычным для программистов,


юторые использо вали друтие языки, в частности, qюртран и РИ1. В
языке "С" все аргументы функций передаются ''по значению". это
означает, что вызванная функция получает значения своих аргументов с
помощью временных переменных lфактически че рез стек 1, а не их
адреса. Это привqдит к некоторым особенностям, отличным от тех. с
юторыми мы сталкивались в языках типа qюртрана и РИ1,
использующих" вызов по ссылке ", где вы з ванная процеАУРа работает с
адресом аргумента, а не с его значен и ем.

Главное отличие состоит в том, что в "С" вызванная функция не может


изменить переменную из вызывающей функц ии ; она может менять

"
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

ТОЛЬЮJ свою собственную временную ЮJПИЮ.

вызов по значению, ОДнаЮJ, не помеха , а весьма ценное качество. Оно


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

можно обращаться как с удобно инициализированными локальными


переменными вызванной процедуры. Вот, например, вариант функции
power использующей это обстоятельство

power(x. n) /* rai5e х n-th power; п > О;


version 2 */
int х,п;
{
int р ;

юг (р = 1; п > О ; --п)
р = р *х;
ге 'шп ( р );
}

аргумент n используется как временная переменная ; из него вычитается

единица до тех пор, пока он не станет нулем. переменная i здесь болыш


не нужна. чтобы ни проис.ю,ци л о с n внутри power это никак не
вл ияет на apryмeHT, с которым первоначально обрати л ись к функции
power .

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


переменную из вызывающей программы. Эта программа должна
обеспечить установ л ение адреса переменной /технически , через
указатель на переменную /, а в вызываемой функции надо описать
соответствующий аргумент как указатель и ссылаться к фзкти ческой
переменной ЮJсвенно через него. Мы рассмотрим это подробно в
ле кци и NQS.

Когда в качестве аргумента BbIcтynaeт имя массива, то фактическим


значением , передаваемым функции, яв л яется адрес начала массива. /
Здесь нет никаЮJГО ЮJпирования элементов массива /. С помощью
индексации и адреса начала функция может найти и изменить любой
э лемент массива. Это - тема сл едующего раздела.
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

1.9. Массивы символов

По-видимому самым общим типом массива в "С" является массив


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

while (и меется el.Lf' строка)


if(эта строка дл иннее само й длинной из
предыдущих)
запом нить эту строку и ее длину напечатать са мую дл инную строку

По этой схеме ясно, что программа естественным образом распадается


на несколько частей. Одна часть читает новую строку, другая проверяет
ее, третья запоминает, а остальные части программы управляют этим

процессом.

Поскол ьку все так прекрасно дел ится, было бы хороll.Ю И написать
программу соответствующим образом. Давайте сначала напишем
отдельную функцию get l in e , которая бу,цет извлекать следующую
строку из файла ввода; это - обобщение функции getcha r . Мы
попытаемся сделать эту функцию по возможности более гибкой, чтобы
она была полезно й и в других ситуация.х. Как м ини мум getli n e
должна передавать с и гнал о возможном появлении конца файла; более
общий полезный вариант мог бы передавать длину строки и л и нуль,
если встретится конец файла. Нуль не может быть длиной строки, так
как каждая строка содержит по крайней мере один символ; даже строка,
содержащая только символ новой строки, имеет длину 1.

Когда мы находим строку, которая длиннее самой длинной из


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

Наконец, нам нужна основная программа для управления функциями


g e t1in e и сору . Вот результат:

#dеfiлe МAXLINE 1000 1* таximwn inрш


Б.8. К<'рниган, Д.М. Ричи Язык "JIOграм""ирования С

5ize */
linе
mainQ /* find lопgе5t line * /
{
int lеп; /* сurтеп( line leпgth */
int maх; /* maximum leпgth 5ееп 50 far */
сhзг line[MAXLlNE]; /* сшгепt inрut line */
сhзг 5ave[MAXLINE]; /* Ioпgе5t linе, 5aved */

maх = О;
while (Оеп = get1ine(line, MAXLINE)) > О)
if (!еп > тах) {
max = lеп;
сору(linе,5ауе);
}
if (maх > О) /* there wa5 а linе */
ргinrК'%5",5ауе);
}
getline(5,lim) /* get linе into 5,геtшп lепgth */
ehar s[];
irnlim;
{
int с, ~

fог(i= О;i< lim-l && (e=getehar())! =EOF && e! ='\n';++i)


s[i] = е;
if (е == '\n') {
s[i] = е;
++~
}
s[i] = '\0';
retuгn(i);
}

СОРу(51, 52) /* сору 51 (о 52;


а55ume 52 blg епоugh */
ehar sl[], s2[];
{
int~
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

i = О;
while ((s2[i] = sl [i]) != '\0')
++i;

функция mai n и g e t l ine общаются как через пару apryмeHToB, так и


через возвращаемое значение. аргументы getl ine описаны в строках

char s[];
iлt Iim;

которые указывают, что первый apryмeHT является массивом , а второй -


целым.

Длина массива s не указана, так как она определена в mai n. функция


g e tlin e использует оператор r e tu r n для передачи з начения назад в
вызывающую программу точно так же, как это делала функция powe r .
Одни функции возвращают некоторое нужное з начение ; другие,
подобно сору , используются из-за их действия и не во з вращают
никакого з начения.

Чтобы пометить конец строки символов , функция g e tl i ne помещает в


конец создаваемого ей массива символ \ О / н ул евой символ, значение
которого равно нулю/. Это соглашение используется также
компилятором с языка "С": когда в "С" - программе встречается строчная
константа типа

'11elloln"

то компилятор создает массив символов , содержащий символы этой


строки , и заканчивает его символом \0 , с тем чтобы функции,
подобные p r i n t f , могли зафиксировать конец массива:

! h !e !I!I !o !\n ! \O !

С пециф1кация ф:эрмата %s указывает, что р r i nt f ожидает строку,


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

"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

оканчивается символом \0 , и копирует этот символ в выходной


аргумент з2 .lВсе э то подразумевает, что символ \0 не является частью
нормального текста/.

Между прочим , сто и т отметить, что даже в такой маленькой программе,


как эта, возникает несколько неприятных организационных проблем.
Например, что должна делать main , если она встретит строку,
превышающую ее макс им ально возможный размер ? ФУНIO.\ И Я getline
поступает разум но: при заполнен и и массива она прекращает

дал ьнейшее извлечение символов, даже если не встречает символа


новой строки. Проверив полученную длину и последний символ,
ФУНIO.\ИЯ та i n может установить, не была ли эта строка слишком
дл инной, и поступить затем, как она сочтет нужным. Ради краткости мы
опустили эту проблему.

Пользователь ФУНIO.\ии getline никак не может заранее узнать,


насколько дл инн ой окажется вводимая строка. По этому в getline
включен контроль переполнения. В то же время пользователь функц ии
сору уже знает /и л и может узнаты, каков размер строк, так что мы
предпочли не включать в эту ФУНIO.\ию дополнительный контроль.

Упражнение 1-14

Переделайте веАУЩУЮ ч асть программы поиска самой длинной строки


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

Упражнение 1-15

Нап иши те программу печати всех строк длиннее 80 символов.

Упражнение 1-16

НаП ИlШ1 те программу, которая будет удалять из каждой строки стоящие в


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

Упражнение 1-17

НаП ИlШ1 те ФУНIO.\ию reverse (з) , которая располагает символьную

"
Б.8. К<'рниган, Д. М . Ричи Я ЗЫ К "JIOграм""ирования С

строку s в обратном порядке. С ее помощью напишите программу,


которая обратит каждую строку из файла ввода.

1.10. Область действия: внешние переменные

nepeMeHHbIeB main (line, save и Т . д . ) яв л яютсявнyrренними


или локальными по отношению к функции main , потому что они
описаны внyrри main и никакая другая функция не имеет к ним
прямого доступа. Это же верно и относительно переменных в других
функциях; например, переменная i в функции getline никак не
связана с i в сору . Каждая локальная переменная c)'l.Цf'CТByeT только
тогда, когда произошло обращение к соответствующей функции , и
исчезает, как только закончится выполнение этой функции. По этой
причине такие переменные , следуя терминологии других языков,

обычно называют автоматическими. Мы впредь бу,цем испол ьзовать


термин автоматические при ссылке на эти динамические л окальные

переменные. /в л екции No 4 обсуждается класс статической памяти,


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

сохранить свои значения между обращениями к функциям /.

Поскол ьку автоматические переменные появляются и исчезают вместе с


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

явно присваивать значения. Если этого не сделать, то они будуг


содержать мусор.

в качестве альтернативы к автомати ческим переменным можно


определ ить переменные, которые бyдyr внешними для всех функций,
Т.е. гл обальными переменными , к которым может обратиться по имени
любая функция, которая пожел ает это сделать. (этот механизм весьма
сходен с соmmоп в фортране и external в РИ1). Так как внешние
переменные доступны всю.цу, их можно использовать вместо списка

аргументов для передачи данных между функциями. Кроме того,


поскольку внешние переменные существуют постоянно , а не

появляются и исчезают вместе с вызываемыми функциями, они


сохраняют свои значения и посл е того, как функции , присвоившие им
эти значения , завершат свою работу.
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

ВнеlllН.ЯЯ переменная должна быть определена вне всех функций; при


этом ей выделяется фактическое место в памяти. Такая переменная
должна быть также описана в каждой функции , которая собирается ее
использовать ; это можно сделать либо явным описанием ex t e r n, либо
неявным по контексту. Чтобы сделать обсуждение более конкретным,
давайте перепишем программу поиска самой длинной строки, сделав
l i ne , save и та х внеll.lНИМИ переменными. Это потребует изменения
описаний и тел всех трех функций , а также обращений к ним.

#define rrnxlinе 1000 /'" rrnХ. inрш line size"'/

cha.r line[rrnxline]; /'" input linе "'/


cha.r save[rrnxline];/'" longest line saved here"'/
int rrnx;/"' length of longesr line seen so far"' /
rrninО /"'find longest linе ; specialized version"'/
{
int 1еп;
ежеrn int rrnх;
ежеrn cha.r save[];
rrnх = О;

while ( (leo = get1ineO) > О )


if(leo > тах) (
rrnх = len;
сору();
}
if (rrnХ > О) /'" there was а linе "'/
printf( '%s", save );
}

getlineO /'" specialized version */


{
int с, ~
ежеrn cha.r line[];

for (i = О; i< rrnxlinе-l

&& (c=gelcharO) !=EOF && с! ='\о' ; ++i)

"
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

line[i] = с;
++ ~
}
line[i] = '\0'
rerurn(i)
}
с оруО /* specialized version */
{
int~
еме rn char linе[], save[];

i= О;
while «save[i] = line[i]) ! ='\О' )
++ ~

ВнеlШlие переменные для фующий main , getline и сору


определ ены в первых строчках приведенного выше примера, которыми

указывается их тип и вызывается отведение для них памяти.

С интаксически внешние описания точно такие же, как описания,


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

функций, соответствующие переменные ЯВЛЯЮТСЯ внешними. Чтобы


функция могла использовать внеll.lНЮЮ переменную, ей надо сообщить
ее имя. Один способ сделать - это включить в фунКЦИЮ описание
extern ; это описание отличается от предьщуrцих только добавлением
ключевого слова е х t е r n.

В определеных ситуациях описание ex t ern может быть опущено: если


BHell.lНee определение переменной на.ХОДится в том же исходном файле,
раньше ее использования в некоторой конкретной функции , то не
обязательно включать описание extern для этой переменной в саму
фунКЦИЮ. описания ex ter n в функциях main , getline и сору
яв л яются , таким образом, излишними. Фактически , обычная практика
заключается в помеll.f'НИИ определений всех внеlШlИХ переменных в
начале исходного файла и послеДУЮIl.f'М опускании всех описаний
extern.

Если программа находится в нескольких исходных файлах, инекоторая


переменная определена, скажем в файле 1, а используется в файле 2, то

'"
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

чтобы связать эти два вхождения переменной, необходимо в файле 2


использовать описание extern . Этот вопрос подробно обсуждается в
лекции No 4.

Вы должно быть заметили, что мы в этом разделе при ссылке на


внеll.nше переменные очень аккуратно используем слова описание и

определение. " определение " относится к тому месту. где переменная


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

Между прочим , существует тенденция объявлять все, что ни попадется,


внеLLlНИМИ переменными, поскольку кажется, что это ynрощзет связи, -
списки apryмeHToB становятся короче и переменные всегда

присутствуют, когда бы вам они ни понадобились. Но внеlllН.ие


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

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

переменные при этом мосуг изменяться неожиданным и даже

неУМЬШlllенным образом, а программы становится тру,цно


МОДиф,щировать, когда возникает такая необходимость. Вторая версия
программы поиска самой длинной строки уступает первой отчасти по
этим причинам , а отчасти потому. что она лишила универсальности две

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


они бy,цyr манипyn.ировать.

Упражнение 1-18

Проверка в операторе for функции getline довол ьно неуклюжа.


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

1.11. Резюме

На данном этапе мы обсудили то, что можно бы назвать традиционным


ядром языка "С". Имея эту горсть строительных блоков, можно писать
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

полезные программы весьма значительного размера , и было бы


вероятно неплоXDЙ идеей, если бы вы задержались здес ь на какое-то
время и поступили таким образом: следующие ниже упражнения
предлагают вам ряд программ несколько БОЛЫllей сложности, чем те,
которые были приведены в это й лекции.

После того как вы овл адеете это й частью 'С", приступайте к чтению
следующих нескольких лекций. Усилия, которые вы при этом затратите,
полностью окупятся, потому что в этих лекциях обсуждаются именно те
стороны 'С", где мощь и выразительность языка начинает становиться
очевидной.

Упражнение 1-19

Напишите программу detab , которая заменяет табynяции во вводе на


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

Упражнение 1-20

Напишите программ у entab, которая заменяет строки пробел ов


минимальным числом табуnяций и пробелов , достигая при этом тех же
самых промежугков. используйте те же табynяционные остановки, как и
в detab .

Упражнение 1-21

Напишите программу для "сгибания" длинных вводимых строк посл е


посл еднего отличного от пробела символа, стоящего до столбца n ввода,
где n - параметр. убедитесь, что ваша программа делает что-то разумное
с очень длинными строками и в случае, когда перед указанным

столбцом нет ни табynяций, ни пробело в.

Упражнение 1-22

Напишите программу удаления из ' С "- программы всех комментариев.


Не забывайте аккуратно обраJ..ЦЗТЬСЯ с "закавыченными" строками и
символьными константами.

50
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Упражнение 1-23

НаПИlШ1те программу проверки 'С"- программы на эл ементарные


синтаксические ОlШ1бки, такие как несоответствие кругл ых, квадратных
и 4игурных скобок. Не забудьте о кавычках, как QДиночных, так и
двойных, и О комментариях. (Эта программа весьма сложна, если вы
будете писать ее для самого общего сл учая).
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Типы, операции и выражения

Изучаются конструкции языка, рассматриваются возможности


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

2. Типы, операции и выражения

Переменные и константы являются основными объектами, с которыми


оперирует программа. описания перечисляют переменные, которые

буАУ" использоваться, указывают их тип И, возможно, их начальные


значения. Операции определяют, что с ними будет сделано. Выражения
объединяют переменные и константы для получения новых значений.
Все это - темы настоящей л екции.

2.1 . Имена переменных

Хотя мы этого сразу прямо не сказали, существуют некоторые


ограничения на имена переменных и символических констант. Имена
составляются из букв и цифр; первый символ должен быть буквой.
Подчеркивание тоже считается буквой; это полезно для
удобочитаемости длинных имен переменных. Прописные и строчные
буквы различаются; традиционная практика в "с" - использовать
строчные буквы для имен переменных, а прописные для
символических констант.

Играют роль только первые восемь символов внутреннего имени, хотя


использовать можно и больше. Для внеll.lНИХ имен , таких как имена
функций и внеll.lНИХ переменных, это число может оказаться меньше
восьми , так как внеl.LIНие имена используются различными

ассемблерами и загрузчиками. Детали приводятся в при л оже нии А.


Кроме того, такие ключевые слова как if, else, int , float и Т.д.,
зарезервированы: вы не можете испол ьзовать их в качестве имен

переменных. (Они пишyrся строчными буквами).

Конечно, разумно выбирать имена переменных таким образом, чтобы


они означали нечто, относящееся к назначению переменных, и чтобы
было менее вероятно спутать их при написании.

52
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

2.2. Типы и размеры данных

в языке С имеется ТОЛЬКО несколько основных типов данных: cha r один


байт, в котором может находиться один символ из внутреннего набора
символов. int Целое, обычно соответствующее естественному размеру
целых в используемой машине. f 1 оа t С плавающей точкой одинарной
точности. double С плавающей точкой двойной точности.

Кроме того имеется ряд квали4икаторов , которые можно использовать с


типом int : short (короткое), long (длинное) и unsigned (без
знака). Квалиф1каторы short и long указ ывают на различные
размеры целых. Числа без знака подчиняются законам арифvtетики по
модулю 2 в степени п, где n - число битов в int ; числа без
знаков всегда неотрицательны. описания с квалиф1каторами имеют
вид:

short inr х;
Iong irn у;
W1Signed int z;

С лово int в таких ситуациях может быть опущено, что обычно и


делается.

Количество битов , отводимых под эти объекты зависит от имеющейся


машины; в таблице ниже приведены некоторые характерные значения.

Таблица
2. 1.
DEC PDP-ll HONEYWELL6000 lВM 370 INТERDAТA8/32

ASC ll ASC ll EBCDlC ASCll


char 8-BIТS 9-ШТS 8-ШТS 8-ШТS

in t 16 32 32 32
short 16 36 16 16
l ong 32 36 32 32
float 32 36 32 32
double 64 72 64 64
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Цель состоит в том, чтобы short и long давали возможность в


зависимости от практических НУЖД использовать разл ичные длины

целых; тип int отражает наиболее "естественный" размер конкретной


маJ.LП.1НЫ. Как вы видите, каждый компилятор свободно интерпретирует
short и long в соответствии со своими аппаратными средствами.
Все , на что вы можете твердо полагаться, это то, что short не
дл иннее, чем long.

2.3. Константы

Константы типа int и float мы уже рассмотрел и. Отметим еще


только, что как обы чная

1 23. 45 6е-7,

так и ''н аучная'' запись

0.12е3

для f lоа t является законной.

Каждая константа с плавающей точкой считается имеющей тип


double , так что обозначение" е "служит как для float , так и для
double.

Длинные константы записываются в виде 12З L. Обычная целая


константа, которая слиl..LIкDм длинна для типа int , рассматривается как

long.

Существует система обозначений для восьмеричных и


шестнадцатеричных констант: л идирующий О (нуль) в константе тип а
int указывает на восьмеричную константу, а стоящие впереди Ох
соответствуют шестнадцатеричной константе. Например, десятичное
числ о 31 можно записать как 037 в восьмеричной ф:Jрме и как Ox1f в
шестнадцатеричноЙ. Шестнадцатеричные и восьмеричные константы
мосут также заканчиваться буквой 1, что делает их относящимися К типу
1ong.
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

2.3.1. Символьная ЮJнстанта

С имвольная ЮJнстанта - это один символ, заключенный в одинарные


кавычки, как, например, ' х ' . 3начением символьной константы
яв л яется численное значение этого символа во внутреннем Mal..lfl.1HHOM
наборе символ ов. Например , в наборе символов ASCl1 символьный
нуль, или' О ', имеет значение 48 , а в коде ebcdic - 240 , и оба эти
значения COBepJ..LJeHHO отличны от числа О. Написание ' о ' вместо
числ енного значения, таЮJГО как 48 или 240 , делает программу не
зависящей от конкретного числ енного представления этого символа в
данной Mal..lfl.1He. С имвольные ЮJнстанты точно так же участвуют в
числ енных операциях, как и любые другие числа, хотя наибол ее часто
они испол ьзуются в сравнении с другими символами. Правила
преобразования бу,цуг изложены по зднее.

НеЮJторые неграфические символы MOryr быть представ л ены как


символьные ЮJнстанты с помощью усл овных посл едовательностей, как,
например,

• \n (новая строка) ,
• \ t (табуляция),
• \ О (н ул евой символ),
• \\ (обратная косая черта),
• \ ' (одинарная кавычка) и Т.д.

Хотя они выглядят как два символа, на самом деле являются одним.
Кроме того , можно сгенерировать произвольную посл едовательность
двои чных знаЮJВ размером в байт, если написать

'\ddd'

где ddd - от одной до трех восьмеричных цифр , как в

#define formfeed '\014' /* form feed */

С имвольная ЮJнстанта '\ О ', изображающая символ со значением О ,


часто записывается вместо целой ЮJнстанты О, чтобы подчеркнуть
символьную прирqцy неЮJТОРОГО выражения.
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

2.3.2. Константное выражение

Константное выражение - это выражение, состоящее и з одних констант.


Такие выражения обрабатываются во время компиляции, а не при
прогоне программы, и соответственно мосуг быть использованы в
любом месте , где можно использовать константу, как, н апример в

#dеfiлe mзxlinе 1000


char linе[mз xlinе+ l J;

или

seconds = 60 '" 60 '" lюurs ;

2.3.3. Строчная константа

С трочная константа - э то последовательность , состоящая из нуnя и л и


более си мволо в , заключенных в двойные кавычки , как, например ,

"j аm а string" /'" я - стро ка '" / и л и


,~, /'" null string "'/ /'" н ул ь-строка "'/

Кавычки не являются частью строки , а сл ужат только для ее


ограничения. те же самые усло вные посл едовательности , которые

использав ал ись в символьных константах, применяются и в строках;

символ дво йной кавычки изображается как \ ".

С технической точки зрения строка представляет собо й массив,


эл ементами которого являются отдельные символы. Чтобы программам
было у,цоб но определять конец ст роки , компилятор автоматически
помещает в юнец каждой строки нуль- с имвол \ О . Такое представ л ение
означает, что не накладыв ается конкретного ограничения на то, какую

дл ину может иметь строка , и чтобы определить эту длин у, программы


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

56
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

strlen(S) !*геtшnlепgthоfs* !
char s[];
{
int~

i= О;
while (s[i] != '\0')
++ ~
rеtшn(i);
}

Будьте внимательны и не пyrайте символьную константу со строкой,


содержащей один символ: ' х ' - это не то же самое, что" х ". Первое -
это отдельный символ, использованный с целью получения численного
значения, соответствующего букве х в машинном наборе символов.
Второе - символьная строка, состояЩ1Я из одного символа (буква х ) и
\0 .

2.4. Описания

Все переменные должны быть описаны до их использования, хотя


некоторые описания делаются неявно, по контексту. описание состоит

из специ<f:икатора типа и следующего за ним списка переменных,


имеющих этот тип, как, например,

int Ioweг, uppeг, step;


char с, line[1000 ];

переменные можно распределять по описаниям любым образом;


приведенны е выше списки можно с тем же успехом записать в виде

int Iower;
int upper;
int step;
char с;
char linе [ 1000 ];

Такая qюрма занимает больше места, но она удобна для добавления


Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

комментария к каждому описанию и для послеАУЮЩИХ моди4икациЙ.

переменным могут быть присвоены начальные значения внутри их


описания , )[)тя здесь имеются некоторые ограничения. Если за именем
переменной следуют з нак равенства и константа , то эта константа
служит в качестве инициализатора, как, например , в

char bac kslash = '\\';


int i = O;
float eps = 1 .0е-5;

Если рассматриваемая переменная является внешней или статической,


то инициал изация провqд,ится тол ько один раз, согласно концепции до

начала выполнения программы . Инициализируемым явно


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

каждом обращении к функции, в которой они описаны. Автомати ческие


переменные, не инициализируемые явно , имеют неопределенные

значения, (т.е. мусор). Внешние и статические переменные по


умолчанию инициализируются нулем , но, тем не менее, их явная

инициализация является признаком XOpOI..LleГO стиля.

Мы продолжим обсуждение вопросов инициализации , когда бу,цем


описывать новые типы данных.

2.5. Арифметические операции

Бинарными ари~етическими операциями являются +, * / и


операция дел ения по модулю %. Имеется унарная операция -, но не

существует унарной операции +.

При дел ении целых дробная часть отбрасывается. Выражение

х%у

дает остаток от деления х на у и , сл едовательно, равно нулю, когда х

делится на у точно. Например , год является високосным , если он


делится на 4, но не дел ится на 100, исключая то , что делящиеся на 400
годы тоже яв л яются високосными. Поэтому

58
Б.8. К<'рниган, Д. М. Ричи Язык "JIOграм""ирования С

if(year % 4 == О && уеас % 100 != О 11 уеас % 400 == О)


го.ц високо сн ый
еЕе
го.ц невисоко с ный

Операцию % нельзя использовать с типами f loa t или double .

Операции + и - имеют один аковое cтapI..lfl.iHCТBO, которое младше


одинакового уровня cтapl..lfl.iHCТBa операций *, / и %, которые в свою
очередь младше унарного минуса. Ари~етические операции
группируются слева направо. (Сведения о cтapl..lfl.iHCТBe и
ассоциативности всех операций собраны в таблице в конце этой
лекции). Порядок выполнения ассоц и ативных и коммутативных
операций типа + и не 4иксируется; компилятор может
перегруппировывать даже заключенные в крутлые скобки выражения,
связанные такими операциями. Таким образом , а+ (Ь+с) может быть
вычислено как (а +Ь) +с . Это редко приводит к какому-либо
расхождению , но если необходимо обеспеч и ть строго определенный
порядок, то нужно использовать явные промежуточные переменные.

Действия, предпринимаемые при переполнении и антипереполнении


(т.е. при получе нии СЛИШКDм мале нького по абсолютной величине
числа), зависят от используемо й Mal..lfl.iHhI.

2.6. Операции отношения и логические операции

Операциями отношения являются

>= > =< <

все они имеют одинаковое cтapl..lfl.iHCТBo. Непосредственно за ними по


уровню cтapl..lfl.iHCТBa следуют операции равенства инеравенства:

== .=

которые тоже имеют одинаковое старшинство. операции отношения

младше ари~етических операций , так что выражения типа i<lim-l


понимаются как i < (lim-l ), как и предполагается.
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Логические связки & & и II более интересны. Выражения, связанные


операциями && и I 1, вычисляются слева направо , причем их

рассмотрение прекращзется сразу же как только сгановится ясно , будет


ли результат исгиной или ложью. Учет этих свойсгв очень сущесгвенен
дл я написания правильно работающих программ. Рассмотрим,
например, оператор цикла в считывающей сгроку функции getl i ne ,
которую мы написали в ле кции No l.

foг(i= О ; i< lim-l & & (e=getehar())


!= '\n' && е != EOF; ++i)
s(i]=e;

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


имеется ли el.Lf' месго в массиве 5 , так что условие i<l i m-l должно
проверяться первым. И есл и э то усл овие не выполняется, мы не
должны считывать следующий символ.

Так же неу,цачным бы л о бы сравнение I с ' С EOF до обраl.Lf'НИЯ к


функции getchar : прежде чем проверять символ, его нужно считать.

С тарJ.LП.1НСГВО операции & & выше , чем у I 1, и обе они младше операций
отношения и равенсгва. Поэтому такие выражения , как

i<lim-l & & (е = getehar()) != '\n' && е != EOF

не нуждаются в дополнительных круглых скобках. Но так как операция


!= старше операции присваивания , то для досгижения правильного
результата в выражении

(е = getehar()) != '\n'

скобки необходимы .

Унарная операция отрицания! Преобразует ненулевой или исгинный


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

if( ! iлwогd )

Вместо

БО
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

if( iлwогd == О )

Трmно сказать, какая qюрма луч ше. конструкции типа ! inword


Читаются довольно у,цобно ("если не в слове ' ). Но в более сл ожных
случаях они мосуг оказаться тру,цными для понимания .

Упражнение 2-1

Нап иши те оператор цикла , э квивалентный приведенному выше


оператору for, не используя операции & &.

2.7. Прео6разование типов

Если в выражениях встречаются операнды разл ичных типов, то они


преобразуются к обl.Lf'МУ типу В соответств и и с неболыl.в1M набором
правил. В обl.Lf'М, автоматически произво.цятся только преобразован ия,
имеющие смысл , такие как, например, преобразование целого в
плавающее в выражениях тип а f +i . Выражения же , лишенные смысла,
такие как использование переменной типа f 1 оа t в качестве индекса,
запреl.Lf'НЫ.

Во-первых, типы char и in t мосуг свободно смеll.В1ваться в


арифметических выражениях: каждая переменная типа char
автоматически преобразуется в in t . Это обеспечивает значительную
Л1бкость при проведении определеных преобразований символов.
Примером может сл ужи ть функция а toi , которая ставит в соответствие
строке ци<W ее числ енный эквивалент.

alo(5) /* convert s to integer >1< /

char 5[);
{
int i, п;

n = О;
for ( i = О; 5[i] >= '0' && 5[i]<='9'; ++ i)
n = 10 >1< n + s[i] - 'О';
return(n);
}
Б.8. К<'рниган, Д. М. Ричи Язык "JIOграм""иJlOВания С

Как уже обсуждалось в лекци и Nol, выражение

s[i] - 'О'

имеет численное значение находяl.lf'ГОСЯ в s [i ] символа, потому что


значение символов т О ', '1' и Т.д. образуют возрастающую
посл едовател ьность расположенных подряд целых положител ьных

чисел.

Другой пример преобразования char в int дает функция lower ,


преобразующая данную прописную букву в строчную. Если
выступающий в качестве аргумента символ не яв ля ется прописной
буквой, то l ower возвращает его неизменным. Приводи мая ниже
программа справедлива тол ько для набора символов ASCl1.

Iower(c) /* сопУеП с {о lower case; ascii оnlу */


int с;
{
if ( с >= '.' && с <= 'z' )
return( с + '@' - '.');
ебе /*@ Записано вместо 'а ' строчного*/
return(c);

Эта функция правильно работает при коде ASCll, потом у что


числ енные значения, соответствующие в этом коде прописным и

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


алфавит является сплоll.lным - между а и z нет ничего , кроме букв. Это
посл еднее замечание для набора символов ebcdi с систем 18М
3601370 оказывается несправеДЛивым , в силу чего эта программа на
таких системах работает неправильно - она преобразует не только
буквы.

При преобразовании символьных переменных в целые возникает один


тонкий момент. Дело в том, ч то сам язык не указывает, должны л и
переменны м типа char соответствовать численные з начения со знаком
и л и без знака. Может л и при преобразовании char в int получиться
отрицательное цел ое? К сожалению, ответ на этот вопрос меняется от
ма J.LП.1НЫ к маlШ1не , отражая расхождения в их архитектуре. На

62
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

некоторых маll.lИнах (PDP-ll , например) переменная типа c h ar ,


крайний левый бит которой содержит 1, преобразуется в отрицательное
целое ("знаковое раСll.В1рение'). На APyrnx Mall.В1HaX такое
преобразование сопровождается добавлением нynей с левого края, в
резynьтате чего всегда получается положительное число.

определение языка 'С" гарантирует, что любой символ из стандартного


набора символов маll.lИНЫ никогда не даст отрицательного числа, так
что эти символы можно свободно использовать в выражениях как
положительные величины. Но произвольные комбинации двоичных
знаков, хранящиеся как символьные переменные на некоторы х Mall.В1Hax,

мосут дать отрицательные значения, а на APyrnx положительные.

Наиболее типичным примером возникновения такой ситуации является


случай, когда з начение - 1 используется в качестве EOF. Рассмотрим
программ у

char с;
с = getcharO;
if( с == EOF)

На MaUМHe, которая не осyrцeствляет знаковorо расuмрения, переменная


t с t всегда положительна, поскольку она описана как cha r, а так как

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


такой ситуации, мы всегда предусм отрительно использовали i nt
вместо char для любой переменной , получающей з начение от
g e tchar .

Основная же причина использования i nt вместо c h ar не связана с


каким - либо вопросом о возможном знаковом раСll.lИрении. просто
функция getc h ar должна передавать все возможные символы (чтобы
ее можно было использовать для произвольнаго ввода) и, кроме того,
отличающееся значение EOF. Сл едовательно з начение EOF не может
быть представлено как char, а должно храниться как int .

Другой полезной фJрмой автоматического преобразования типов


является то, что выражения отношения, подобные i>j , и ЛО П1ческие
выражения, связанные операциями && и I 1, по определению имеют


Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

значение 1, если они истинны, и О , если они ложны. Таким образом,


присваивание

isdjgjt = с >= 'о' && с <= '9';

полагает isdigi t равным 1 , если с - ци<Wа , и равным О в противном


случае. (В проверочной части операторов if, whi1e , for и Т.д.
'Истинно" просто означает "не нуль").

Неявные ариф\1етические преобразования работают в основном, как и


ожидается. В общих чертах, если операция типа + или *, которая
связывает два операнда (бинарная операция), имеет операнды разных
типов, то перед выполнением операции ''низший'' тип преобразуется к
''выс шему' и получается ре зультат ' 'высшего'' типа. Более точно, к
каждой ари~етической операции применяется следующая
посл едовательность прави л преобразования.

• типы char и short преобразуются в


in t , а f 10а t в double .
• Затем, если один из операндов имеет тип double , то друтой
преобразуется в double , и результат имеет тип double .
• В противном случае, если один из операндов имеет тип long , то
д ругой преобразуется в long , и резyn.ьтат имеет тип l ong .
• В противном случае, если один из операндов имеет тип
unsigned , то дрyroй преобра зуется в unsigned и результат
имеет тип unsigned .
• В противном случае операнды должны быть типа in t , и резyn.ьта1
имеет тип in t . Подчеркнем, что все переменные типа f10at в
выражениях преобразуются в double ; в 'С" вся п лавающая
ариф\1етика выполняется с дво йной точностью.

Преобразования возникают и при присваиваниях; значение правой


части преобразуется к типу лев ой, который и яв ляется типом резyn.ьтата.
С имвольные переменные преобразуются в целые либо со знаковым
расширением, либо без него, как описано выше. Обратное
преобразование i nt в cha r ведет себя хороllЮ - Л Иll.lН.ие биты
высокого порядка просто отбрасываются. Таким образом

int t char с;
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

i = c;c = i;

значение т с т не изменяется. Это верно независимо от того,


вов л екается ли знаЮJвое раСlШ1рение или нет.

Если х типа f loat, а i типа i nt , то как

х = ~ так и
i = х;

приводят к преобразованиям ; при э том float преобразуется в i n t


отбрасыванием дробной части. тип dou bl e преобразуется во float
окрутлением. Длинные целые преобразуются в более ЮJроткие целые и
в переменные типа cha r посредством отбрасывания ЛИlШlих битов
ВЫСОЮJго порядка.

Так как аргумент фующии является выражением , то при передаче


функциям аргументов также происходит преобразование типов: в
частности , cha r и short становятся i nt , а f l oat становится
double . Именно поэтому мы описывали аргументы функций как i nt
и double даже то гда, I'Dгда обращзл ись к ним с переменными типа
С НARи float .

НаЮJнец , в любом выражении может быть осуществлено


(''принуждено ' ) явное преобразование типа с помощью I'Dнструкции,
наз ываемой перевод ( cas t ). В ЭТОЙ I'Dнструкции, имеющей вид

(имя т ип а) в ыр ажение

Выражение преобразуется к указанному типу по правилам


преобразования, изл оженным выше. Фактически точный смысл
операции перевода можно описать следующим образом: выражение как
бы присваивается неl'DТОРОЙ переменной указанного типа , I'Dторая
затем используется вместо всей ЮJнструкции. Например, библиотечная
процедура sq r t ожидает аргумента типа dou bl e и выдаст
бессмысленный ответ, если к ней по небрежности обратятся с чем­
нибудь иным. таким образом , есл и n- целое , то выражение

sqrt((double) п)

65
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

до передачи аргумента функции sqrt преобразует n к типу double .


(Отметим, что операция перевод преобразует з начение n в надлежащий
тип; фактическое содержание переменной n при этом не изменяется).
Операция перевода имеет тот же уровень cтapl..lМHCТBa, что и другие
унарные операции , как указывается в табл иц е в конце этой лекци и .

Упражнение 2-2

Составьте программу для функции h toi (з) , которая преобра зует


строку I..L.Ieстнадцатеричных циtП> в эквивалентное ей цел ое значен и е.
При этом до пустимыми циtП>ами являются циtП>ы от 1 до 9 и буквы от а
ДО F.

2.8. Операции увеличения и уменьшения

в яз ыке "С" преАУСмот рены две необычные операц и и для увеличения и


уменьшен ия з начений переменных. Операция увеличения ++ добавляет
1 к своему операнду, а операция умены..L.Ieияя -- вычитает 1. Мы часто
использовали операц ию ++ для увеличения переменны х, как,

например, в

Щс == '\n' )
++ ~

Необычный аспект заключается в том, ч то ++ и - - можно использовать


либо как префиксные операции (перед переменной, как в + +п ), л ибо
как постфиксные (после переменной: п++ ). Э Wкт в обоих случаях
состоит в увел ич ен ии п . Но выражение + +п увеличивает переменную n
до использования ее значения, в то время как п++ увеличивает

переменную n после того, как ее значение было использовано. Это


означает, что в контексте, где используется значение переменной, а не
тольЮJ эWкт увеличен ия, использован и е ++ п и п++ приводит к
разным результатам. Если n = 5, то

х = п++ ;

устанавл и вает х равным 5, а

б6
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Х = ++п;

полагает х равным б. В обоих случаях n становится равным б.


Операции увеличения и уменьшения можно применять только к
переменным ; выражения типа х = ( i + j ) ++ яв ляются незаконными.

в случаях. где нужен только эwкт увеличения, а само значение не


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

if( с == '\n' )
nl++;

выбор префиксной или постфиксной операции яв ляется делом вкуса. но


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

операцию. Рассмотрим, например, функцию squeeze (s, С) , которая


удаляет символ ' С ' из строки S , каждый раз, как он встречается.

squeeze(s,c) /* delete all с from s */


char s[ );
int с;

int i, j ;

for (i = j = О; s[i) != '\0'; i++)


if( s[i) != с)
sU++) = s[i);
sUJ = '\0';
}

Каждый раз, как встречается символ, отличный от ' с " он копируется в


текytЦYЮ позицию j, и только после этого j увеличивается на 1, чтобы
быть готовым для поступления следующего символа. Это в точности
эквивалентно запис и

if(s[i] ! = с) (
sU) = s[i);
j++;

Другой пример подобной конструкции дает функция get1ine , которую


67
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

мы запрограммировали в леЮJИИ Nol , где можно заменить

if(c == '\n'){
s(i] = с;
++~

более компактной записью

if( с == '\n')
s[i++ ] = с;

в качестве третьего при мера рассмотрим функцию strcat (5, t) ,


которая приписывает строку t в конец строки 5, образуя конкатенацию
строк 5 и t. При этом предполагается , что в 5 достаточно места для
хранения полученной комбинац ии .

strcat(s,t) /* corк:atenate t to end of 5 */


char s(], t(] ; j* s must Ье Ьщ enough *!
(
int i, j;

i=j = О;
while(s(i]! = '\O') j *find еnd ofs */
i++;
while«s(i++] = tU++]) != '\0') j*copy t*/

Так как из t в 5 копируется каждый символ, то для подготовки к

следующему прохождению цикла постф1ксная операция ++


применяется к обеим переменным i и j .

Упражнение 2-3

НаП ИlШ1 те другой вар и ант функции squeeze{51 , 52) , который


удаляет из строки 51 каждый символ, совпадающий с каким-либо
символом строки 52 .

Упражнение 2-4
ба
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

НаПИlШ1те программу для функции any (51 , 52) , юторая находит


место первого появления в строке 51 каюro-либо символа из строки
52 и, если строка 51 не содержит символ ов строки 52 , возвращает
значение -1.

2.9. Побитовые логические операции

в языке предусмотрен ряд операций для работы с битами; эти операции


нельзя применять к переменным типа f10a t или double .

Таблица 2.2.
& Побитовое AND
I Побитовое включающее OR
л побитовое исключающее OR

« сдвиг влево

» СДвиг вправо

дополнение (унарная операция)

"I "имитирует вертикальную черту.

Побитовая операция and часто используется для маскирования


неюторого множества битов; например, оператор

с = n &0071

передает в I С I семь мла,цlШ1х битов n, полагая остальные равными


нулю. Операция I I I побитового or используется для включения
битов:

c=xlmask

устанавливает на единиц у те биты в х , юторые равны единице в mask .

С ледует быть внимательным и отл ичать побитовые операции & и I от


логических связ ок && и I 1, юторые подразумевают вычисл ение

значения истинности слева направо. Например, если х = l, а у=2 , то

69
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

значение х&у равно нуnю, в то время как значение х&&у равно

единице.

Операции сдвига << и >> осytЦf'ствляют соответственно сдвиг влево и


вправо своего левого операнда на ч и сло битовых позиций , задаваемых
правым операндом. Таким образом, х«2 сдвигает х влево на две
позиции, заполняя освобождающиеся биты нyn.ями, что эквивалентно
умножению на 4. Сдвиг вправо величины без з нака заполняет
освобождающиеся биты на неЮJТОРЫХ машинах, таких как PDP-ll,
заполняются содержанием знаЮJВОГО бита "арифvlети ч еский сдвиг", а на
друтих - нулем "логический сдвиг".

Унарная операция - дает дополнение к целому; это означает, что


каждый бит со з начением 1 получает значение О и наоборот. Эта
операция обычно оказывается пол ез ной в выражениях типа

х & - О77

где последние I..L.IeCТb битов х маскируются нулем. Подчеркнем, что


выражение х&! 077 не зависит от длины слова и поэтому
предпочтительнее, чем, например , х&01 77700 , где предполагается,
что х занимает 16 битов. Такая переносимая форма не требует никаких
до полнительных затрат, ПОСЮJльку - О77 является ЮJнстантным
выражением и, следовательно, обрабатывается во время ЮJмпиляции .

Чтобы проиллюстрировать использование неЮJТОРЫХ операций с


битами, рассмотрим функцию getb i ts (х I Р , п), ЮJторая возвращает I

СДвинугыми к правому краю/ начинающиеся с позиции Р поле

переменной х дл ино й n битов. Мы предполагаем , ч то крайний правый


бит имеет номер О , и что П И Р - разумно заданные положительные
числа. Например, getbi ts (х , 4 1 3) возвращает СДвинугыми к
правому краю биты, занимающие по зиции 4, 3 и 2.

gerblts(x,p,n) /* ger n blts from pos.it:lon р */


unsigned Х, р , п;
{
rerurn( x» (p+l-n» & - (- 0 « п» ;
}

70
Б.8. К<'рниган, Д. М . Ричи Я ЗЫ К "JIOграм""ирования С

Операция х » (p+l-n ) сдвигает желаемое поле в правый конец


слова. описание аргумента х как unsigned гарантирует, что при сдвиГ1
вправо освобождающиеся биты будуг заполняться н ул ями , а не
содержимым знаЮJВОГО бита, независимо от того , на какой машине
пропускается программа. Все биты ЮJнстантного выражения -о равны
1 ; сдви г его на n по з иций влево с помощью операции -O«n создает
маску с нулями в n крайних правых битах и единицами в остальных;
дополнение - создает маску с единицами в n крайних правых битах.

Упражнение 2-5

Переделайте getbi ts таким образом, чтобы биты отсчитывал ись


слева направо.

Упражнение 2-6

Напишите программу для функции word l ength (), вычисл яющей


дл ину сл ова испол ьзуемой машины , Т.е. Числ о битов в переменной тип а
int . функция должна быть переносимой , Т.е. Одна и та же исходная
программа должна прави л ьно работать на любой машине.

Упражнение 2-7

Напишите программу для функции righ t ro t (n, Ь) , сдвигающей


циклически цел ое n вправо на Ь битовых позиций.

Упражнение 2-8

Напишите программу для функции invert (х , р , n) , которая


инвертирует (т.е. заменяет 1 на О и наоборот) n битов х , начинающи)[:я
с по з иции р , остав л яя др)'Тие биты неизмененными.

2.10. Операции и выражения присваивания

Такие выражения , как

i= i+2
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

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


в сжатой qюрме

i += 2

используя операцию присваивания вида +=.

БОЛЫllИНСТВУ бинарных операций (операций подобных + , которые


имеют лев ый и правый операнд) соответствует операция присваивания
вида ОП= , где оп - одна из операций

+ _ '" 1%«» & л I!


Если е 1 и е2 - выражения, то е 1 оп= е2 эквивалентно

еl = (еl) оп (е2)

за исключением того, что выражение е 1 вычисляется только один раз.

Обратите внимание на крутлые скобки вокрут е 2 :

х *= у + 1
то

х =х *(у + l)

не

х=х"'у+ l

в качестве примера приведем функцию bi tcoun t , которая


подсчитывает число равных 1 битов у целого аргумента.

bllcounr(n) /'" count 1 bits in n "'/


W1Signed п;
(
int Ь ;
for(b = О; п! = О; п »= 1)
"(п&ОI)
Ь ++;
ге lШП(Ь);
72
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Не говоря уже о кратЮJСТИ, такие операторы присваивания имеют то


преимущество, что они лучше соответствуют образу чел овечесЮJГО
мыuиения. Мы говорим: ''прибавить 2 к i " или ' 'увеличить i на 2", но
не "взять i , прибавить 2 и поместить результат опять в i ". Итак. i +=
2 . Кроме того , в громо здких выражениях, подобных

yyvаl(yypv(р З + р4 ] + yypv(pl + р 2]] += 2

Такая операция присваивания облегчает понимание программы , так как


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

выражения действительно одинаЮJВЫМИ, или задумываться, почему они


не совпадают. Такая операция присваивания может даже помочь
ЮJмпилятору получить более э <lФ"ктивную программу.

Мы уже использовал и тот факт, что операция присваивания имеет


неЮJторое значение и может входить в выражения; самый типичный
пример

while ((е = getehar()) != EO F)

присваивания , использующие другие операции присваивания ( += I

= И т.д.) также мосуг входить в выражения, хотя э то случается реже.

типом выражения присваивания является тип его левого операнда.

Упражнение 2-9

в двоичной системе счисл ения операция х& (x-l) обнуляет самый


правый равный 1 бит переменной х .(почему?) используйте это
замечание для написания более быстрой версии функции bi tcoun t .

2.11. Условные выражения

операторы

if(a > Ь)
z = а;

Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

else
z = Ь;

конечно вычисляют в z максимум из а и в . Усл овное выражение ,


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

el ?E2 : e3

сначала вычисляется выражение el . Если оно отлично от нуля


(истинно), то вычисл яется выражение е2 , которое и становится
значением условного выражения. В противном случае вычисл яется е3 ,
и оно становится з начением условного выражения. Каждый раз
вычисляется только одно из выражения е2 и е З . Таким образом, чтобы
положить z равным максимуму из а и В , можно написать

z= (а > Ь) ? а : Ь; j* z = mз х(а, Ь) *!

С ледует подчеркнуть, что условное выражение действительно является


выражением и может использоваться точно так же, как любое другое
выражение. Есл и е2 и еЗ имеют разные типы, то тип результата
определ яется по правилам преобразования , рассмотренным ранее в
этой лекции. Например, если f имеет тип float , а n - тип int , то
выражение

(п > О) ? f: n

Имеет тип double независимо от того, положительно л и n или нет.

Так как уровень cтapl..lfl.1HCТBa операции ?: очень низок, прямо над


присваиванием, то первое выражение в условном выражении можно

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


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

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


программам. Например , следующий ниже оператор цикла печатает n
элементов массива, по 10 в строке , разделяя каждый столбец одним
пробел ом и заканчивая каждую строку (включая посл еднюю) одним

"
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

символом перевода строки.

for (i = О; i< п; i++)


print~' %6d%c", a [iJ ,(i% l O== 9 1 1 i== n-l) ? '\n' :' ')

Символ перевода строки записывается посл е каждого десятого э лемента


и после n -го элемента. За всеми остал ьными элементами слеАУет один
пробел. Хотя, возможно, это выглядит му,цреным , бы л о бы
поучител ьным попытаться записать это, не используя условного

выражения.

Упражнение 2-10

Перепишите программу для функции lower , которая переводит


прописные буквы в строчные , используя вместо конструкции if - else
условное выражение.

2,12, Старшинство и порядок вычисления

в приводимой ниже таблице сведены правила старшинства и


ассоциативности всех операций, включая и те , которые мы еще не
обсуждали. Операции , расположенные в одной строке , имеют один и
тот же уровень старшинства; строки расположены в порядке убывания
старшинства. Так, например, операции *, I и % имеют одинаковый
уровень старшинства , ЮJТорый выше, чем уровень операций +и -.

Таблица 2.3.
OPERAТOR ASSOCIAllVIТY

о [] -> . LEFТ то RIGНТ

! л ++ ___ (ТУРЕ)' & SIZEOF RIGHTТO LEFT


• 1% LEFТ то RIGНТ

+- LEFT то RIGНТ

« » LEFТ то RIGНТ

< <= > >= LEFТ то RIGНТ

== 1= LEFT то RIGНТ

& LEFТ то RIGНТ


75
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Л LEFТ то RlGНТ

LEFТ то RlGНТ

&& LEFT то RlGНТ

11 LEFТ то RlGНТ

7: RlGHTТO LEFT
= += -= ЕТс. RlGHTТO LEFT
, (CНAPТER 3) LEFТ то RlGНТ

Операции -> И . Используются для доступа к элементам структур ; они


буАУ'" описаны в лекции No6 вм есте с s i zе о f (размер объекта). В
лекции NQS обсуждаются операции * (ко свенная адресация) и & ( адрес
). Отметим , что уровень cтapllD1HCТBa побитовых логических операций
&, л и ' ни же уровня операций == и ! =. Это приво.ци т к тому, что
осуществляющие побитовую проверку выражения, подобные

if «х & IТ\З s k) == О) ...

Для п олучени я пр ав ил ьных резynьтатов должны заключаться в круглые


скобки.

Как уже отмечалось ранее, выражения, в которые в)[)Дит одна из


ассоциативных и коммутативных операций ( *, +, &, " , ' ), MOгyr
пер егруп пиро в ыв аться, даже если они заключены в круглые скобки. В
БолыlD1с твеe случаев это не при во.цит к каким бы то ни было
р асхождениям ; в си туациях, где такие р асхожде ния в се же возможны,

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


явные пром ежyrочные пер еменные.

в языке 'С", как и в БолыlD1ствеe языков , не фиксируется порядок


вычи сле ния операндов в операторе. Например в о ператоре вида

х= Ю+g() ;

сначала может бы ть вычислено f , а потом g , и н аобо рот; по этому, есл и


либо f, л ибо g и зменяют внеll.lНЮЮ переменн ую, от ЮJТорой зав исит
др уго й операнд, то з начение х может зав исеть от порядка вычислений.
Для обеспечения нужной последовател ьности пр омежyrо чные
ре зynьтаты можно опять зап оминать во временных переменны х.

76
Б.8. К<'рниган, Д. М . Ричи Я ЗЫ К "JIOграм""ирования С

Подобным же образом не ф1ксируется порядок вычисл ения apryмeHToB


функции, так что оператор

printf{'"%d %d\n",++n, power(2, n));

может давать (и действител ьно дает) на разных машинах разные


резynьтаты в зависимости от того , увеличивается ли n до или посл е

обращения к функции powe r. Правильным решением, конечно, являето


запись

++0; printf(' 'Yod %dln", 0,power(2,o));

Обращения к функциям , в л оженные операции присваивания, операции


увел ичения и уменьшения приводят к так наз ываемым ''побочным
э фfx:'ктам" - некоторые переменные изменяются как побочный резynьтат
вычисл ения выражений. В любом выражении , в котором во з никают
побочные э Ф1х"кты, MOryr существовать очень тонкие зависимости от
порядка , в котором определ яются входящие в него переменные.

примером типичной неу,цачной ситуации яв л яется оператор

a[i] = i++;

Возникает вопрос, старое или новое з начение i служит в качестве


индекса. Компилятор может поступать разными способами и в
зависимости от своей интерпретации выдавать раз ные ре зynьтаты. Тот
случай , когда происходят побочные э Ф1х"кты (присваивание
фактическим переменным ), - оставляется на усмотрение компи л ятора,
так как наи лучший порядок сильно зависит от архитектуры машины.

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


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

вас от неприятностей. (Отл адочная про грамма 1 in t укажет


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

77
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Управление потоком

Изучаются средства управления процессом обработки данных.

3 Управление потоком

Управляющие операторы языка определяют порядок вычисл ений. В


приведенных ранее при мерах мы уже встречались с наиболее
употребительными управляющими конструкциями языка 'С"; здесь мы
опиJ.LJeм остальные операторы управления и угочним действ ия
операторов, обсуждавlШ1ХСЯ ранее.

3.1. Операторы и блоки

Такие выражения, как х =о , или i + +, или printf ( ... ) , становятся


операторами, если за ними слеАУет точка с запятой, как, например,

х= О;
i++;
pгint~ ... );

в языке 'С" точка с запятой является признаком конца оператора , а не


разделителем операторов, как в яз ыках типа алгола.

Фигурные скобки { и } используются для объединения описаний и


операторов в составной оператор или блок. так что они оказываются
синтаксически эквивалентны одному оператору. Один явный при мер
такого типа да ют фигурные скобки, в которые закл ючаются операторы,
составляющие функцию, другой - фигурные скобки вокруг группы
операторов в конструкциях if, else, while и for (на самом деле
переменные мосут быть описаны внугри любого блока; мы поговорим
об этом в лекции 4). Точка с запятой никогда не ставится после первой
4игурной скобки, которая завершает блок.

3.2 . IF - ELSE

78
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

Оператор i f - е 1 se используется при необходимости сделать выбор.


Формально синтаксис имеет вид

if (выражение)
оператор-l

еЕе
оператор-2,

Где часть else является необязательноЙ. Сначала вычисляется


выражение ; если оно ''истинно'' /Т.е. значение выражения отлично от
н уля/, то выполняется оператор-l . Если оно ложно /значение
выражения равно нулю/, и если есть часть с else , то вместо
оператора-l выполняется оператор-2 .

Так как i f просто проверяет численное значение выражения, то


во зм ожно некоторое сокращение записи. Самой очевидной
во зм ожностью яв л яется запись

if (выражение)

вместо

if (выражение ! = О)

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


становится загадочной.

То, что часть else в конструкции if else является


необязательной, приводит к двусмысленности в случае, когда else
опускается во вложенной посл едовательности операторов i f. Эта
нео.цно значность разрешается обычным образом - е1 s е связывается с
ближайшим предыду1ЦИМ if , не содержащим else .

Например, в

if(n > O)
Ща>Ь)
z= а;

еЕе
z= Ь;
79
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

конструкция е 1 sе относится к внутреннему i f, как мы и показали,


сдвинув else под соответствующий if . Если это не то , что вы хотите,
то для получения нужного соответствия необходимо испол ьзовать
4иrypные скобки:

if (п > О) {
if (a> b)
z = а;

еЕе
z= Ь;

Такая двусмысленность особенно пагубна в ситуациях типа

if (п > О)
fo г (i = О; i < п; i++)
if(s[i] > О ) {
pгinrf(".. .');
гe Iuтn( i);
}
еЕе /* wгong */
p гintf("e ггo г - n is zeгo\n' );

Запись е 1 зе под i f ясно показывает, чего вы хотите , но компи лятор не


получит соответствующего указания и свяжет else с внугренним if .
Ошибки такого рода очень трудно обнаруживаются.

Между прочим , обратите внимание, что в

if (a > Ь)
z = а;
еЕе
z = Ь;

посл е z =а стоит точка с запятой. Дело в том , что согласно


грамматическим правилам за i f должен сл едовать оператор , а
выражение типа z =a , являющееся оператором , всегда заканчивается

точкой с запятой.
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

3.3 . ELSE - IF

Конструкция

if (выражение)
оператор

еБе if (выражение)
оператор

еБе if (выражение)
оператор

else
оператор

встречается настолько часто, что заслуживает отдельного краткого

рассмотрения. Такая последовательность операторов i f является


наиболее распространенным способом программирования выбора из
нескольких возможных вариантов. выражения просматриваются

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

выполняется относящийся к нему оператор, и этим вся цепочка


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

Последн яя часть с е 1 s е имеет дело со случаем, ЮJгда ни одно из


проверяемых условий не выполняется. Иногда при этом не надо
предпринимать никаких явных действий; в этом случае хвост

else
оператор

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

Для иллюстрации выбора из трех возможных вариантов при ведем


программу ФУНIO.\ИИ , которая методом половинного деления определяет,
находится ли данное з начение х в отсортированном массиве v.
Элементы массива v должны быть расположены в порядке
во з растания. ФУНIO.\ия возвращз:ет номер позиции (число между О и n-l
), в которой значение х находится в v , и -1 , если х не содержится в v.
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

blnary(x, v, п) 1* find х in v(O]... v(n- l ] *1


int Х, v( ], п;
{
int юw, high, mid;

юw = О;
high = n-l;
while (Iow <= high) {
mid = (low + high) 12;
if (х < v(mid ])
high = mid - 1;
else if (х > v(mid ])
ю w = mid + 1;
else /* fo und match */
return(mid);
}
return(- I );
}

ОСНОВНОЙ частью каждого шага алгоритма является проверка, бу,цет ли х


меньше, больше или равен среднему эл ементу v [mid J ; использование
конструкции е 1 se - i f здесь вполне естественно.

3.4. ПереКЛЮЧi!Гель

оператор s wi tch дает специальный способ выбора одного из мно гих


вариантов , который заключается в проверке совпадения значения
данного выражения с одной из заданных констант и соответствующем
ветвлении. В л екции 1 мы привели программу подсчета числа
вхождений каждой циcWы, символов пустых промежугков и всех
остальных символов , использующую последовательность if ... else
i f ... е 1 s е . Вот та же самая программа с переключателем.

mainО /* со urn digits,white space, others */


{
int с, i. nwhite, nother, ndjgit[10];

nwhite = поtПeг = О;
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

for(i = О; i < 10; i++)


rк1igit(i] = О;

while « с= gelchar()) != EOF)


switch (с) {
case ' О':
case '1':
case '2':
case '3':
case '4':
case '5':
case '6':
case '7':
case '8':
case '9':
ndigil(c- 'O']++;
break;
case' ':
case '\n':
case '\t':
nwhite++;
break;
defaull:
nother++;
break;
}
prinrf("digits =');
for (i = О; i < 10; i++)
prinrf(" 0/0<1", ndigil(i]);
prinrf('\nwhite space = %d, other = %d\n",
nwhite, пошег);

Переключател ь вычисляет целое выражение в круглых скобках (в


данной программе - значение символа с ) и сравнивает его з начение со
всеми случаями ( cas e ). Каждый случай должен быть помечен л ибо
целым , либо символьной константой, л ибо константным выражением.
Если з начение константного выражен ия, стоящего после вариантного
префикса case , совпадает со значением целого выражен ия, то
выполнение начинается с этого случая. Если ни один из случаев не

'"
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

ПОДХОДИТ, то выполняется оператор после пре4икса defaul t . Пре4икс


de f а u 1 t является необязательным ,если его нет, и ни один из случаев
не ПОДХОДИТ, то вообще никакие действия не выпол няются. Сл учаи и
выбор по умол чанию MOгyr располагаться в любом порядке. Все случаи
должны быть различными.

Оператор break приводит к немедленному выходу из переключателя.


Поскольку случаи служат только в качестве меток, то если вы не
предпримите явных действий после выполнения операторов,
соответствующих одному случаю , вы провалитесь на следующий
случай. операторы break и return являются самым обычным
способом выхода из переключателя. Как мы обсудим позже в этой
лекции , оператор break можно использовать и для немедленного выхода
из операторов ЦИКJJа while , for и do .

Проваливание скво з ь случаи имеет как свои достоинства, так и


недостатки. К положител ьным качествам можно отнести то , что оно
позволяет связать несколько случаев с одним действием, как было с
пробел ом, табуляцией и новой строкой в нашем примере . Но в то же
время оно обычно приводит к необходимости заканчивать каждый
случай оператором break, чтобы избежать пере.юда к следующему
случаю. Проваливание с одного случая на другой обычно бывает
неустойчивым, так как оно с кл онно к расщеплению при мqд,иф1кации
программы. За исключением, ко гда одному вычисл ению соответствуют
несколько меток, проваливание сл едует использовать умеренно.

Заведите привычку ставить оператор break после последнего случая


(в данном примере посл е defau l t ), даже если это не является
логически необходимым. В один прекрасный день, когда вы добавите в
конец еще один случай , эта маленькая мера предосторожности избавит
вас от неприятностей.

Упражнение З-1

НаПИlШ1те программу для функции expand (s I t ), которая копирует


строку s в t, заменяя при этом символы табуляции и новой строки на
видимые условные последовательности , как \n и \t. используйте
переключател ь.
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

3.5 . ЦИКЛЫ - WНILE И FOR

Мы уже сталкивались с операторами ЦИКJJа while и for . В


конструкции

while (в ы раже н ие )
о пер ато р

вычисляется выражение. Если его з начение отлично от н уnя, то


выполняется оператор и выражение вычисляется снова. Этот ЦИКJJ
продолжается до тех пор , пока з начение выражения не станет нулем,

посл е чего выполнение программы продолжается с места посл е

оператора.

Опе р ато р

for ( в ы р ажение 1; вы р ажени е 2; в ы ражение 3)


о п е р ато р

э квивалентен последовательности

вы р ажени е 1;
while (в ы раже н ие 2) {
о п е р ато р

в ыр аже ни е 3;
}

Грамматически все три компонента в for являются выражениями.


Наиболее распространенным является случай , когда выражение 1 и
выражение 3 являются присваиваниями или обращениями к
функциям, а выра ж ение 2 - усл овным выражением. любая из трех
частей может быть опущена, хотя точки с запятой при этом должны
оставаться. Если отсутствует выражение 1 или выражение 3, то
оно просто выпадает из раСLl.D1рения. Есл и же отсутствует проверка,
выражение 2, то считается, как бу,цто оно всегда истинно, так что

юг (;;) {
Б.8. К<'рниган, Д. М . Ричи Я ЗЫ К "JIOграм""ирования С

является бесконечным циклом, о котором предполагается, что он будет


прерван другими средствами (такими как break или return ).

Использовать ли while или for - это , в основном дел о вкуса.


Например в

while ((е = getehar())


== ' , 11 е == '\n' 11 е == '\t')
/* skjp white space characters */

нет ни инициализации, ни реинициализации, так что цикл whi l e


выгл ядит самым естественным.

Цикл f оr , очевидно, предпочтительнее там , где имеется простая


инициализация и реинициализация, поскол ьку при этом управл яющие

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


цикла. Это наиболее очевидно в конструкции

for (i = О; i< п; i++)

которая яв л яется идиомой яз ыка ''С'' для обработки первых n элементов


массива, анал оrnчной оператору цикла do в фJртране и PL/l .
АналоЛ1Я , однако , не полная, так как границы цикла MOfyr быть
изменены внyrри цикла , а управляющзя переменная сохраняет свое

значение после выхода из цикла , какова бы ни была причина этого


выхода. Поскольку компонентами for MOryr быть произвольные
выражения, они не ограничиваются тол ько арифметическими
прогрессиями. Тем не менее является п л охим стилем включать в for
вычисления, которые не относятся к управлению циклом, лучше

поместить их в управляемые ци кл ом операторы.

в качестве большего по размеру примера приведем другой вариант


функции atoi , преобразующей строку в ее численный эквивалент.
Этот вариант яв л яется более общим ; он доп ускает присyrствие в начал е
символов п устых промежyrков и з нака + или - . (В лекции 4 приведена
функция а to f , которая выполняет то же самое преобразование для
чисел с п л авающей точкой).

Общзя схема программы отражает фJрму поступающих данны х:


Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

• пропустить пустой промежугок, если он имеется


• извлечь знак, если он им еется

• извлечь целую часть и преобразовать ее

Каждый шаг выполняет свою часть работы и оставляет все в


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

ato.(s) /"* convert s to inreger"*/


char s[];
{
int ~ ", sign;
for(i=O;s[i] == ' , 11
s[i]=='\n' 11 s[i]=='\l';i++)
; /"* skip white space "*/
sign = 1;
if(s[i] == '+' 11 s[i] == '-') /* sign */
sign = (s[i++ ]=='+') ? 1 : - 1;
for( n = О; s[ i] >= 'о ' && s[i] <= '9'; i++)
n = 10 "* n + s[i] - ' О ' ;
геrшп(sign "* п);
}

Преимущества ц ентрализации ynравления циклом становятся еще более


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

удаленные элементы, а не смежные, как в обычном методе сорт ир овки.


Это приводи т К быстрому устранению больuюй части
неynорядоченносги и coкpall.fleт последующую работу. Интервал между
элементами постепенно сокращается до единицы , когда сортировка

фактически превращается в метод перестановки соседних элементов.

shell(v, п) /* sort v(О] ... v(П- l ]


into increasing order "* /
int v[], п;
{
int gap, i. j, temp;
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

for (gap = n12; gap > О ; gap 1= 2)


for (i = gap; i < п; i++)
for U=i-gap; j>=O && v(j»v(j +gap); j-=gap) (
tеЩ> = v(j);
v(j) = v(j+gap);
v(j +gap) = (етр;
)
}

Здесь имеются три вложенных цикла. Самый внешний цикл управляет


интервалом между сравниваемыми элементами, уменьшая его от n/ 2
вдвое при каждом ПРОXDДе , пока он не станет равным нулю. Средний
цикл сравнивает каждую пару элементов, раздел енных на величину

интервала; самый внугренний цикл переставляет любую


неупорядоченную пару. Так как интервал в конце концов сводится к
единице, все элементы в результате упорядочиваются правильно.

Отметим, что в силу общности конструкции for внешний цикл


укладывается в ту же самую qюрму, что и остальные, хотя он и не
яв л яется ари~етической прогрессиеЙ.

Последней операцией языка "С" является запятая ",", которая чаще всего
используется в операторе for . Два выражения , разделенные запятой,
вычисляются слева направо, причем типом и значением результата

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


различные части оператора for можно включить несколько
выражений , например, для паралл ельного изменения двух индексов.
Это иллюст рируется функцией reverse (S) , ЮJТорая располагает
строку 5 в обратном порядке на том же месте.

reverse(s) /* reverse string s in place */


char s[];
(
int с, i. j;

for(i = О , j = strlen(s) - 1; i < j; i++, j--)


с = s[i);
s[i] = sUJ;
sUJ = с;
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Запятые , которые разделяют apryмeHTЫ функций, переменные в


описаниях и Т.д. , не имеют отношения к операции запятая и не

обеспечивают вычислений слева направо.

Упражнение З-2

Составьте программу для функции expand(51 , s2) , которая


расLШ1РЯет сокращенные обозначения вида а - z из строки s 1 в
эквивалентный полный список авс ... xyz в 52 . Допускаются
сокраl.l.fНИЯ для строчных и прописных букв и циcW. Будьте готовы
иметь дело со случаями типа а-в-с , a-zО-9 и - a-z . (Полезное

соглашение состоит в том, что символ -, СТОЯЩИЙ В начале и л и конце,


воспринимается буквально).

3 . б . ЦИКЛ ОО - WНILE

Как уже отмечал ось в л екции 1, циклы while и for обл адают тем
приятным свойством, что в них проверка окончания ОС)'ll.f'ствляется в
начале , а не в конце цикла. Третий оператор цикла языка "С ", do-
whi le , проверяет условие окончания в конце, после каждого прохода
через тело ЦИКJJа ; тело цикла всегда выполняется по крайней мере один
раз. Синтаксис этого оператора имеет вид:

do
оператор while (выражение)

С начала выполняется оператор, затем вычисляется выражение. Если


оно истинно, то оператор выполняется снова и Т.д. Если выражение
становится ложным, цикл заканчивается.

Как и можно было ожидать, цикл do-while используется значительно


реже, чем while и for , составляя примерно пять процентов от всех
циклов. Тем не менее, иногда он оказывается полезным , как, например ,
в слеДУЮI.l.fЙ ФУНIO.\ии i toa , КDторая преобразует число в символьную
строку (обратная фунIO.\ИИ а toi ). Эта задача оказывается несКDЛЬКО
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

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

itoa(n,.s) /"'convert n {о characters in 5 "'/


char s[];
in! п;
{
in! ~ sign;

if « sign = п) < О) /* record sign */


n = -п; /* rrnke n positive "'/
i = О;
do { /* generate djgits in reverse order */
s[i+ +] = n % 10 + '0';/'" get neх! djgit */
} while ((п /= 10) > О) ; /* delete it */
if (sign < О)
s[i++] = '-'
s[i] = '\0';
reverse(s);
}

Цикл do-while здесь необходим , или по крайней мере у,цобен,


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

Упражнение 3-3

При представлении чисел в ДВО ИЧНОМ дополнительном коде наш


вариант i toa не справляется с наиБОЛЫllИМ отрицательным числом,
Т.е. со значением n равным -2 в степени M-l, где м размер слова.

объясните почему. Измените программу так, чтобы она правильно


печатала это значение на любой MaUМHe.

Упражнение 3-4
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Напишите аналогичную функцию i tob ( n , s ) , которая преобразует


целое без знака n в его двоичное символьное представление в s.
Запрограммируйте функцию i to h , которая преобразует целое в
шестнадцатеричное представление.

Упражнение З-5

Напишите вариант i toa , который имеет три , а не два аргумента.


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

3.7. Оператор BREAК

Иногда бывает у,цобным иметь возможность управлять выходом из


цикла иначе, чем проверкой услов ия в начале или в конце. Оператор
b rea k позволяет выйти из операторов for , wh ile и do до окончания
цикла точно так же, как и из переключател я. Оператор Ь r e a k приводит
к немедленному вы.хо.цу из самого внутреннего охватывающего его

цикла (или переключателя).

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


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

#dеfiлe mзxlinе 1000


mainО /* rermve trailing blanks and tabs */
(
int п;
char linе [ mзxlinе) ;

while ((п = gеtlinе(linе,mзxline)) > О) {


while (--п >= О)
"(linе[п) != " && line[п) != 'It'
&& line[п) != '\n')
break;
line[п+ 1) = '\0';
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

printf('%s\n", line);
}
}

функция getline возвраl.Lflет длину строки. Внугренний цикл


начинается с последнего символа line (напомним, что --п уменьшает
n до использования его значения) и движется в обратном направлении
в поиске первого символа , который отличен от пробела, табуnяции или
новой строки. Цикл прерывается , когда либо найден таЮJЙ символ , либо
n становится отрицательным (т.е. , когда просмотрена вся строка).
С оветуем вам убедиться, что таЮJе поведение правильно и в том случае,
ЮJгда строка состоит ТОЛЬЮJ из символ ов п устых промежугков.

в качестве альтернативы к break можно ввести проверку в сам цикл:

while ((п = getline(line,maxline)) > О) {


while ( --п >= О
& & (linе[ п] == ' , Il line[n] == '\,'
Illine[n] == '\n'))

Это ycтynaeт предыдyI.Цf'МУ варианту, так как проверка становится


тру,цнее для понимания. Проверок, ЮJторые требуют переплетения &&,
I 1, ! и кругл ых скобок, по воз можности сл едует избегать.

3.8 . Оператор CONТINUE

Оператор continue родcrsеиеи оператору break, ио используется


реже; он приводит к начал у слеАУЮщей итерации охватывающего цикла
( for , while , do ). В циклах while и do это означает
непосредственный переход к выпол нению проверочной части; в цикле
for управ л ение передается на ша г реинициализации. (оператор
continue применяется ТОЛЬЮJ в циклах. но не в пере кл ючателях.
оператор continue внугри переключателя внугри цикла вы з ывает
выполнение слеАУЮщей итерации цикла).
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

в качестве примера приведем CЙJагмент, который обрабатывает только


положительные элементы массива ,
а' отрицательные значения

пропускаются.

for (i = О; i < п; i++) {


if (a[i] < О) /* skip negative е lеm:'Пts */
continue;
... /* do positive e""ments */

оператор continue часто используется, когда последующая часть


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

Упражнение З-6

НаПИlШ1те программу копирования ввода на вывод, с тем исключением,


что из каждой группы посл едовател ьных одинаковых строк выводится
тол ько одна. (Это простой вариант ути л иты uпiq систем UNIX).

Оператор GOTO и метки

в языке 'С" предусмотрен и оператор goto , которым бесконечно


ЗЛ ОУТlОтребляют, и метки для ветв л ения. С формальной точки з рения
оператор goto нико гда не яв л яется необходимым, и на практике почти
все гда можно обойтись без него. Мы не использовали goto в этом
курсе.

Тем не менее, мы укажем несколько ситуаций , где оператор goto может


найти свое место. Наиболее характерным является его испол ьзование
то гда, когда нужно прервать выполнение в некоторой глубоко
вл оженной структуре , например , выйти сразу из двух циКJJОВ. Здесь
нельзя непосредственно испол ьзовать оператор break, так как он
прерывает тол ько самый внутренний цикл. Поэтому:

for( ... )
Ю r ( ... )
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

~ (disaSler)
gO(Q erтoг;

егroг:

сleал up the mess

Если лрограмм а обработки Оll.lИбок нетривиал ьна и Оll.lИбки могуг


во зникать в несюльких местах, то такая организация оказывается

удобной. Метка им еет такую же qюрму, что и имя переменной , и за ней


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

в качестве др утого примера р ассмотри м задачу нахождения пе рво го


отрицательного эл емента в двумерном массиве. (Многомерные
массив ы ра ссмат рив аются в л екции лекц ии 5). Вот одна из
во з можностей:

for(i = О; i < л; i++)


far U= О; j < m; j++)
~ (v[iJUJ < О)
gO(Q fоШ1d;
/* dkl.n't firX:I */

[aurк!:
/* foLll1d оле а! ро s itioл i. j */

Программа, И С ПОЛ ЬЗУЮ J..ЦЗя оператор goto , всегда может быть написана
без него , хотя, возможно , за счет повторения некоторых проверок и
в ведения дополнительных пе рем енных. Наприм е р , программа пои ска в
массиве примет в ид:

[aurк! = О;
for (i = О; i < n && !foLll1d; i++)
far U= О; j < m && !faurк!; j++)
faund = v[i]UJ < О;
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

if (fourк1)
/* it was at i-l , j- l */

еЕе
/* rюt foLll1d */

Хотя мы не являем ся в этом вопро се догматикам и , нам все же кажется,


что если и нужно использов ать оператор goto, то весьма умеренно.
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Функции и структура про грамм

Дается описание и работа с функциями , рассказывается о структуре


программ.

4. Функции и структура программ

Функции разб и вают БОЛbl.LIИ е вычислительные задачи на маленькие


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

Язык ''С'' раз рабатывался со стремлением сделать функц ии


э фfx:'ктивными и у,цоб ными для использования; ''С''- программы обычно
состоят из болыl.юо числа маленьких функций , а не из нескол ьких
БОЛbl.LIИ Х. Программа м ожет размещзться в одном или нескол ьких
ИСХОДНЫХ фзйлах любым у,цобным образом; И О.Dдные фзй лы могуг
комп и лир оваться отдельно и загружаться вместе наряду со

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


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

используем ой системы.

БОЛЫllИ.НСТВО программистов )[)рошо знакомы с ''библиотечными''


функциями для ввода и вывода
/ getchar , putchar / и для
численных расчетов / sin, cos , sqrt /. В это й лекции мы со общим
больше о написании новых функций.

4.1. Основные сведения

Для начала давайте разработаем и составим программу печати каждой


строки ввода , которая содержит определенную комбинац и ю символов. /
Это - специальный случай yrи литы grep системы " UN IX "/.
Напр и мер, при поиске комб ин ац ии " the " в наборе строк
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

now ic; the tirrI"


for all good
rneп to соrne to the аю
of their party

в качестве вы)[)Да получим

now ic; the tirrI"


rneп to соrne to the аю
of their party

основная схема выпол нения задания четко разделяется на три части:

while (и меется el.Lf' строка)


if (стро ка соде ржит н ужную комбинацию)
вывод этой ст роки

Конечно, возможно запрограммировать все действия в виде одной


основной процедуры, но лучше использовать естественную структуру
задачи и представить каждую часть в виде отдельной фующии. С тремя
маленькими кусками легче иметь дело, чем с одним БОЛЫlIИ.М, потому
что отдельные не относящиеся к C)ll..ЦeCТBY дела детали можно включить

в функции и уменьшить возможность нежелател ьных взаимодействий.


Кроме того, эти куски могут оказаться полезн ыми сами по себе.

'Пока имеется еще строка" - это getline , функция, которую мы


запрограммировали в лекции N~ l, а "вывод этой строки" - это функция
printf, которую уже кто-то подготовил для нас. Это з начит, что нам
осталось только написать процедуру для определения, содержит ли

строка данную комбинацию символов или нет. Мы можем решить эту


проблему, позаимствовав разработку из PUl: функция index (з I t)
во зв ращзет позицию, или индекс, строки з , где начинается строка t, и

-1 , если s не содержит t. В качестве начал ьной позиции мы


используем О , а не 1, потому что в языке 'С" массивы начинаются с
позиции н уль. Когда нам в дал ьнейшем понадобится проверять на
совпадение более сложные конструкции, нам придется заменить только
функцию index ; остальная часть программы останется той же самой.

После того, как мы потратили столько усилий на разработку, написание


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

"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

целиком вся программа, так что вы можете видеть, как соединяются

вместе отдельные части. Комбинация символо в , по которой


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

выпол нения программы. Программа также содержит новый вариант


функции getline ; вам может оказаться полезным сравнить его с
ва ри антом из лекции N21.

#dеfiлe JТВxIine 1000


mainО /* find allline5 matching а pattern */
{
ehar fiлe[JТВxIine];

while (getline(line, таxline) > О)


if (indех(fiлe , '\he'j >= О)
printf('%5", line);

gerline(5, lim) /* get linе into 5, геtШ11leпgth *


ehar s[];
irnlim;
(
int с, ~

i= О;
while(--lim> О && (e=getehar()) != ЕОР && е != '\n')
5[i++ ] = с;
"(е == '\n')
s[i++ ] = е;
s[i] = '\0';
гешrn(О;
}

index(5,t) /* return indех of t in 5,- 1 if попе */


ehar s[], tП ;
{
int i, j, k;
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

fo г (i = О ;
5[i] != '\0'; i++) (
fo гU = \ k=O; t[k) ! ='\О' && 50 ) == t[k) ; j ++; k++)

if (t[k) == '\0')
гetuгn(~ ;
)
гe tuгn( -1);

Каждая функция имеет вид имя ( список аргументов, если они имеются)
описания аргументов, есл и он и имеются

о пи сания и о п е р ато ры , есл и о н и и меются

Как и указывается, некоторые части мосут отсутствовать; минимальной


функцией является

dummy О { }

которая не совершает никакихдействий.

{[акая ничего не делаюLL.flЯ функция ино гда оказывается у,цобной для


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

Программой является просто набор определений отдельных функций.


С вязь между функциями осуществ л яется через аргументы и
возвращземые функциями значения /в э том случае/; ее можно также
осуществлять через внеll.lНие переменные. функции могут располагаться
в ис.ходном файле в любом порядке, а сама исходная программа может
размещзться на нескол ьких файлах, но так, чтобы ни одна функция не
расщеп л ялась.

Оператор return служи т механизмом для возвраl.Lf'НИЯ значения из


вызванной функции в функцию , которая к ней обрати л ась. За return
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

может следовать любое выражение:

Ге(ШП (выражение)

Вызывающзя функция может игнорировать возвращземое значение,


если она этого пожелает. Более того, после return может не быть
вообще никакого выражения; в этом случае в вызывающую программу
не передается никакого з начения. )hравление также возвращзется в
вызывающую программу без передачи какого-либо значения и в том
случае, когда при выполнении мы ''nроваливаемся'' на конец функции,
достигая закрывающейся правой фиrypной скобки. Если функция
во зв ращзет значение из одного места и не возвращзет никакого

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


признаком каких-то неприятностей. В любом случае "значением"
функции, которая не возвращзет значения, несомненно бу,цет мусор.
Отладочная программа lint проверяет такие ошибки.

Механика компиляции и загрузки "С"- программ, расположенных в


нескольких исходных файлах, меняется от системы к системе. В системе
"UNIX ", например , эту работу выполняет команда' се ., упомянyrая в
ле кции NQ1. Предположим, что три функции находятся в трех различных
фай лах с именами main . с , getline . с и index . с . Тогда команда

сс maт.С getline.c indех.С

компилирует эти три файла , помещзет полученный настраиваемый


объектный код в фай лы main . о , getline . о и index . о и загружает
их всех в выполняемый фай л, называемый а . out .

Если имеется какая-то ошибка, скажем в main . с , то этот файл можно


перекомпилировать отдельно и загрузить вместе с предыAYIЦИМИ

объектными файлами по команде

сс maт.С getline.o mdex.O

Команда' с с ' использует соглашение о наименовании с " .с" и " .о"


для того, чтобы отличить исходные файлы от объектных.

)hражнение 4-1

'00
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

С оставьте программу для функци и r index (s t ) , которая возвращает


I

позицию самого правого вхождения t в s и -1, если s не содержит t .

4.2. Функции, возвращающие нецелые значения

До сих пор ни одна из наlШ1Х программ не содержала какого-либо


описания типа функции. Дело в том, что по умолчанию функция неявно
описывается своим появлением в выражении или операторе, как,

например , в

while (getline(line, maxliлe) > О)

Если некоторое имя, которое не было описано ранее, появ ляется в


выражении и за ним следует левая круглая скобка, то оно по контексту
считается именем некоторой функции. Кроме того, по умолчанию
предполагается, что эта функция возвращает значение типа i n t . Так как
в выражениях char преобразуется в in t , то нет необходимости
описывать функции, возвращающие char . Эти предположения
покрывают большинство случаев, включая все приведенные до сих пор
примеры.

Но что проиоццит, если функция должна возвратить значение какого-то


другого типа? Многие численные функции , такие как sqrt , sin и соз
во зв ращают double ; другие специальные функции возвращают
значения д ругих типов. Чтобы показать, как постynать в этом случае,
давайте напишем и используем функцию а tof (з) , которая преобразуеl
строку s в эквивалентное ей плавающее число двойной точности.
функция atof является расширением atoi , варианты которой мы
написали в лекции No2 и NQЗ; она обрабатывает необязательно з нак и
десят ичную точку, а также целую и дробную часть, каждая из которых
может как присугствовать , так и отсугствовать'/Эта процедура
преобразования ввода не очень высокого качества; иначе она бы заняла
больше места, чем нам хотелось бы/о

Во-первых, сама atof должна описывать тип возвращаемого ею


значения, поскол ьку он отличен от int . Так как в выражениях тип
f l oat преобразуется в double , то нет никакого смысла в том, чтобы

'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

ato f возвращала float мы можем с равным успехом

воспользоваться дополнительной точностью, так что мы полагаем, что


возвращаемое значение типа doubl e . Имя типа должно стоять перед
именем функции, как показывается ниже:

double atof(s) /* сопvеrt string s {о double */


char s[ ];
(
double vaL power;
int i, sign;

for(i=O; s[i]==' , 11 s[i]=='In' 11 s[i]=='\t'; i++)


/* skip white space * /
sign = 1;
if(s[i] == '+' 11 s[i] == '-') /* sign */
sign = (s[i++ ] == '+') 7 1 :-1;
for (уа l = О; s[i] >= 'о' && s[i] <= '9'; i++)
va! = 10 * va! + sO] - 'О';
if(s[i] == '.')
i++;
for (power = 1; s[i] >= ' о' && s[i] <= '9'; i++) {
va! = 10 * va! + sO] - 'О';
power "'= 10;
}
return(sign '" val / power);
}

Вторым , но столь же важным, является то, что вызывающая функция


должна объявить о том, что atof возвращает значение, отличное от
i n t типа. ТаЮJе объявление демонстрируется на примере следующего
примитивного настольного калькулятора /едва ПРИГодНОГО для
подведения баланса в чековой книжке/, ЮJторый считывает по одному
числу на строку, причем это число может иметь знак, и складывает все

числа, печатая сумму после каждого ввода.

#dеfiлe mзxlinе 100


mainО /* rudiIп>пrаry desk саllшlatог */
{
double s uщ аtоЮ;

>О,
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

char line[maxline];

sшn = О;
while (getline(line, maxline) > О)
ргiлtf(' \l%. 2 fIn",swn+=alo ~linе ));
}

описание

double SW11, аlОЮ;

говорит, что sum является переменной типа double , и что atof


является функцией , возвращающей значение типа double . Эта
мнемоника означает, что значениями как sum, так и atof ( ... )
яв л яются плавающие числа двойной точности.

Если функция а t о f не бу,цет описана явно в обоих местах, то в 'С"


предполагается, что она возвращает целое значение, и вы получите

бессмысленный ответ. Если сама atof и обраl.lf'ние к ней в ma i n


имеют несовместимые типы и находятся в одном и том же файле , то это
будет обнаружено ЮJмпилятором. Но если atof была СЮJмпилирована
отдельно /что более вероятно/, то это несоответствие не бу,цет
заф1ксировано, так что а tof будет возвращать значения типа double ,
с ЮJторым ma i n будет обращаться, как с int , что приведет к
бессмысленным резynьтатам. / Про грамма lint вылавливает эту
ошибку/.

Имея atof , мы, в принципе , могли бы с ее помощью написать atoi


(преобразование строки в i n t ):

ato(s) /* convert string 5 to integer */


char s[];
{
double аlОЮ;

return(alO~s));
}

Обратите внимание на структуру описаний и оператор retu r n .


>03
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Значение выражения в

Ге(ШП (в ыр ажен и е )

все гда преобразуется к типу функции перед выполнением самого


возвращения. Поэтому при появлении в операторе r e tu r n значение
функции atof , имеющее тип
double , автоматически преобразуется в
i n t , поскольку функция atoi возвращает i nt . (Как обсуждалось в
ле кци и No2 , преобразование значения с плавающей точкой к тип у i n t
осуществляется посредством отбрасывания дробной части).

Упражнение 4-2

PaCll.В1pbTe atof таким образ ом , чтобы она могла работать с числами


вида

1 23. 4 5е-6

где за числом с плавающей точкой может следовать ' е ' и показател ь


э кспоненты , во з можно со знаком.

4.3 . Еще 06 аргументах функций

в лекции No l мы уже обсуждали тот фзкт , что аргументы функций


передаются по з начению , Т.е. вы з ванная функция получает свою
временную копию каждого аргумента , а не его адрес. Это означает, что
вызванная функция не может воздействовать на исходный аргумент в
вызывающей функции. Внутри функции каждый аргумент по существу
яв л яется локальной переменной , которая инициал изируется тем
значением , с КDTOPЫM к ЭТОЙ функции обратились.

Если в качестве аргумента функции BbIcтynaeт имя массива , то


передается адрес начала этого массива; сами э лементы не копируются.

функция может изменять элементы массива , используя индексацию и


адрес начала. Таким образом, массив передается по ссылке. В л екции
NQS мы обсу,цим, как использование указател ей позволяет функциям
воздействовать на отличные от массивов переменные в вы з ывающих
функциях.

'"
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

Между прочим, не существует полностью у,цовлетворительного способа


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

Обычно со случаем переменного числа аргументов безопасно иметь


дел о , если вы з ванная функция не использует аргументов, которые ей на
самом деле не были переда ны , и если типы согласуются. Самая
распространенная в языке "С" функция с переменным числом -
printf . Она получает из первого аргумента инqюрмацию,
позволяющую определ ить количество остальных аргументов и их типы.

функция р r i n t f работает совершенно неправильно, если вызывающая


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

Если же типы аргументов известны , то конец списка аргументов можно


отметить, используя какое-то соглашение; например , считая, что

некоторое специальное значение аргумента (часто нуль) является


признаком конца аргументов.

4.4. Внешние переменные

Программа на языке 'С" состоит из набора внеlШlИХ объектов, которые


являются либо переменными, л ибо функциями. Термин ' 'внеllПiИЙ''
используется главным образом в противопоставление термину
''внугренний'', которым описываются аргументы и автоматические
переменные, определеные внугри функций. ВнеlШlие переменные
определены вне какой- л ибо функции и, таким образом, потенциально
достyn ны для многих функций. Сами функции всегда являются
внеl.LlНИМИ , потому что прави ла языка 'С" не разрешают определять
одни функции внугри других. По умолчанию внеll.lН.ие переменные
являются также и " глобальными ", так что все ссылки на такую
>05
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

переменную, использующие ОДНО и то же имя (даже из функций,


скомпилированных независимо), будуг ссылками на одно и то же. В
этом смысле внеl.LIНие переменные аналоrnчны переменным common в

ф:Jртране и ex t e rnal в РИl. Позднее мы покажем, как определить


внеl.LIНие переменные и функции таким образом, чтобы они были
доступны не глобально, а только в пределах одного ис.ю.цного файла.

В силу своей глобал ьной доступности внеl.LIНие переменные


предоставляют другую, отличную от аргументов и возвращаемых

значений, возможность для обмена да нными между функциями. Если


имя внеl.LIНеЙ переменной каким-либо образом описано, то любая
функция имеет доступ к этой переменной , ссылаясь к ней по этому
имени.

В случаях, когда связь между функциями осуществляется с помощью


большого числа переменных, внеl.LIНие переменные оказываются более
удобными и эфtx:>ктивными, чем использование длинных списков
аргументов. Как, однако, отмечалось в л екции No21, это соображение
следует и спользовать с определенной осторожностью, так как оно
может плохо отразиться на структуре программ и приводить к

программам с больU.D1М числом связей по данным между функциями.

Вторая причина использования внеl.LIНИХ переменных связана с


инициализацией. В частности , внеl.LIНие массивы MOгyr быть
инициализированы , а автоматические нет. Мы рассмотрим вопрос об
инициализации в конце это й лекции.

Третья причина использования внеl.LIНИХ переменных обусловлена их


областью действия и временем существования. Автомати ческие
переменные являются внугренними по отношению к функциям; они
возникают при вюде в функцию и исчезают при выюде из нее.
Внеl.LIНие переменные, напротив, существуют постоянно. Они не
появляются и не исчезают, так что MOгyr сохранять свои значения в

период от одного обращения к функции до другого. В силу этого, если


две функции используют неЮJторые общие данные, причем ни одна из
них не обращается к другой , то часто наиболее удобным оказывается
хранить эти общие да нные в виде внеl.LIНИХ переменных, а не
передавать их в функцию и обратно с помощью аргументов.

>О,
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Давайте продолжим обсуждение этого вопроса на большом примере .


Задача бу,цет состоять в написании другой программы для калы<улятора,
лучшей, чем предыдущая. Здесь допускаются операции +, - , * , I и знак
= (для выдачи ответа). Вместо инф1ксного представления калькулятор
будет использовать обратную пол ьскую нотацию, поскол ьку ее
несколько легче реализовать. В обратной польской нотации знак следует
за операндами; инф1ксное выражение типа

(1- 2)*(4+5)=

записывается в виде

12-45+ * =

Круглые скобки при этом не нужны

Реализация оказывается весьма простой. Каждый операнд помещзется в


стек; когда поступает знак операции , нужное число операндов (два для
бинарных операций) вынимается, к ним применяется операция и
результат направляется обратно в стек. Так в приведенном выше
примере 1 и 2 помещзются в стек и затем заменяются их разностью, -1.
После этого 4 и 5 вводятся в стек и затем заменяются своей суммоЙ,9.
Далее числа -1 и 9 заменяются в стеке на их произведение, равное -9.
Операция = печатает верхний элемент стека, не удаляя его (так что
промежyrочные вычисления могут быть проверены).

С ами операции помещения чисел в стек и их извлечения очень просты,


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

while( п оступает о п е р ация и л и о п е р анд, а н е ко н е ц)


if ( чи сло )
п омести ть е го в сте к

el5e if ( о п е рация)
в ын ут ь о п ер ан ды из сге ка

в ып олнить о п е р ацию

>О ,
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

п оместить резуnътат в стек

ебе
о ши бка

Основной вопрос , который еще не бы л обсужден, заключается в том, где


поместить стек, т. е. какие процедуры CMOгyr обращаться к нему
непосредственно. Одна из таких возможностей состоит в помеl.Lf'НИИ
стека в та i n и передачи самого стека и текущей поз иции в стеке
функциям , работающим со стеком. Но функции та i n нет необходимости
иметь дел о с переменными, ynравляющими стеком ; ей естественно
рассуждать в терминах помещения чисел в стек и извлечения их отту,ца.

В силу этого мы решили сделать стек и связанную с ним июJюрмацию


внеl.LlНИМИ переменными , достynными функциям ри s h (помеl.Lf'ние в
стек) и рор (извлечение из стека), но не main .

Перевод этой схемы в программу достаточно прост. Ведущз:я программа


является по существу большим переключателем по тип у операции или
операнду; это, по-видимому, более характерное применение
переключател я, чем то , которое бы л о продемонстрировано в л екции
NQЗ .

#define rrn хо р 20 /* rrnх siz.e of о р егаnd , operator */


#define пUIТiJег 'о' /* signal that пwnb е г fo Lll1d */
#define toobig '9' /* signa! that string is {оо blg */

rrninО /* reverse polish desk calcu\ator */


(
int tupe;
char s[maxop ];
double op2,atof(),pop(), pushQ;

while ((tupe=getop(s,maxo p)) !=EOF);


switch(tupe) (
case пшnb ег:
push(atof(s));
break;
case '+':
push(pop()+pop());
break;
>О,
Б.8. К<'рниган, Д.М. Ричи Язык "JIOграм""ирования С

case '*':
push(popO*pop());
break;
case'-':
ор2 = рорО;
push(poPO-ор2);
break;
case 'f :
ор2 = рорО;
if(op2 != 0.0)
push(poPO/op2);
el5e
ргiлtf("zего divisor popped\n');
break;
case '=':
ргiлtf(' \l%I\n",рush(рорО));
break;
case 'с':
clear();
break;
case toobig:
prinrf('"%.20s ... is too kюg\n",s);
break;
}
}
#define rrnxva! l OO /* rrnxlmumdepth ofva! stack */

int Sp = О; /* stack pointer */


double vanrrnxval]; /*value stack */
double push(f) /* push f опro value s(ack */
double f,
{
if (Sp < maxval)
relurn(Va~Sp ++ ] =1);
el5e {
printf{"error: stack fUlI\n');
clear();
relurn(O);

>О,
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

double Р О РО /"* ро р {о р value from steack "*/


(
if(sp > О)
relurn( va~ --sp J);
else (
printf{"error: stack еmptу\п' 1 ;
с /е ю"О;
relurn(O);
}
}

c/eal\) /"* clear stack "* /


{
sp=O;
}

Команда С очищает стек с помощью функции c l ea r , ЮJторая также


используется в сл учае ошибки функциями push и рор . К функции
g e top мы очень СЮJро вернемся.

Как уже говорилось в л екции No l , переменная является внеl.LIН.еЙ , есл и


она определена вне тела каЮJЙ бы то ни было функции. Поэтому стек и
указатель стека, ЮJторые должны использоваться функциями push, рор
и cl e ar , определены вне этих трех функций. Но сама функция mai n Hf
ссылается ни к стеку, ни к указателю стека - их участие тщательно

замаскировано. В силу этого часть программы, соответствующая


операции =, использует ЮJнструкцию

push(popQ);

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


его.

Отметим также, что так как операции + и * ЮJммyrативны, порядок, в


ЮJтором объединяются извлеченные операнды, несущественен , но в
случае операций - и / необходимо разл ичать левый и правый
операнды .
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Упражнение 4-3

Приведенная основная схема допускает непосредственное раСlШ1рение


возможностей калькyn.ятора. Включите операцию деления по модулю
/ %/ и унарный минус. Включите ЮJманАУ "стереть", ЮJторая у,цаляет
верхний элемент стека. Введите ЮJманды для работы с переменными. /
Это просто, если имена переменных бy,zwr состоять из одной буквы из
имеЮЩИ)<J:Я двадцати шести букв /.

4.5. Правила, определяющие область действия

Функции и внеll.nше переменные, входящие в состав "С"- программы , не


обязаны ЮJмпилироваться одновременно; программа на исходном языке
может располагаться в нескольких фаЙлах. и ранее скомпилированные
процедуры могут загружаться из библиотек. Два вопроса представляют
интерес:

1. Как сл едует составлять описания, чтобы переменные правильно


воспринимались во время ЮJмпиляции?
2. Как следует составлять описания, чтобы обеспечить правильную
связ ь частей программы при загрузке?

4.5.1. Область действия

Областью действия имени является та часть программы , в ЮJторой это


имя определено. Для автоматической переменной, описанной в начал е
функции, областью действия является та функция, в ЮJторой описано
имя этой переменной, а переменные из разных функций, имеющие
одинаЮJвое имя, считаются не относящимися друг к APyry. Это же
справедливо и для аргументов функций.

Область действия внешней переменной простирается от точки , в


ЮJторой она объяв л ена в исходном файле, до ЮJнца этого файла.
Например , если val , sp, pus h, РОР и clea r определены в одном
файле в порядке, указанном выше, а именно:

int sp = О;

ш
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

double va~maxvalJ ;

double push(Q {... }

double РОРО {... }

clear() {... }

то переменные val и sp можно использовать в pus h , рор и clear


прямо по имени; никакие дополнительные описания не нужны.

с друтой стороны, если нужно сослаться на внеlШlЮЮ переменную до


ее определения, или если такая переменная определена в файле ,
отличном от того, в котором она используется, то необходимо описание
e x te r n.

Важно разл ичать описание внеll.lНеЙ переменной и ее определение.


описание указывает свойства переменной /ее тип, размер и т.д .!;
определение же вызывает еще и отведение памяти. Если вне какой бы
то ни было функции появляются строчки

int sp;
double va~maxva lJ;

то они определяют внеll.lНие переменные sp и val , вызывают


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

ежеrn int sp;


ежеrn double valf];

описывают в остальной части этого исходного файла переменную зр ка!


i nt , а val как массив типа double /ра зме р которого указан в другом
месте/, но не создают переменных и не отводят им места в памяти.

Во всех фаЙлах. составляющих исходную программу, должно


содержаться только одно определение внеlШlей пере мен ной ; другие
файлы мосут содержать описания е х te r n для доступа к ней. / описание
e x te r n может иметься и в том файле, где находится определение /.
Любая инициализация внеlШlей переменной проводится только в
ш
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

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


в описании ex ter n этого можно не делать.

Хотя подобная организация приведенной выше программы и


маловероятна, но va l и sp могли бы быть определены и
инициализированы в одном файле , а функция push, рор и clear
определ ены в другом. В этом случае для связи бы л и бы необходимы
следующие определ ения и описания:

в файле 1:

int sp = О ; /* stack pointer */


double va~maxval) ; /* value stac k */

в фа й ле 2:

extern int sp;


extern double ya~ ];

doub le push(Q {... }

doub le РОРО {... }

cleaI\J {... }
так как описания extern ' в файле l' находятся выше и вне трех
указанных функций, они относятся ко всем ним ; одного набора
описаний достаточно для всего ' файла 2'.

Для программ больuюго раз мера обсуждаемая по зже в этой л екции


во з можность включения файлов , #" inc l ude , позволяет иметь во всей
программе только одн у копию описаний extern и встав лять ее в
каждый исходный фай л во время его компиляции.

Обратимся теперь к функции getop, выбираюl.Ц"Й из фай л а ввода


следующую операцию или операнд. Основная задача проста:
пропуcrить пробел ы, знаки табул яции и новые строки. Если следующий
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

символ отличен от циcWы и десятичной точки, то возвратить его. В


противном случае собрать строку циcW !она может включать
десятичную точку! и возвратить numb e r как сигнал о том, что выбрано
числ о.

процедура существенно усл ожняется, если стремиться правильно

обрабатывать ситуацию, когда вводимое число оказывается слишком


дл инным. функция getop считывает циcWы подряд !возможно С
десятичной точкой ! и запоминает их. пока последовательность не
прерывается. Если при этом не происходит переполнения , то функция
возвращзет numbe r и строку ци<W. Если же числ о оказывается
слиl.LIкDм длинным , то getop отбрасывает остальную часть строки из
файла ввода, так что пользователь может просто перепечатать эту
строку с места ошибки; функция возвращзет toobig как сигнал о
переполнении.

gerop(s, lim) !* get next oprerator о г operand *!


ehar s[];
iлt Iim;
(
int i. с;

while« e=getehQ)== ' '1 1 e=='\t' 11 е== '\n')

if(e! = '.' & & (е < ' о ' 11 е > '9'))


return(e);
s[O] = е;
for(i= l ; (e=getehar()) >='0' & & е <= '9'; i++)
if (i < lim)

s[i] = е;
if (с == '.') { !* collect fraction *!
if (i < lim)
s[ i] = е;
for(i++;(e=getehar()) >='0' & & e<='9';i++)
if (i < lim)
s[i] = е;

if (i < lim) { !* number i<; ok *!

'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

ungeleh(e);
s[i] = '\0';
return (пштЬег);

} el5e { /* it' s too blg; skjp res( of line */


while (е != '\n' && е != EOF)
е = gelehaI\);
s[lim-l] = '\0';
гelurn (lOOblg);
}

Что же представляют из себя функции' getch ' и ' ung e tc h '? Часто
так бывает, что программа, считывающая входные данные, не может
определить, что она прочла уже достаточно, пока она не прочтет

слиll.lкDм много. Одним из примеров является выбор символов,


составляющих число: пока не появится символ, отличный от цифры ,
числ о не закончено. Но при этом программа считывает один лиll.lНИЙ
символ, символ, для которого она еще не подготовлена.

Эта проблема была бы решена , если бы было бы возможно ''прочесть


обратно" нежелательный символ. Тогда каждый раз, прочитав лиll.lНИЙ
символ, программа могла бы поместить его обратно в файл ввода таким
образом, что остальная часть программы могла бы вести себя так,
словно этот символ никогда не считывался. к счастью, такое

неполучение символа легко иммитировать, написав пару действующих


совместно функций. функция getc h доставляет следующий символ
ввода , подлежащий рассмотрению; функция ungetc h помещает символ
назад во ввод, так что при следующем обращении к getch он будет
во зв ращен.

То, как эт и функции совместно работают, весьма просто. функция


u n getc h помещает возвращаемые назад символы в совместно
используемый бу<fx:'р, являющийся символьным массивом. функция
g e tch читает из этого бу<fx:'ра, если в нем что-либо имеется; если же
бу<fx:'р пуст, она обращается к getchar . При этом также нужна
индекс ирующзя переменная, которая бу,цет фиксировать позицию
TeкytЦeгo символа в буфере.
Б.8. К<'рниган, Д. М. Ричи ЯЗЫК "JIOграм""ирования С

Так как бу<fx:'р и его индекс совместно используются функциями getch 1-


u ngetch и должны сохранять свои значения в период между
обращениями, они должны быть внеl.LIНИМИ для обеих функций. Таким
образом, мы можем написать getch, u ngetch и эти переменные как:

#dеfiлe bufsize 100


char buf[bufsize]; /* buffer for ungetch */
int ЬШр = О; /* пеХ! free position in ЬШ */

gelchO j* gel" (possibly pushed b"ck) char"Cler * !


{
retuгn((bufp > О)? buf[--bufp] : getchar());
}

ungetch(c) /* push character back оп input */


int с;
{
if (bufp > bufsize)
printf(''ungetch: too тапу characters\n');
еЕе
ЬШ [bufp++] = с;
}

Мы использо вали для хранения возвращаемых символов массив, а не


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

Упражнение 4-4

НаПИlШ1те функцию ungets (s), которая будет возвращать во ввод


целую строку. Должна ли ungets иметь дело с buf и bufp или она
может просто использовать ungetch ?

Упражнение 4-5

Предположите, что может возвращаться только один символ. Измените


g e tch и ungetch соответствующим образом.

Упражнение 4-6
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

НаlШ1 функции getch и u n getc h не обеспечивают обработку


возвращенного символа EOF переносимым образом. РеlШ1те , каким
свойством ДОЛЖНЫ обладать эти функции, если возвращзется EOF, и
реализуйте ваlШ1 выводы.

4.6. СТi!ГИческие переменные

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


до полнении к автоматическим переменным и extern, с которыми мы

уже встречались.

Статические переменные MOryr быть либо внугренними , либо


внеll.lНИМИ. Внутренние статические переменные точно так же, как и
автоматические, являются локальными для некоторой функции , но , в
отличие от автоматических, они остаются c.YJ..Цf'CТBoBaTЬ, а не

появляются и исчезают вместе с обращением к этой функции. это


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

Внеll.lНие статические переменные определены в остальной части того


исходного файла, в котором они описаны, но не в каком-либо д ругом
файле. Таким образом , они дают способ скрывать имена, подобные Ьи f
и bufp в комбинации getch - ungetch , которые в силу их
совместного использования должны быть внеll.lНИМИ, но все же не
доступными для пользо вателей getch и u ngetch, чтобы
исключал ась возможность конфrшкта. Если эти две функции и две
переменные объединить в одном файле следующим образом

statК: char buf[bufsize]; />1< buffer for ungetch >1</


statК: iпt ЬШр = О; /*next fгee position iп buf >1</

ge,ehQ {... }

tmge,ehO {... }

то никакая друтая функция не бу,цет в состоянии обратиться к buf и


ш
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

bufp ; фактически, они не бy,цyr вступать в конфликт с такими же


именами из других файлов той же самой программы.

Статическая память, как внутренняя, так и внеll.lНЯЯ, специфицируется


словом static , стоящим перед обычным описанием. Переменная
является внеll.lНеЙ, если она описана вне какой бы то ни было функции,
и внутренней, если она описана внутри неЮJТОРОЙ функции.

Нормально функции являются внеll.lНИМИ объектами; их имена известны


глобально. возможно, ОДнаЮJ, объявить функцию как static ; тогда ее
имя становится неи з вестным вне фай ла, в ЮJТОРОМ оно описано.

В языке 'С"" static "отражает не ТОЛЬЮJ постоянство , но И степень


того, что можно наз вать ''приватностью''. Внутренние статические
объекты определены ТОЛЬЮJ внугри одной функции внеlllН.ие
статические объекты / переменные или функции / определены ТОЛЬЮJ
внутри того исходного фзйла, где они появ ляются, и их имена не
вступают в конфликт с такими же именами переменных и функций из
друти х фзЙлов.

ВнеlllН.ие статические переменные и функции предоставляют способ


организовывать данные и работающие с ними внугренние процедуры
таким образом, что другие процедуры и данные не могуг прийти с ни м и
в ЮJнфликт даже по недо разумению. Например, функции getch и
ungetch образуют ''мqцуnъ'' для ввода и возвращения символов; Ьи f и
bufp должны быть статическими, чтобы они не были доступны извне.
Точно так же функции push, рор и clear ф:Jрм ируют модуль
обработки стека v а r и s р тоже должны быть внеll.lНИМИ
стати ческими .

4.7. Регистровые переменные

Четвертый и последний класс памяти н азы вается реЛ1СТРОВЫМ.


описание register указывает ЮJмпилятору, что данная переменная
будет часто использоваться. Когда это возможно, переменные,
описанные как register , располагаются в маl..lfl.1ННЫХ реЛ1страх. что
может привести к MeHhl..lfl.1M по разме ру и более быстрым программам.
описание register выглядит как
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

regic;(er int х;
regic;(er сМг с;

и Т.Д.; часть in t может быть опущена. описание register можно


использовать только ДЛЯ автоматических переменных и qюрмальных
параметров функций. В этом последнем сл учае описания выглядят
следующим образ ом:

~с,п)
regic;ter in( С, Л;
{
regic;(er int ~

На практике возникают некоторые ограничения на регистровые


переменные, отражающие реальные возможности имеющи)[:я

аппаратных среДств. В реrnстры можно поместить тол ько несколько


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

неразрешенных типов слово register игнорируется. Кроме того


нево з можно изв л ечь адрес регистровой переменной (этот вопрос
обсуждается в ле кции No5). Эти специ<f:ические ограничения
варьируются от машины к машине. Так. например , на PDP-ll
э фрективными являются тол ько первые три описания register в
функции, а в качестве типов допускаются in t , char или указател ь.

4.8 . Блочная структура

Язык "С " не является языком с блочной структурой в смысл е PUl или
алгол а; в нем нельзя опи сывать одни функции внутри других.

переменные же, с другой стороны, мосуг определяться по метоАУ


бл очного структурирования. описания переменных (включая
инициализацию) могут следовать за л евой <f:игурной
скобкой,открывающей любой оператор, а не только за той, с которой
начинается тело функции. переменные , описанные таким образом,
вытесняют любые переменные из внеll.lНИХ блоков, имеющие такие же
Б.8. К<'рниган, Д. М . Ричи Я ЗЫ К "JIOграм""ирования С

имена, и остаются определенными до соответствующей правой


4иrypной скобки. Например в

if (п > { О)
int ~
/* decJare а new i */
for 0= О; i < п; i++)

Областью действия переменной i является ''истинная'' ветвь i f ; это i


никак не связано ни с какими другими i в программе.

Блочная структура влияет и на область действия внешних переменны х.


Если даны о пи сания

int х;

ю
{
double х;

То появление х внугри фунЮJ,ИИ f относится к внугренней переменной


типа double , а вне f - к внешней целой переменной. Это же
справедливо в отношении имен ф:Jрмальных параметров:

int х;
~x)
double х;
{

Внугри фунЮJ,ии f имя х относится к формальному параметру, а не к


внешней переменной.

4.9. Инициализация

>20
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Мы до сих пор уже много раз упоминали инициализацию, но всегда


мимоходом, среди других вопросов. Теперь, после того как мы обсу,цили
различные классы памяти, мы в этом разделе просуммируем некоторые

правила , относящиеся к инициал и зации.

Если явная инициализация отсутствует, то внешним и статическим


переменным присваивается значение нуль ; автоматические и

регистровые переменные имеют в этом случае неопределенные

значения (мусор).

Простые переменные (не массивы или структуры ) можно


инициализировать при их описании, добавляя вслед за именем знак
равенства и константное выражение:

int x = l ;
char sqoote = '\";
Iong day = 60 '" 24; /'" minutes in а day "'/

Для внешних и статических переменных инициализация выполняется


ТОЛЬЮJ оДИН раз, на этапе компиляции. Автоматические и регистровые
переменные инициализируются каждый раз при входе в функцию и л и
блок. В случае автоматических и регистровых переменных
инициализатор не обязан быть ЮJнстантой: на самом деле он может
быть любым значимым выражением, ЮJторое может включать
определеные ранее величины и даже обращения к функциям. Например,
инициализация в программе бинарного поиска из лекции N"з могла бы
быть записана в виде

blnary(x, v, п)
int х, V(], п;
{
int Iow = О;
int high = п- 1;
int mid;

вместо

blnary(x, v, п)

ш
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

int х, V(], п;
{
int Iow, high. mid;

Iow = О;
Шgh = П-l;

По своему результату, инициализации автоматических переменных


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

заметна. Автоматич еские массивы не могут быть инициализированы.


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

mainО /* СОUП( digits, white space, others */


(
int с, ~ пwhitе, rюthег;
inl ndЩit[10 ];

nwhite = rюtheг = О;
for (i = О ; i < 10; i++)
ndigil[i] = О;

может быть переписана в виде

int nwhite = О;
int nother = О;
int ndigit[lO] = { О, О, О, О, О, О, О, О, О, О };

main() /* COLll1t digits, white space, others */


(
int с, ~
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Эти инициализации фактически не нужны, так как все присваиваемые


значения равны нулю, но )[)РОl..lМй стиль - сделать их явными. Если
количество начальных з начений MeHbll.le, чем указанный размер
массива, то остальные элементы заполняются нулями. Перечисление
слиl.LIкDм больuюro числа начальных значений является Оll.В1бкоЙ. К
сожалению, не предусмотрена возможность указания, что некоторое

начальное значение повторяется, и нельзя инициализировать элемент в

середине массив без перечисления всех предыду1ЦИХ.

Для символьных массивов существует специальный способ


инициализации ; вместо 4игурных скобок и запятых можно
использовать строку:

char pattern[J = 'llie";

Это сокраJ..Цeние более длинной, но экв ивалентной записи:

char pattern[] = { 't', 'h', 'е', '\0' };

Если размер массив любого типа опущен, то компилятор определяет его


дл ину, подс читывая число начальных значений. В этом конкретном
случае размер равен четырем (три символа плюс конечное \ О ).

4.10. Рекурсия

в яз ыке "С " функции могут использоваться рекурсивно; это означает, что
функция может прямо или косвенно обращаться к себе самой.
Традиционным примером является печать числа в виде строки
символов. как мы уже ранее отмечали, циcWы генерируются не в том
порядке: цифры младll.В1х разрядов появляются раньше цифр из cтapll.В1x
разрядов, но печататься они должны в обратном порядке.

Эту проблему можно pel..lМTb двумя способами. Первый способ,


которым мы воспользовались в лекции NQЗ в функции i toa ,
заключается в запоминании циcW в некотором массиве по мере их
поступ ления и послеАУЮщем их печатании в обратном порядке. Первый
m
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

вариант функции pr in td сл едует это й схеме.

pгintd(n) /* pгint n in decima! */


int п;

char 5(10J;
int ~

if(n < O) {
ршсhaг('-');
n = -п;

i = О;
do {
s[ i++ ] = n % 10 + 'О'; /* get next char */
} while ((п {= 10) > О); {* d~card il *{
while (--i >= О)
ршсhaг(5(iJ);

Альте р натив ой этом у способу яв ляется р екурси вное решение, когда при
каждом вызове функция printd сначала снова об ращается к себе,
чтоб ы ско пировать л идирую щие циcWы , а затем печатает по сл едню ю
ци41:>у.

pгintd(n) /* ргiш n in decimal (recursive)*/


int п;
(
int ~

if(n < О){


ршсhaг('-');
n = -п;
}
if ((i = n/l0) != О)
prinld(O;
putchar(n % 10 + 'О');
)

'"
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Когда функция вызывает себя рекурсивно, при каждом обраl.Цf'НИИ


образуется новый набор всех автоматических переменных, совершенно
не зависящий от предыдущего набора. Таким образом, в
printd(123) первая функция printd имеет n = 123 . Она
передает 12 второй printd, а когда та возвращзет ynравление ей,
печатает 3 . Точно так же вторая printd передает 1 третьей (которая
эту единицу печатает), а затем печатает 2.

Рекурсия обычно не дает никакой экономии памяти, поскольку


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

деревьями; хороший пример бmет приведен в ле кции No6 .

Упражнение 4-7

Приспособьте идеи, использованные в printd для рекурсивного


написания i toa ; Т.е. Преобразуйте целое в строку с помощью
рекурсивной процеАУРЫ.

Упражнение 4-8

Напишите рекурсивный вариант функции reverse (s) , которая


распола гает в обратном порядке строку s.

4.11. Препроцессор языка "С"

в языке "с" предусмотрены определ еные расum:рения языка с помощью


простого макропредпроцессора. одним из самых распространенных

таких расширений, которое мы уже использовали, является конструкция


#define ; другим расширением яв л яется возможность включать во
время компи л яции содержимое других файлов.

4.11.1. Включение фай л ов


Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

Для облегчения работы с наборами конструкций #define и оп и сан ий


(среди прочи х средств) в языке "с" предусмотрена возможность
включения файлов. Любая строка вида

#include "Шеname"

заменяется содержимым файла с и менем filename . (Кавычки


обязательны). Часто одна или две строки такого вида появляются в
начале каждого исходного файла, для того чтобы включить общие
конструкции #"define и описан ия extern для глобальных
переменных. Допускается вложенность конструкций #" i nc 1 ude .

конструкция #" include является предпочтительным способом связи


описаний в БОЛЫШIХ программах. Этот способ гарантирует, что все
исходные файлы будуг снабжены одинаковым и определениями и
описаниями переменных, и, следовательно, исключает особенно
неприятный сорт ошибок. Естественно, когда какой-то включаемый
файл изменяется, все зависящие от него файлы должны быть
перекомпилированы.

4.11.2. Макроподстановка

определение вида

#define tes 1

приводит К макроподстановке самого простого вида - замене имени на

строку символов. Имена в #"define имеют ту же самую фJрму, что и


иденти фикаторы в "с"; заменяющий текст совершенно произволен.
Нормально заменяющим текстом является остальная часть строки;
дл инное определение можно продолжить, поместив \ в конец
продолжаемой строки. 'Область действия" имени , определенного в
#"define, простирается от то чки определен ия до конца ис.ю,цного
файла. Имена могут быть переопределены, и определения могут
использовать определения, сделанные ранее. Внугри заключенных в
кавы чки строк подстановки не прои зводятся, так что если, например,

yes - определе нное имя, то в printf{ " yes " ) не бу,цет сделано
никакой подстановки.

",
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Так как реализация #define является частью работы


макропредпроцессора, а не собственно ЮJмпилятора, имеется очень
мало грамматических ограничений на то, ч то может быть определено.
Так, например, люб ители алгола могут объявить

#dеfiлe then
#dеfiлe Ьеgin {
#dеfiлe еnd ;}

и затем написать

if (i > О) шеп
Ьеgin
а = 1;
Ь = 2
еnd

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


что заменяющий текст будет зависеть от вида обращения к макросу.
Определим, например, макрос с именем mах следующим образом:

#dеfiлe mзх(а , Ь) «а) > (Ь)? (а): (Ь))

когда строка

х = max(p+q, r+5);

будет заменена СТРОЮJЙ

х = «p+q) > (r+s)? (p+q) : (r+s));

Такая возможность обеспе чив ает " функцию макс и мума", ЮJторая
расLШ1РЯется в последо вател ьный код, а не в обращение к функции. При
правильном обращении с аргументам и таЮJЙ макрос бу,цет работать с
любыми типами данных; здесь нет необ)[)Димости в разл ичных видах
тах для да нных разных типов, как это было бы с функциями.

Конечно, есл и вы тщзтельно рассмотрите приведен ное выше


раСLШ1рение mах , вы замети те определеные недостатки. Выражения
вычисляются дважды; это плохо , если он и влекут за собо й побочные
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

э фректы, вызванные , например, обращениями к функциям или


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

#define square(x) х >1< Х

при обращении к ней, как square ( z + l ) . Здесь возникают даже


некоторые чисто лексические проблемы: между именем макро и л евой
кругл ой скобкой, открываюl.Lf'Й список ее аргументов, не должно быть
никаких пробелов.

Тем не менее аппарат макросов является весьма ценным. Один


практический пример дает описываемая в л екции Ng7 стандартная
библиотека ввода-вывода, в которой getchar и putchar
определ ены как макросы (очевидно putcha r должна иметь apryмeHT),
что по з воляет избежать затрат на обращение к функции при обработке
каждого символа.

Другие возможности макропроцессора описаны в при ложении А.

Упражнение 4-9

Определите макрос swap ( х, у) , который обменивает з начениями


два своих аргумента типа int . (В этом случае поможет бл очная
структура ).

'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Указатели и массивы

Дается описание указателей и массивов, инициализация и работа с


ними.

5. Указатели и массивы

Указател ь - это переменная, со,цержащзя адрес другой переменной


указатели очень I..lfl.1pOкo используются в яз ыке "С". Это происходит
отчасти потому, что иногда они да ют единственную возможносгь

выразить нужное действие, а отчасти потому, что они обычно Beдyr к


более компактным и эфtx:>ктивным программам , чем те, которые могуг
быть пол учены другими способами.

указатели обычно смеl..lfl.1вают в одну кучу с операторами goto,


характеризуя их как чу,цес ный способ написания программ, которые
нево зм ожно понять. Это безусловно справедливо, если указатели
используются беззаботно; очень просто ввести указатели, которые
указывают на что-то совершенно неожиданное. Однако, при
определенной дисциплине, использование указателей помогает достичь
ясности и простоты. Именно этот аспект мы попытаемся здес ь
проиллюстрировать.

5.1 . Указатели и адреса

Так как указатель содержит адрес объекта, это дает возможносгь


''косвенного'' досгупа к этому объекту через указатель. Предположим,
что х - переменная, например , типа int , а рх - указатель, созданный
неким еще не указанным способом. Унарная операция & выдает адрес
объекта, так что оператор

рх = &х;

присваивает адрес х переменной рх ; говорят, что рх "указывает" на х .


Операция & применима только к переменным и элементам массива,
конструкции вида & (х -1) и & 3 являются незаконными. Нельзя также
получить адрес реЛ1СТРОВОЙ переменной.

'"
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Унарная операция * рассматривает свой операнд как адрес конечной


цели и обращается по этому адресу, чтобы извлечь содержимое .
С ледовательно , если у тоже имеет тип i nt , то

у = "' рх;

присваивает у содержимое того, на что указывает рх . Так


посл едовательность

рх = &х;
у = "' рх;

присваивает у то же самое значение, что и оператор

у = х;

переменные, участвующие во всем этом необ>DДИМО описать:

int Х, у; int "' р х;

с описанием для х и у мы уже неоднократно встречались. описание

указателя

int "' р х;

яв л яется новым и должно рассматриваться как мнемоническое; оно

говорит, что комбинация * р х имеет тип i nt . Это означает, что есл и р х


появляется в контексте * р х, то это эквив ал ентно переменной типа int .
Фактически синтаксис описания переменной имитирует синтаксис
выражений , в которых эта переменная может появ л яться. Это
замечание полез но во всех сл учаях, связанных со сложными

описаниями. Например ,

double . tof(), *dp;

говорит, что atof ( ) и *dp имеют в выражениях з начения типа


double .

Вы должны также заметить, что из этого описания следует, что


указатель может указ ывать тол ько на определ енный вид объектов.

>30
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

указатели мосуг в.хо,цить в выражения. Например, если р х указывает на


целое х , то * р х может появляться в любом контексте, где может
встретиться х. Так оператор

у = "' рх+ 1

присваивает у значение, на 1 большее з начения х ;

p гintf{'%d\n", "' р х)

печатает текущее значение х ;

d = sqrt((double) ' рх)

получает в d квадратный корень из х , причем до передачи функции


sqrt значение х преобразуется к тип у doubl e . (Смотри л екцию NQ2).

в выражениях вида

у = "' рх+ 1

унарные операции * и & связаны со своим операндом бол ее крепко , чем


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

у = ' (р х+ 1)

С сы л ки на указатели MOгyr появ л яться и В левой части присваиваниЙ.


Если р х указывает на х, то

"' р х = о

полагает х равным нулю , а

"' р х += 1

увел ичивает его на единицу, как и выражение

(' рх)++
ш
Б.8. К<'рниган, Д. М . Ричи Я ЗЫ К "JIOграм""ирования С

Круглые скобки в последнем при мере необходимы; если их опусгить, то


поскольку унарные операции , подобные * и ++ , выполняются справа
налево , это выражение увеличит р х , а не ту переменную , на юторую

он указывает.

и наюнец , так как указатели являются переменными , то с ними можно


обращпься, как и с осгальными переменными. Если ру - другой
указатель на переменную типа i n t , то

ру = рх

юпирует содержимое р х в ру , в результате чего ру указ ывает на то же ,

что и р х .

5.2. УказатеIШ и аргументы функций

Так как в "с" передача аргументов функциям осущесгвляется ' 'по


значению", вы з ванная процедура не имеет непосредственной
во з можносги изменить переменную из вызывающей программы. Что
же дел ать, если вам действительно надо изменить аргумент? Например,
программа сортировки захотела бы поменять два нарушающих порядок
э лемента с помощью функции с именем swap. Дл я этого недостаточно
написать

swap(a, Ь) ;

определ ив функцию swap при э том сл едующим образом:

swap(x, у) /* wrong */
int Х, у;
{
int {е mp ;

tе ЩJ = х;
х = у;
у = temp;

ш
Б.8. К <'р ниган, Д. М . Ричи Я з ы к "JIOграм""ирования С

из-за вызова по значению swap не может воздействовать на аргументы


а и Ь в вызывающей фунЮJ,ИИ.

к счастью, все же имеется возможность получить желаемый э<lxJx:'кт.


Вы з ывающзя программа передает указатели подлежащих изменению
значений: swap (&а , &Ь) ; так как операция & выдает адрес
переменной, то & а является указателем на а . В самой swap аргументы
описываются как указатели и доступ к фактическим операндам
осуществляется через них.

swap(px, р у) /* interchange "' р х and * ру "'/


int * р х, *ру;
{
int {е mp ;

= * р х;
tе IТЧJ
* рх = "' р у;
*ру = temp;
}

Указатели в качестве аргументов обычно используются в фунЮJ,иях,


которые должны возвращзть бол ее одного з начения. (Можно сказать,
что swap во з вращзет два значения, новые з начения ее аргументов ). В
качестве примера рассмотрим фунЮJ,ию getint, которая осуществляет
преобразование поступающих в свободном qюрмате данных, раздел яя
поток символов на целые значения, по одному целому за одно

обращение. фунЮJ,ИЯ g e tint должна возвращзть л ибо найденное


значение, л ибо признак конца фай л а, есл и входные данные пол ностью
исчерпаны. Эти з начения должны во з вращзться как отдельные объекты ,
какое бы з начение ни испол ьзовал ось для EOF , даже есл и это значение
вводимого целого.

Одно из решений , основывающееся на описываемой в лекции No7


функции ввода scanf, состоит в том, чтобы при выходе на конец фай л а
g e tint возвращзла EOF в качестве з начения фунЮJ,ИИ ; любое другое
во з вращенное з начение говорит о нахо ждении нормал ьного целого.

Численное же з начение найденного цел ого возвращзется через


аргумент, который должен быть указателем цел ого. Эта организация
разделяет статус конца файла и числ енные з начения.
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Следую щий цикл заполняет массив целыми с помощью обраl.ЦE'НИЙ к


функции

geunr:
int n, У, array[size);

ЮГ (п = О ; n < size && geUnt(&v)! = EOF; п++ )


аlТау(п] = у;

в резyn.ьтате каждого обраl.ЦE'НИЯ v становится равным следующему


целому значению, найденному во входных данных. Обратите внимание,
что в качестве аргумента getint необходимо указать &v а не v.
Использование просто v скорее всего приведет к ошибке адресации,
поскольку get i nt полагает, что она работает именно с указателем.

Сама ge t i n t является очевидной модификацией написанной нами


ранее функции atoi :

geUnr(pn) /* ger next integer from input */


int *рп;
{
int c,sign;

while ((е = geleh()) == " 11 е == 'In'


11 с == '\t'); /* skip whire space */
sign = 1;
if(e == '+' 11 е == '-') { {* record
sign */
sign = (е == '+') ? 1 :-1;
е = getehO;
}
юг (*рп = О; е >= 'о' && е <= '9'; е = geleh())
*pn = 10 * * рп + с - 'О';
* рп *= sign;
if(e! = EOF)
ungeleh(e);
геlШ11{е );
}

'"
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

Выражение *pn используется всю.цу в g e t i nt как обычная


переменная типа i nt . Мы также использовали функции g e tc h и
u n getc h ( описанные в лекции No4) , так что один л ишний символ,
который приходится считывать, может быть помещен обратно во ввод.

Упражнение 5-1

НаПИlШ1те функцию g e tfloat , аналог geti n t для чисел с


плавающей точкой. Какой тип должна возвра1..ЦЗТЬ g e tf l oat в
качестве значения функции ?

5.3. Указатели и массивы

В языке 'С" существует с и льная взаимосвязь между указателями и


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

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


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

iлt а[10]

определяет массив размера 10, Т.е. Набор из 10 последовательных


объектов, называем ых а [О ] I а [1 ] I ••• I а ( 9 ]. Запись а [i ]
соответствует элементу массива через i позиций от начала. Если ра -
указатель целого , описанный как

int"'pa

то присваивание

ра = &а[ О]

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


означает, что ра содержит адрес элемента а [ О ] . Теперь присваивание

х = "' ра
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

будет копировать содержимое а [ О] в х .

Если ра указывает на некоторый определенный элемент массива а , то


по определению pa+l указывает на следующий элемент, и вообще ра­
i указывает на элемент, стоящий на i позиций до элемента,
указываемого ра , а pa+i на элемент, стоящий на i позиций после.
Таким образом, если ра указывает на а [ О ] , то

'(pa+l)

ссылается на содержимое a[ l ] , pa+i - адрес a [ i] , а * (pa + i) -


содержимое а [i ] .

Эти замечания справедливы независимо от типа переменных в массиве


а. Сугь определения ' 'добавления 1 к указателю ", а таюке его
распространения на всю ариф\1етику указателей, состоит в том, что
при ращение масuпабируется разме ром памяти , занимаемой объектом,
на который указывает указатель. Таким образом, i в ра + i перед
прибавл ением умножается на разме р объектов, на которые указывает
ра .

Очевидно CyrцeCТByeT очень тесное соответствие между индексацией и


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

ра=&а[О] можно записать как

ра = а

Еще более удивительным, по крайней мере на первый взгляд, кажется


тот факт, что ссылку на а (i ] можно записать в виде * (а + i ). При
анализировании выражения а [i ] в языке "С " оно немедленно
преобразуется к ВИАУ * (а + i ) эти две формы совершенно
эквивале нтны. Если применить операцию & к обеим частям такого
соотношения эквивалентности, то мы получим , что & а [ i] и а + i тоже
идентичны: a+i - адрес i -го элемента от начала а . С другой стороны,
если ра является указателем, то в выражениях его можно использовать

>3'
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

с индексом: ра [ i ] идентично * (pa +i ) . Короче, л юбое выражение ,


включающее массивы и индексы , может быть записано через указатели
и смеl.lf'НИЯ и наоборот, причем даже в одном И том же угверждении.

Имеется одно различие между именем массива и указателем , которое


необходимо иметь в виду. указател ь является переменной, так что
операции ра=а и ра + + имеют смысл. Но имя массива является
константой, а не переменной: конструкции типа а=ра или а + + , И Л И
р=&а БWJ.YТ незаконными.

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


местопол ожение начала этого массива. Внугри вы з ванной функции
такой аргумент является точно такой же переменной , как и любая
друтая, так что имя массива в качестве аргумента действительно
яв л яется указателем, Т.е. переменной, сqцержаl.lf'Й адрес. Мы можем
использовать это обстоятел ьство для написания нового варианта
функции strle n , ВЫЧИСЛЯЮl.lf'й длин у строки.

strlen(s) /* геtшn length of 5tring 5 */


char *5;

int п;

fo r (n = О; *5! = '\0'; 5++)


п+ + ;

retШ11(п);
}

Операция увеличения s совершенно законна, поскол ьку эта переменная


яв л яется указател ем ; з++ никак не влияет на символьную строку в

обратившейся к str l en функции , а тол ько увеличивает л окальную для


функции strle n копию адреса. описания формальных параметров в
определ ении функции в виде

char 5[ ); char *5;

совершенно эквивалентны ; какой вид описания следует предпочесть,


определ яется в значительной степени тем, какие выражения БWJ.YТ
использованы при написании функции. Если функции передается имя
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

массива, то в зависимости от того, что у,цобнее, можно полагать, что


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

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


аргумента указатель начала по.цмассива. Например , если а - массив, то
как

[(&a(2J)

как и

~a + 2)

передают функции f адрес элемента а [2] , потому что и & а (2 ] , и а +2


являются указательными выражениями , ссылающимися на третий
элемент а . Внугри функции f описания аргументов MOryr присугствоваl
в виде:

[(arr) int arr();


{

или

f(arr) int *arr;


{

Что касается функции f, то тот фнcr, что ее apryмeHT в действительности


ссылается к части большего массива,не имеет для нее никаких
посл едствий.

5.4. Адресная арифметика

Если р является указателем, то каков бы ни был сорт объекта, на


>3'
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

который он указывает, операция р++ увеличивает р так, что он


указывает на следующий элемент набора этих объектов, а операция р
+=i увеличивает р так, чтобы он указывал на элемент, отсгоящий на i
элементов от текущего элемента. Эти и аналогичные консгрукции
предсгавляют собой самые просгые и самые распросграненные формы
арифметики указателей или адресной ариф\1етики.

Язык "С" последователен и постоянен в своем подходе к адресной


арифметике; объединение в одно целое указателей, массивов и
адресной ариф\1етики является одной из наиболее сильных сторон
языка. Давайте проиллюстрируем неЮJторые из соответсгвующих
во зм ожностей языка на примере элементарной (но полезной, несмотря
на свою просготу) программы распределения памяти. Имеются две
функции: функция alloc (n) возвраl.Lflет в качестве своего значения
указатель р , ЮJторый указывает на первую из n последовательных
символьных позиций, которые MOryr быть использованы вызывающей
функцию a l loc программой для хранения символов; функция f ree (р
освобождает приобретенную таким образом память, так что ее в
дал ьнейшем можно снова использовать. программа является
"элементарной", потому что обращения к free должны производиться
В порядке, обратном тому, в ЮJтором производились обращения к
a l loc . Таким образом, ynравляемая функциями alloc и free память
является стеЮJМ или С ПИСЮJМ, в ЮJтором посл едний вводимый элемент
извлекается первым. Стандартная библиотека языка "С" содержит
аналогичные функции , не имеющие таких ограничений, и, кроме того, в
лекции No8 мы приведем улуч шенные варианты. Между тем, qд,наЮJ, для
многих приложений нужна ТОЛЬЮJ тривиальная функция al l oc для
распределе ния неБОЛЫ1Ш Х участЮJВ памяти неизвесгных заранее
размеров в непредсказуем ые моменты времени.

Простейшая реал изация состоит в ТОМ, чтобы функция раздавала


отрезки БОЛЫIЮГО символьного массива, ЮJторому мы присвоили имя
a l locbuf . Этот массив яв ляется собственностью функций alloc и
free . Так как они работают с указателями, а не с индексами массива,
никаЮJй другой функции не нужно знать имя этого массива. Он может
быть описан как внеllnШЙ статический, Т.е. он будет локал ьным по
отношению к исходному фай лу, содержащему alloc и free , и
невидимым за его пределам и. При практичесЮJЙ реализации этот

>3'
Б.8. К<'рниган, Д. М . Ричи Я ЗЫ К "JIOграм""ирования С

массив может даже не иметь имени; вместо этого он может быть


получен в резуnътате запроса к операционной системе на указател ь
HeКlJТOpOГO неименованного блока памяти.

Другой необходимой ишlюрмацией является то, какая часть массива


a l locbuf уже использована. Мы пользуемся указателем первого
свободного элемента , названным al l ocp. Ко гда к функции alloc
обращзются за выделением n символов, то она проверяет, достаточно
ли осталось для этого места в al l ocbuf . Есл и достаточно , то alloc
возвращзет текущее значение al l ocp (т.е. начал о свободного блока),
затем увеличивает его на п , с тем чтобы он указывал на следующую
свободную область. функция free (р) просто полагает a l locp
равным р при условии , что р указывает на позицию внутри

a l locbuf .

#define null О /* pointer value for е гroг report */


#define allocsize 1000 /* size of available space */

statК: char allocbuf[allocsize];/* storage for alloc */


static char *а По с р = allocbuf; /* next free position */

char *аlЮ с (п) /* Ге[ШП pointer {о n characters */


int п;
(
if (allocp + n <= allocbuf + allocsize) (
allocp += п;
геtшп(аllo с р - п); /* old Р */
} ебе /* rю t еrю ugh roо m */
retШ11(пull) ;
)

free (p) /* free storage poinred Ьу р */


char * р ;
(
if (р >= allocbuf & & р < allocbuf + allocsize)
а По с р = р;
)

Дадим HeКlJTOpыe пояснения. Вообще говоря , указатель может быть

'"
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

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


обычно единственными осмысленными значениями яв л яются nu l l
(это обсуждается ниже) или выражение , включаЮl.Цf'е адреса ранее
определ еных данных соответствующего типа.

описание

static сЬас "'allocp = allocbuf;

определ яет a l locp как указатель на символы и инициализирует его


так, чтобы он указывал на allocbuf , Т.е. на первую свободную
позицию при начале работы программы. Так как имя массива является
адресом его нулевого э лемента, то это можно бы л о бы записать в виде

static сЬас "'allocp = &allocbuf[Q );

используйте ту запись, ЮJТорая вам кажется более естественной. С


помощью проверки

if (allocp + n <= allocbuf + allocsize)

выясняется, остал ось л и достаточно места, чтобы у,цов л етворить запрос


на n символов. Если достаточно , то новое значение allocp не будет
указ ывать дальше, чем на посл еднюю позицию a l locbuf . Есл и
запрос может быть у,цовлетворен , то alloc возвращает обычный
указатель (обратите внимание на описание самой функции ). Если же
нет, то alloc должна вернугь неЮJТОРЫЙ признак, говорящий о том,
что больше места не осталось. В языке "С" гарантируется, что ни один
прави л ьный указател ь данных не может иметь значение нуль, так что
во з вращение нуля может служить в качестве сигнала о ненормальном

событии, в данном случае об отсугствии места. Мы, однако , вместо н ул я


пишем null , с тем чтобы более ясно показать, что это специальное
значение указателя. Вообще говоря, цел ые не могут осмысленно
присваиваться указател ям , а н ул ь - это особый случай.

Проверки вида

if (allocp + n <= allocbuf + aloocsize) и


if (р >= allocbuf && р < allocbuf + allocsize)

'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

демонстрируют несЮJЛЬЮJ важных аспектов ари~етики указателей. 80-


первых , при определеных условиях указател и можно сравнивать. Если
р и q указывают на элементы одного и того же массива, то такие

отношения, как <, >= И Т.д., работают надлежащим образом. Напр и мер,

p<q

истинно, если р указывает на более ранний элемент массива, чем q.


Отношения == и != тоже работают. Любой указатель можно
осмысленным образом сравнить на равенство или неравенство с nu 11 .
Но ни за что нельзя ручаться, есл и вы используете сравнения при
работе с указателям и , указывающими на разные массивы. Есл и вам
повезет, то на всех машинах вы получите очев и дную бессмысл иц у. Если
же нет, то ваша программа б)Щет правильно работать на одной MallD1He и
давать непостижимые результаты на друтой.

Во-вторых, как мы уже видел и, указатель и целое можно складывать и


вычитать. ЮJнструкц ия

р + п

подразумевает n -ый объект за тем, на ЮJторый р указывает в


настоящий момент. Это справедливо независимо от того, на каЮJЙ вид
объектов р долже н указывать; ЮJмпилятор сам масuпабирует n в
соответстви и с определяемым из описания р размером объектов,
указываемых с помощью р . Например, на PDP-ll масuпабирующий
множитель равен 1 для char, 2 для int и sho r t , 4 для 10ng и
f 1 0a t и 8 для doubl e .

Вычитание указателей тоже возможно: если р и q указывают на


элементы одного и того же массива, то p-q - количество элем ентов

между р и q. Этот фзкт можно использовать для написания еще одного


варианта функции

strlen:
strlen(s) /'" геtшп length of string s "'/
char "'s;
{
char "'р = s;
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

while (* р != '\0')
р ++;

rerurn(p- s);
}

При описании указатель р в этой 4о/Нкции инициал изирован


посредством строки s , в результате чего он указывает на первый символ
строки. В цикле while по очереди проверяется каждый символ до тех
пор, пока не появится символ конца строки \ О . Так как значение \ О
равно нулю, а while только выясняет, имеет ли выражение в нем
значение О , то в данном случае явную проверку можно опустить. Такие
циклы часто записывают в виде

while (* р)
р ++ ;

Так как р указ ывает на символы , то оператор р++ передвигает р кажды~


раз так, чтобы он указывал на слеАУЮЩИЙ символ . В результате p-s
дает число просмотренных символов , Т.е. длин у строки. Ари~етика
указателей последовательна: если бы мы имели дело с переменными
типаfloat, которые занимают БОЛЫllе памяти , чем переменные типа
char, и есл и бы р был указателем на float , то оператор р++
передвинул бы р на слеАУЮl.Цf'еfloat . Таким образом, мы могли бы
написать другой вариант 4о/Нкции alloc , раcnределяюl.Цf'Й память для
f l oat , вместо char, просто заменив всюду в a l loc и free
описател ь char на float . Все действия с указателями автоматически
учитывают размер объектов, на которые они указ ывают, так что БОЛ Ыllе
ничего менять не надо.

За исключением упомянутых выше операций (сложение и вычитание


указателя и цел ого, вычитание и сравнение двух указателей ), вся
остал ьная ари~етика указателей является незаконноЙ. Запрещено
складывать два указателя, умножать, делить, сдвигать или маскировать

их, а также прибав л ять к ним переменные типа f loat или double .

5.5 . УказатеJШ СИМВОЛОВ И фУНКЦИИ


Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

С трочная константа , как, например,

"j а т а string"

яв л яется массивом символов. Компи л ятор завершает внутреннее


представление такого массива символом \0 , так что программы могуг
находить его конец. Таким образом , длина массива в памяти
оказывается на единицу БОЛЫllе числа символов между двойными
кавычками.

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


аргументов функций, как, например , в

pгintf C'hello, wo гklln',;

когда символьная строка, подобная этой, появ л яется в программе, то


Aocтyn к ней осуществляется с помощью указателя символов; функция
pr i n t f фактически получает указатель символьного массива.

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


функций. Если описать me s sage как

cha.r *message;

то в результате оператора

message = ''now ic; the tirпe";

переменная message станет указателем на фактический массив


символов. Это не копирование строки; здесь участвуют только
указатели. В языке 'С" не предусмотрены какие-либо операции для
обработки всей строки символов как целого.

Мы проилл юстрируем друтие аспекты указател ей и массивов , разбирая


две полезные функции из стандартной библиотеки ввода-вывода,
которая бу,цет рассмотрена в л екции Ng7.

Первая функция - это strcpy ( s , t) , которая копирует строку t в


строку s. аргументы написаны именно в этом порядке по аналогии с

операцией присваивания, когда для того , чтобы присвоить t к s

'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

обычно пишyr

s=t

сначала приведем версию с массивами:

strcpy(s, () /* сору t (о s */
char s[], t[];
{
int~
i= О;
while « s[i] = t[i]) != '\0')
i++;

Для сопоставления ниже да ется вариант s trcpy с указателями.

strcpy(s, () /* со ру t (о s; pointer version 1 */


сhзг *s, *t;

while «*s = *t) != '\0') {


s++;
н+;

Так как аргументы переда ются по значению , функция st r cpy может


использов ать s и t так, как о на пожелает. Здесь они с у,цобст вом
пол агаются указателями , которые передви гаются вдоль масс и в о в, по

одному символу за ша г, по ка не бу,цет скопи ро ван в s завершающий в t


символ \ о.

На пракrике функция st r cpy была бы за пи сана не так, как м ы показали


выше. Вот вторая возм ожност ь:

strcpy(s, () /* со ру t (о s; pointer version 2 */


сhзг *s, *t;

while ((*s++ = "'t++) != '\0')

'"
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Здесь увел ичение s и t внесено в проверочную часть. Значением * t ++


яв л яется символ , на который указывал t до увел ичения; постфиксная
операция ++ не изменяет t , пока этот символ не бу,цет извлечен. Точно
так же этот символ помещзется в старую позицию З , дО того как s будет
увеличено. Конечный результат заключается в том, что все символы,
включая завершающий \0 , копируются из t в з.

и как последнее сокращение мы опять отметим , что сравнение с \ О


яв л яется излиll.lним, так что функцию можно записать в виде

strcpy(s, {) /* со ру t {о s; pointer version З */


char *s, *t;

while (*s++ = "'t++)

хотя с первого взгляда эта запись может показаться загадочной , она дает
значительное удобство. Этой идиомой сл едует овладеть уже хотя бы
потому. что вы с ней будете часто встречаться в 'С"- программах.

Вторая функция - 5 trcmp (з I t) , которая сравнивает символьные


строки s и t, возвращзя отрицательное, нулевое или положительное

значение в соответствии с тем, меньше, равно или бол ьше


лексикографически з, чем t. Во з вращземое значение получается в
результате вычитания символов из первой позиции, в которой s и t не
совпадают.

strcmp(s, {) /'" ге шrn < О if s<t, О if s==t, >0 if s>t */


char s[], t[];
{
int i;

i = О;
while (s[i] == t[i])
if (s[i++ ] == '\0')
return(O);

'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

relurn( ,[i]-l[i]);
}

Вот версия 5 trcmp с указателями:

strcmp(s, () /* return <О if s<t, О if s==t, >0 if s>t */


char *s, *t;

юг ( ,. *s == *t·" s+ + t++)
if (*, == '\0')
relUТn(O);
return(*s-*t);
}

так как ++ и -- мосуг быть как постфиксными, так и префиксны м и


операциями, в ст речаютсядрутие комбинации * и ++ и -- , хотя и менее
ча сто.

Наприм е р

*++р

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

*--р

сначала уменьшает р .

Упражнение 5-2

НаПИlШ1те вариант с указателями функции 5 trca t из лекции No2:


s trca t (s I t) копирует строку t в конец s.

Упражнение 5-3

НаПИlШ1те макрос для strcpy.

Упражнение 5-4

ПереПИlШ1те подходящие программы из предыдущих л екций и


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

'"
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Хорошие возможности для этого предостав л яют функции getline /


л екции 1 и N~/, а to i, i toa и и х варианты /л екция NQ2, NQЗ и NQ4/,
r e ve r s e /ле кция NоЗ/, i nde x и getop /л екция No4/.

5.6. УказатеIШ - не целые

Вы , возможно, обрати ли внимание в предыдущих "с"- программах на


довольно непринужденное отношение к копированию указател ей. В
общем это верно, ч то на большинстве машин указател ь можно
присвоить целому и передать его обратно, не и змен ив его; при этом не
происходит никакого маClшабирования и ли преобразования и ни один
бит не теряется. к сожалению, э то ведет к вол ьному обращению с
функциями, возвращзющими указател и, которые затем просто
передаются другим функциям , - необходимы е описания указател ей часто
опускаются. Рассмотрим, например , функцию s t r s а ve ( s ), которая
копирует строку s в неЮJторое место для хранения , выделяемое

посредством обращения к функции alloc, и возвращает указател ь на


это место. Правильно она должна быть записана так:

cha.r *strsave(s) /* save string s sоtп:'w heге */


cha.r *s;
{
cha.r * р , *allocO;
if ((р = alloc(strlen(s)+ I» != null)
strcpy(p, s);
ге tшп( р) ;
}

на практике c)'l.Цf'CТByeT сильное стремл ение опускать описания:

*strsave(s) /* save string s sоtп:'w heге */


{
cha.r * р ;

if ((р = a lloc(strlen(s)+ 1» != null)


strcpy(p, s);
ге tшп( р) ;
}

'"
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

Эта программа бу,цет правильно работать на многи х ма lllИН ах, потому


что по умолчанию функции и аргументы имеют тип i nt , а указатель и
целое обы чн о можно безопасно пересылать ту,ца и обратно. Однако
таЮJЙ стиль программирования в своем существе является
РИСЮJванным, ПОСЮJльку зависит от деталей реализации и архитектуры
маlllИНЫ и может привести к неправи льны м результатам на ЮJнкретном

используем ом вами компиляторе. Разумнее всюду использовать пол ные


описания. (Отладочная программа li n t предупреди т о таких
ЮJнструкциях, если они по неосторожности все же появятся).

5.7. Многомерные массивы

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


на практике существует тенденция к их знач и тельно более редЮJМУ
использованию по сравнен ию с массивами указателей. В этом разделе
мы рассмотрим неЮJторые их свойства.

Рассмотрим задачу преобразования дня месяца в де нь года и наоборот.


Например, 1 -0е марта яв ляется БО-м днем не ВИСОЮJсного года и б l -м
днем ВИСОЮJсного года. Давайте введем две функции для выпол нения
эти х преобразований: day of year преобразует месяц и день в день
года, а mont h day преобразует день года в месяц и день. Так как эта
посл едняя функц ия возвращает два значен ия, то аргументы месяца и дня
должны быть указателями:

IТЮпth_d ау( 1977 , 60, &щ &d)

Полагает m равным 3 и d равным 1 (l-oe марта).

Обе эти функции нуждаются в одной и той же информационной


таблице, указывающей число дней в каждом месяце. Так как число дней
в месяце в ВИСОЮJсном и В не ВИСОЮJсном году отличается, то проще

представ ить их в виде двух строк двумерного масс и ва, чем пытаться

прослеживать во время вычислений, что именно происходит в ф:>врале.


Вот этот массив и выполняющие эти преобразования функц и и:

staoc int day_tab[2][13] = (


(0,31, 28,31, 30,3 1,30,3 1, 31,30,31,30,31),

'"
Б.8. К<'рниган, Д.М. Ричи Язык "JIOграм""ирования С

(0,31,29,31,30,31,30,31 , 3 1,30,31,30,31)
};

day_ oCyear(year, rтюnth. day) /* set day of year */


int year, rтюпtл, day; /* fro m rmnth & day */
{
int ~ leap;
leap = year'/04 == О && year'/o I 00 != О 11 year'/o400 == О;
for (i = 1; i < rтюnth; i++)
day += day_ tab[leap ][i];
re,urn(day);

rmnth_day(year, yearday, р rтюnth. pday) /*set rmnth,day */


int year, yearday, *prmnth, *pday; /* fro m day of year */
{
leap = year'/04 == О && year'/oI00 != О 11 year'/0400 == О;
for (i = 1; yearday > day_ tab[leap ][i]; i++)
yearday - = day_,ab[ leap][i];
*prmnth = i;
*pday = yearday;
}

Массив dау tаЬ долженбытьвнеtшlимкакдля dау of уеаr ,так и


дл я топ t h da у , по скольку он испол ьзуется обеими этими функц иями.

Массив day _ tab яв л яется первым двумерным массивом, с которым м ы


и меем дело. По определению в 'С" двумерный масси в по существу
яв л яется одномерным массивом, каждый элемент которого является
массивом. По этому индексы запис ываются как

как в БОЛЫlшн стве языков. В остал ьном с двумерными масс и вами


можно в основном обращаться таким же образом, как в других языках.
Элементы хранятся по строкам, Т.е. при обраl.Lf'НИИ к элементам в
порядке и х размещения в памяти быстрее всего изменяется самый
пра в ый индекс.

>50
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

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


заключенных в фигурные СЮJбки ; каждая строка двумерного массива
инициализируется соответствующим ПОДCnИСЮJМ. Мы поместили в
начало массива day tab столбец из нулей для того, чтобы номера
месяцев изменялись естественным образом от 1 до 12, а не от О до 11.
Так как за ЭЮJномию памяти у нас пока не награждают, таЮJЙ способ
проще, чем подгонка индексов.

Если двумерный массив передается функц ии , то описание


соответствующего аргумента функции должно содержать ЮJЛ И чество
столбцов; ЮJличество строк HecyrцeCТBeHHo, ПОСЮJльку, как и прежде,
фактически передается указатель. В нашем ЮJнкретном случае это
указатель объектов, являющихся массивами из 13 ч и сел типа i nt . ТаКИ1\!
образом, если бы требовалось передать массив day tab функции f , то
описание в f имело бы вид:

f(day_tab) in! dау_tаЬ[2][1З);


{

Так как ЮJличество строк является HecyrцeCТBeHHЫM , то описание


аргумента в f могло бы быть таким:

и л и таким

in' (*dау_taЬ)[lЗ);

в ЮJтором говорится, что аргумент является указателем массива из 13


целых. Круглые скобки здесь необходимы, потому что квадратные
скобки [ J имеют более высокий уровень стаРll.lИнства, чем * ; как МЫ
увидим в следующем разделе, без кругл ых скобок

является описанием масс и ва из 13 указателей на целые.


Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

5.8. Массивы укаЗi!Гелей; укаЗi!Гели укаЗi!Гелей

Так как указатели сами являются переменными, то вы вполне могли бы


ожи дать использования массива указателей. Это действител ьно так. Мы
проиллюстрируем это написанием программы сортировки в

алфавитном порядке набора текстовых строк. предельно упрощенного


варианта угилиты sort операционной системы UN IX.

в л екции NоЗ мы привели функцию сортировки по Шеллу, которая


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

помощью одной операции. Мы нуждаемся в таком представлении


данных. которое бы позвол яло у,цобно И э Ф1х"ктивно обрабатывать
строки текста переменной длины.

Здесь и возникают массивы указател ей. Если подлежащие сортировке


строки хранятся одна за другой в длинном символьном массиве
(управляемом, например, функцией alloc ), то к каждой строке можно
обратиться с помощью указателя на ее первый символ. С ами указатели
можно хранить в массиве. Две строки можно сравнить, передав их
указатели функции strcmp.

Если две расположенные в неправи л ьном порядке строки должны быть


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

Процесс сортировки включает три шага:

• чтение всех строк ввода

• их сортировка
• вывод их в правильном порядке

Как обычно, лучше разделить программу на нескол ьЮJ функций в


соответствии с естественным делением задачи и выделить ведущую

функцию , управляющую работой всей программы. Давайте отложим на

>52
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

некоторое время рассмотрение шага сортировки и сосредоточимся на

структуре данных и вводе-выводе. ФУНIO.\ИЯ, осyrцeствляюJ..ЦЗЯ ввод,


должна изв лечь символы каждой строки, запомнить их и построить
массив указателей строк. Она должна также подсчитать число строк во
вводе , так как эта инф:Jрмация необходима при сортировке и выводе.
Так как ФУНIO.\ИЯ ввода в состоянии справиться только с конечным
числ ом вводимы х строк, в случае слиI..LIюм больuюro их числа она
может возвращать некоторое число, отличное от возможного числа

строк, например -1. ФУНIO.\ИЯ осyrцeСТВЛЯЮЩ'JЯ вывод, должна печатать


строки в том порядке , в каком они появляются в массиве указателей.

#dеfiлe null О
#define lines 100 /* тах lines to Ье sorted */

татО /* sort mput lines */


{
char *lineptr[lines]; /*pomters to text lines */
int nlines; /* пumbег of трш lines read */

if ((nlines = readlines(lineptr, lines)) >= О) {


sort(lineptr, nlines);
writelines{lineptr, nlines);
}
else
prinrf("input too Ыg to sort\n");

#dеfiлe mзxleп 1000

readlines(lineptr, maxlines) /* read mput lines */


char *lineptr[); /* {ог sorting */
mt maxlines;
{
int lеп. nlines;
char * р , *allocO, linе [mзxle п] ;

nlines = О ;
while ((len = getline(line, mзxlеп)) > О)
if (nlines >= mзxlines)
>53
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

return(-1);
еЕе if «р = alloc(len)) == null)
return (-1);
еЕе {
line[len-l] = '\0'; /* zap newline */
strcpy(p,llne);
llneptr[nllnes++ ] = р ;

return(nllnes);
}

С ИМВОЛ новой строки в конце каждой строки удаляется, так ч то он


никак не будет влиять на порядок. в котором сортируются строки.

writelines{lineptr, nlines) /* write оutрш lines * /


char *llneptr[];
int nlines;
{
int~

for (i = О; i < nlines; i++)


printf(' %s\n", llneptr[i]);

Существенно новым в этой программе яв л яется описание

char *llneptr[lines];

которое сообщает, что l inept r является массив ом из l in e s


элементов , каждый из которых - указатель на переменные типа c h a r .
Это означает, что l in e pt r [ i ] указатель на символы, а
*l inept r [ i ] извлекает символ.

Так как сам


l i neptr является массивом, который передается функц ии
writel i nes , с ним можно обращаться как с указателем точно таким
же образом, как в Hauмx более ранних при мерах. Тогда посл еднюю
функцию можно переписать в виде:

writelines{lineptr, nlines) /* write оutрш lines * /


char *llneptr[];
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

int nlines;
{
int i;

while ( --nliлe5 >= О)


prinrf(''%S\n'', "'lineptr++);

здесь * lineptr снач ала указывает на первую строку; каждое


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

n l ines убывает до нуля.

С правившись с вводом и выводом , мы можем перейти к сортировке.


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

уверенность, ч то он по-прежнему бу,цет работать.

50rt(V, п) /* 50rt 5trings V(O) ... v(n-l) */


char "'V[]; 1'" into increasing order "'1
int п;
{
int gap, i. j;
char "'temp;

[ог (gap = n12; gap > О; gap /= 2)


for (i = gap; i < п; i++)
[ог U= i - gap; j >= О; j - = gap) (
if (5trcmp(v[j), v[j+gap)) <= О)
bгeak;
(еmp = v[j );
v[j) = v[j+gap);
v[j+gap) = (еmp;

Так как каждый отдельный эл емент массива v (имя фJрмального


параметра , соответствующего l ineptr ) является указателем на

>55
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

символы, то и temp должен быть указателем на символы, чтобы их


было можно копировать друг в друга.

Мы написали эту программу по возможности более просто с тем, чтобы


побыстрее получить работающую программу. Она могла бы работать
быстрее, если, например, вводить строки непосредственно в массив,
ynравляемый функцией readlines , а не копировать их в l ine , а
затем в скрытое место с помощью функции al l oc . Но мы считаем, что
будет разум нее первоначальный вариант сделать более простым для
пони мания, а об ''эфfx:'ктивности'' позаботиться позднее. Все же, по­
видимому, способ, позволяющий добиться заметного ускорения работы
программы состоит не в исключении ЛИll.lНего копирования вводимых

строк. Более вероятно, что существенной разницы можно достичь за


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

быстрой сортировки.

в ле щи и N21 мы отмечали, что поскольку в циклах while и for


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

Упражнение 5-5

ПереПИl.LIИте функцию readl ines таким образом, чтобы она помещзла


строки в массив, предоставл яемый функцией main, а не в память,
ynравляемую обращениями к функции alloc . Насколько быстрее стала
программа?

5.9. Инициализация массивов указателей

Рассмотрим задачу написания функции month пате (п), которая


во зв ращает указатель на символьную строку, содержащую имя n -го

месяца. Это идеальная задача для применения внутреннего статического


массива. функция топ th пате содержит локальный массив
символьных строк и при обращении к ней возвращает указатель нужной
>56
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

строки. Тема настояl.Цf'ГО раздела - как инициализировать этот массив


имен.

cha.r *rm пth_na lП'(П) /* return na lП' of n-th rmnth */


int п;

static cha.r *nalП' [ ) = {


"illega! nюпth",
'January",
"february",
''rnarch'',
"april",
''rnay'',
'Jun",
'July",
"augusr",
"septe IТiJer",
"october",
''november'',
"december"
};
relurn ((п < 1 11 п > 12) ? naп-.. [О ] : naп-..[п] ) ;

описание массива указателей на символы пате то чн о такое же , как


аналогичное описание lineptr в примере с сортировкой.
Инициал изатором является просто список символьных строк; каждая
строка присваивается соответствуюl.Цf'Й позиции в массиве. Бол ее
точно , символы i -ой строки помещаются в какое-то иное место, а ее
указатель хранится в пате [i J. Поскольку размер массива пате не
указан, компилятор сам подсчитывает I'Dличество иниц и ализаторов и

соответственно устанав л ивает правильное чи сло.

5.10. Указатели и многомерные массивы

Начин ающие изучать яз ык "с" иногда становятся в тупик перед


вопросом о разл ичии между двумерным массивом и массивом

указателей , таким как пате в приведенном выше примере. Если


>5 7
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

имеются описания

iлl a[10][ lOJ ; iлl *b[10J ;

то а и Ь можно использовать с)[)Дным образ ом в том смысле, что как


а [ 5] (5 ] , так и Ь [ 5] [ 5] являются законными ссылками на отдельное
числ о типа in t . Но а - настоящий массив: под него отводится 100 ячеек
памяти и для нахождения любого указанного элемента проводятся
обычные вычисления с прямоугольными индексами. Для Ь , однако,
описание выделяет только 10 указател ей ; каждый указатель должен
быть установлен так, чтобы он указывал на массив цел ых. Есл и
предположить, что каждый из них указ ывает на массив из 10
элементов , то тогда где-то будет отведено 100 ячеек памяти плюс еще
десять ячеек дл я указателей. Таким образом , массив указател ей
использует несколько БОЛЫllИЙ объем памяти и может требовать
нали чие явного шага инициал изации. Но при этом возникают два
преимущества: доступ к элементу осуществляется косвенно через

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

мосут иметь различные длины. Это означает, что каждый э лемент Ь не


должен обязательно указывать на вектор из 10 эл ементов; некоторые
мосут указ ывать на вектор из двух элементов, другие - из двадцати , а

третьи мосут вообще ни на что не указывать.

Хотя мы вели это обсуждение в терминах целы х. несомненно, чаще

всего массивы указателей используются так, как мы

продемонстрировали на функции топ th пате , - для хранения

символьных строк различной длины.

Упражнение 5-6

Перепишите функции day - of - year и month - day , используя вместо


индексации указатели.

5.11. КоманДная строка аргументов

С истемные средства , на которые опирается реализация языка "с",


позволяют передавать командную строку apryмeHToB или параметров

начинающей выпол няться программе. Ко гда функция main вызывается

>58
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

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


(условно называемый arge ) указывает число аргументов в командной
строке, с которыми происходит обращение к программе ; второй
аргумент ( argv ) яв л яется указателем на массив символьных строк,
содержащих эти аргументы, по одНОМУ в строке. Работа с такими
строками - это обычное использование многоуровневых указателей.

Самую простую иллюстрацию этой возм ожности и необходимых при


этом описаний дает программа ee h o , которая просто печатает в ОдНУ
строку аргументы командной строки, разделяя их пробелами. Таким
образом, если дана команда

есоо hello, work:J

то выходом бу,цет

hello, work:J

по соглашению argv [О] является именем, по КОТОРОМУ вызывается


программа, так что a r ge по меньшей мере равен 1 . В приведе нном
выше примере a r ge равен 3, а argv [О] , argv [ 1] и argv [ 2 ]

равны соответственно " eeho ", " h e110 ," и " wor l d ". Первым
фактическим аргументом является argv (1 ], а последним
argv(arge - 1 ]. Если arge равен 1 , то за именем программы не
следует никакой командной строки аргументов. Все это показано в
eeho :

main(argc, argv) /* echo агgшne пts ; 1st vеrsюп */


int argc;
char *argv[];
{
int~

for (i = 1; i < argc; i++)


printf(' %s%c", argv[i], (i<argc-l)?" : '\n');
}

Поскол ьку argv яв ляется указателем на массив указателей, то


cyrцeCТByeT несколько способов написания этой программы,

>59
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

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


продемонстрируем два варианта.

main(argc, argv) /* echo агgшne пts; 200 version */


int argc;
char *argv[J;
{
while (--argc > О)
printf('%s%c",* ++argv, (argc > 1)?": '\n');
}

Так как argv яв л яется указателем на начало массива строк- аргументов,


то , увеличив его на 1 ( ++argv ), мы вынуждаем его указывать на
подлинный аргумент argv [ 1 ] , а не на argv [О] . Каждое
посл едующее увеличение передвигает его на сл едующий аргумент ; при
этом *argv становится указател ем на этот аргумент. Одновременно
величина a r gc уменьшается; когда она обратится в H)m.b, все аргументы
будyr уже напечатаны .

Другой вариант:

main(argc, argv) /* echo агgшne пts; З гd version */


int argc;
char *argv[ J;
{
while (- -argc > О)
printf((argc > 1)? '%5" : '%5\n", *++argv);
}

Эта версия пока з ывает, что аргумент qюрмата функции printf может
быть выражением, точно так же , как и любой другой. Такое
использование встречается не очень часто , но его все же стоит

запомнить.

Как второй пример, давайте внесем некоторые усовершенствования в


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

С ледуя утилите grep системы UNIX, давайте изменим программу так,

>6'
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

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


строки.

#dеfiлe mзxlinе 1000

main(argc, argv) /* find pattem from firs t агgumeпr */


int argc;
char *argv[];
{
char fiлe[mзxline];

if(argc != 2)
prinrl ('\Jsage: find pattem\n');
else
while (gе t1ine(fiлe, mзxlinе) > О)
if (index(line, argv[ l ] >= О )
prinrf('%s", linе);

Теперь может быть раз вита основная модель, иллюстрирующая


дальнейшее испол ьзование указателей. Предположим , что нам надо
преАУсмотреть два необязател ьных аргумента. Один угверждает:
''напечатать все строки за исключением тех, которые содержат данную
комбинацию", второй гласит: ' 'перед каждой выводимой строкой должен
печататься ее номер".

Общепринятым согл ашением в "с"- про граммах является то , что


аргумент, начинающийся со з нака мин ус , вводит необязательный
признак или параметр. Если мы , для того , чтобы сообщить об инверсии,
выберем -х, а для указания о нумерации нужных строк выберем -
n ( " номер " ) , то команда

find -х -п the

при входных данных

ПОW ic; the tirrI"


for all good IП:'П
to СОIП' {о ше а ю
01 their party.
'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Должна выдать

2:for all good tп:'п

Нужно, чтобы необязател ьные аргументы могли располагаться в


произвольном порядке, и чтобы остальная часть программы не зависела
от количества фзктически присутствующих аргументов. В частности ,
вызов функции i nde x не должен содержать ссылку на argv [2] , когда
присутствует один необязательный аргумент, и на a r gv [ 1 ] , когда его
нет. Более того, для пользователей у,цоб но, чтобы необязательные
аргументы можно было объединить в виде:

find -nx the

вот сама программа:

#dеfiлe mзxlinе 1000

main(argc, argv) /* find pattern from firs( агgumeп( */


int argc;
char *argv[];
{
сhзг line[maxline], *5;
long fiлerю = О;
int except = О , пumbег = О;
while (--argc > О && (*++argv)[O] == '-')
for (5 = argv[O]+ 1; *5 != '\0'; 5++)
5wi1ch (*5) (
са5е ' х':

except = 1;
break;

са5е 'п' :
пumbег = 1;
break;
default:
printf("find: illega! орtioп %с\n", *5);
argc = О;
break;

>6'
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

}
if (argc != 1)
pгinrf(''usage: firx:I-х -п pattem\n");
еЕе
while (getline(line, maxliлe) > О) (
linеrю + +;
if«index(line, *argv) >= О) != except) \
if(number)
printf('%Id: ", lineoo);
pгintf('%s", line);
}
}

аргумент argv увел и чивается перед каждым необязательным


аргументом, в то время как аргумент argc уменьшается. Если нет
о шибок, то в конце ци кла величина argc должна равняться 1, а * argv
до лжно указывать на заданную комб ин ацию. Обратите внимание на то,
что * ++a rgv яв л яется указателем аргументной строки; (*+ +a rgv)
[О] - ее первый символ. Крутлые скобки здесь необходимы, потому что
без них выражение бы приняла совершенно отличный (и
неправильный) вид * ++ (argv [О]) . Другой прав ильной qюрмой
была бы ** ++a rgv .

Упражнение 5-7

Нап иши те программу add, вы чи сляющую обратное польское


выражение из командной строки. Например,

add23 4 + *
вычисляет 2*(3+4).

Упражнение 5-8

Модиф1цируйте программы entab и detab (указанные в качестве


ynражнен ий в ле кuии No1) так, чтобы они пол учали список
табynяционных остановок в качестве аргументов. Если аргументы
отсутствуют, используйте стандартную установку табуляций.

>63
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Упражнение 5-9

PaCll.В1pЬTe en tab и detab таким образом, чтобы они воспринимал и


сокраl.lf'ННУЮ нотацию

entab m + п

означающую таб)чtяционные остановки через каждые n столбцов,


начиная со столбца т . Выберите у,цоб ное (для пользователя) поведение
функции по умолчанию.

Упражнение 5-10

НаПИll.В1те программу для функции tai l, печатаюl.Цf'Й последние n


строк из своего файла ввода. Пусть по умолчанию n равно 1 0, но это
числ о может быть изменено с помощью необязательного аргумента, так
что

tail-n

печатает последние n строк. Программа должна де йствовать


рационально , какими бы неразумными ни были бы ввод или значение
п. Составьте программу так. чтобы она оптимальным образом
использовала достynНУЮ память: строки должны храниться, как в

функции sort, а не в двуме рном массиве ф1ксированного размера.

5.12. Указатели на функции

в языке "с" сами функции не яв ляются переменными , но имеется


во зм ожность определить указатель на функцию, который можно
обрабатывать, передавать другим функциям , помещать в массивы и Т.д.
Мы прои ллюст рируем это, проведя мо.циф1кацию написанной ранее
программы сортировки так. чтобы при задании необязательного
аргумента -n она бы сортировала строки ввода числе нно, а не
лекс икографически.

Со ртировка часто состоит из трех частей - сравнения, которое


определяет ynорЯДочивание л юбой пары объектов, перестановки,

'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

изменяющей и х порядок. и алгоритма сортировки, осуществляющего


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

функции сравнен ия и перестановки , мы можем организовать


сортировку по различным критер иям . Именно такой подход
используется в нашей новой программе сортировки.

Как и прежде, лексикогра4ическое сравнение двух строк осуществляется


функцией strcmp, а перестановка функцией swap ; нам нужна еще
функция numc mp , сравнивающая две строки на основе численного
значения и возвращающая условное указание того же вида, что и

strcmp. Эти три функции описываются в main и указатели на них


передаются в sort . В свою очередь функция so r t обращается к эт им
функциям через их указатели. мы урезали обработку Оll.В1бок в
аргументах с тем, чтобы сосредоточиться на главных вопросах.

#define lines 100 /* тах пumbег of lines


{о Ье sorted * /

main(argc, argv) /* sort input lines */


int argc;
char *argv[J;
(
char *lineptr[lines]; /* pointers to text lines */
int nlines; /* пl.lIТbег of input lines read */
int s tfс ЩJО, пштх:mpО ; /* соmpаrsiоп functions */
int swapO; /* ехсhaпgе ft.mсtiоп */
int Пl.lI1Y'гic = О; /* 1 if Пl.lI1Y'гic sort */

if(argc>l && argv(l](O] == '-' && argv[l](l]=='n')


пl.lI1y'гk: = 1;
if(nlines = readlines(lineptr, lines» >= О) (
if (numeric)
sort(lineptr, nlines, пштх:mp , swap);
еЕе
sort(lineptr, nlines, strcmp, swap);
writelines(lineptr, nlines);
} еЕе
>65
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

prinrf("input too blg to sort\n");

Здесь strcmp, numcmp и swap - адреса функций; так как известно,


что это функции, операция & здесь не нужна совершенно анал оrnчно
тому, как она не нужна и перед именем массива. Передача адресов
функций ор гани зуется компилятором.

Второй шаг состоит В модификации so r t :

sort(v, п, СО пlp , exch) j'" sort stringc; v[Q] ... v{n-l] "'!
сhзг "'v{]; j'" into increasing oгde r "'!
int п;
iлt (*со тр)() , (* ехсЬ)() ;
(
int ga p, i. j;

fo r(gap = n/2; gap > О ; gap /= 2)


fo r(i = gap; i < п; i++)
for(j = i- gap; j >= О; j - = gap) (
Щ (*со mp )(v[j], v[j+gap]) <= О)
bгeak ;
(*exch)(&v[j], &v[j+ga p]);

Здесь слеАУет обратить определенное внимание на описания. описание

iлt (*со тр)()

говорит, что соmр явл яется указателем на функцию, которая возвращает


значение типа in t . Первые кругл ые скобки здесь необходимы ; без них
описание

iлt *со трО

говори л о бы , что соmр является функцией , возвращающей указатель на


целые, что , конечно , совершенно другая вещь.

Использование соmр в строке

>6'
Б.8. К<'рниган, Д.М. Ричи Язык "JIOграм""ирования С

if «*СОЩJ )(v(j), v(j+gap)) <= О)

полно стью согласуется с описанием: сотр - указатель на фующию ,


* сотр - сама функция, а

(*СОЩJ)(v(j), v(j+gap))

- обращение к ней. круглые скобки необ)[)Димы для пр ав ил ьного


объединения компонентов.

Мы уже при водил и функцию 5 trcmp, сравнивающую две ст роки по


пер в ом у численному значению:

num:mp(sl, 52) /* соmpаге 51 аnd 52 numerically */


сhзг *51, *52;

double atof{), Уl, у2;

Уl = .'ol(sl);
У2 = .'0I(s2);
if(vl < У2)
relurn( -1);
else if(vl > У2)
relurn(l);
else
relurn (О);

Заключительный шаг состо ит в добав лении функции swap,


пер еставляющей два указателя. Это леf1(() сделать, непосредстве н но

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

5wap(px, ру) /* inrercha.nge *рх аnd *ру */


char *рх(), *ру();
{
сhзг *temp;

temp = *рх;

*рх = *ру;
*ру = {еmp ;
>6 '
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Имеется множество других необязательных аргументов, которые могуг


быть включены в программу сортировки: некото рые из них составляют
ин тересные ynражнения.

Упражнение 5-11

Модиф1цируйте sort таким образом , чтобы она работала с меткой -r,


указываю щей на сорт иро вку в обратном (убывающем) порядке.
Конечно, -r должна работать с - n.

Упражнение 5-12

Добавьте необязател ьный аргумент -f, объединяющий вместе


прописные и строчные буквы, так чтобы различие реЛ1СТРОВ не
учитывалось во время сортировки: данные из верхнего и нижнего

ре гистров сорти руются вместе, так что буква 'а ' прописное и 'а'
строчное оказываются соседними а не разделенными целы м

алфавитом.

Упражнение 5-13

Добавьте необязательный а р гумент -d ("словарное ynорядочивание''),


при наличии которого сравниваются только буквы, чи сла и пробелы.
Позаботьтесь о том , чтобы эта функция работала и вместе с -f.

Упражнение 5-14

Добавьте возможность обработки пол ей , так чтобы можно было


со ртировать поля внугри строк. Каждое п оле должно сортиров аться в
соответствии с независимым набором необязательных аргументов.
(предметный указатель этой книЛ1 сортировался с помощью apryмeHToB
-df для категории указателя и с -n для номе ро в страни ц).

>6'
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Структуры

Дается описание структур и методов работы с ними.

6. Структуры

Ст руктура - это набор из одной или более переменных, возможно


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

Традиционным примером структуры яв ляется учетная карточка


работающего: "служащий" описывается набором атрибутов таких, как
фамилия, имя, отчество (ф.и.о.) , адрес, код социального обеспечения,
зарплата и Т.д. Некоторые из этих атрибутов сами могут оказаться
структурами: ф.и.о. Имеет несколько компонент, как и ад рес, и даже
зарплата.

структуры оказываются полезными при организации сложных данных

особенно в больших программах, поскольку во многих с итуациях они


позволяют сгруппировать связанные данные таким образом, что с ними
можно обраl.LflТЬСЯ, как с одним целым, а не как с отдельными
объектами. В этой лекции мы постараемся продемонстрировать то, как
используются структуры. программы, которые мы для этого бу,цем
использовать, БОЛЫllе, чем многие друтие в этой книге , но все же
достато чно умеренных размеров.

6.1 . Основные сведения

Давайте снова обратимся к процедурам преобразования даты из л екции


NQS. Дата состоит из нескольких частей таких, как день, месяц, и год, и,
во зм ожно, день года и имя месяца. Эти пять переменных можно
объединить в одну структуру вида:

struct date {
int day;
int rтюпш;

>6'
Б.8. К<'рниган, Д. М. Ричи Язык "JIOграм""ирования С

int уеа г;
int yearday;
char nюп_name[4];
};

описание структуры, состоящее из заключенного в фигурные скобки


списка описаний, начинается с ключевого слова struct . За словом
struct может следовать необязательное имя, называемое ярлыком
структуры (здес ь это da te ). Такой ярлык именует структуры этого вида
и может использоваться в дальнейшем как сокращенная запись
подробного описания.

Элементы или переменные, ynомянугые в структуре, называются


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

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

struct { ... } x,y,Z;

синтаксически аналоrnчен

int x,y,z;

в том смысле, что каждый из операторов описывает х, у и z в качестве


переменных соответствующих типов и приводит К выделению для них

памяти.

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

привqд,ит к выделению какой-либо памяти; оно только определяет


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

struct date d;
>70
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

определ яет переменную d в качестве структуры типа da te . ВнеlilliЮЮ


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

за ее определением список иници ализаторо в для ее ЮJмпонент:

strucldaled={ 47
" 1776, 186, "J'uJ"},,
Член определенной структуры может быть указан в в ыр ажен ии с
помощью ЮJнструкции ви да

имя структуры. Член

Операция указа ния чл ена структуры" ." связывает имя структуры и имя
чл ена. В качестве примера определим 1 еар (признак ВИСОЮJсности
года) на основе даты, на)[)ДЯщей ся в структуре d ,

leap = d.year % 4 == О && d.year % 100 != О


11 d,year % 400 == О;

и л и пров е рим имя месяца

if (Slrсmp(d,nюп_name , "aug') == О) .. ,

Или преобра зуем первый символ имени месяца так, чтобы оно
начиналось со стро чной буквы

структуры могуг быть в л оженными; учетная карточка служащего может


факти чески выглядеть так:

struct реrsоп {
char nalП'(nalП'size];
char address[adrsize];
Ioпg zipcode; /* почтовый ин декс */
Ioпg ss_пшmег; /* юд соц. Обеспечения */
double salary; /* за рп лата */
struct date birthdate; /* дата ро жден ия */
struct date hiredate; /* дата поступления
на работу */
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

};

структура p er son содержит две структуры типа date . Если мы


определим е т р как

struct person еmp;

то

еmp.Ыгthdаtе.пюпth

будет ссылаться на месяц рождения. Операция указания члена


структуры" ." ассоциируется слева направо.

6.2. Структуры и функции

в языке "С" cytЦf'CТByeT ряд ограничений на использование структур.


Обязательные правила заключаются в том, что единственные операции,
которые вы можете проводить со структурами, состоят в определении

ее адреса с помощью операции & и достynе к одному из ее членов. Это


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

структуры и функции все же могут с удобством работать совместно. И


наконец , автоматические структуры, как и автоматические массивы, не

мосут быть инициализированы; инициал изация возможна только в


случае внешних или статических структур.

Давайте разбе рем некоторые из этих вопросов, переписав с этой целью


функции преобразования даты из предыдyI.Цf'Й лекции так, чтобы они
использовали структуры. Так как правила запрещают непосредственную
передачу структуры функции, то мы должны либо передавать отдельно
компоненты, либо передать указатель всей структуры. Первая
во зм ожность демо нстрируется на примере функции day o f year , кafi
мы ее написал и в лекции No5:

d.yearday = day_of...year(d.year, d.m:шth, d.day);


Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

друтой способ состоит в передаче указателя. если мы опишем


h i redate как

struct date hiredate;

и перепишем day_of year нужным образом, мы сможем тогда


написать

hiredate .yearday = day_oCyear(&hiredate);

передавая указатель на h iredate функции day of year . функция


должна быть модифицирована, потому что ее аргумент теперь является
указателем, а не СЛИСЮJм переменных.

day_ oCyear(pd) /* ser day of year from пюпth, day */


struct date *pd;
{
int ~ day, leap;

day = pd- >day;


leap = pd- >year % 4 == О && pd- >year % 100 != О
11 pd- >year % 400 == О;
for (i =1; i < рd- > nюпth; i++)
day += day_tab[leap)[i);
ге tшп(dау) ;
}

описание

struct date *pd;

говорит, что pd является указателем структуры типа da te . Запись,


локазанная на лримере

pd- >year

является новой. Если р - указатель на структуру, то

р- > член структуры


Б.8. К <'рниган, Д. М. Р ич и Я ЗЫ К "JIOграм""ирования С

обращзется к конкретному члену. (Операция -> - это знак минус, за


которым слеАУет з нак" > ".)

Так как pd указывает на структуру, то к члену yea r можно обратиться и


следую щим образом

(*pd).year

но указатели структур используютс я на столько -> часто, что запись

оказывается у,цобн ым со краl.Цf'нием. круглые скобки в (* pd) . y e ar


необходи м ы , потому что операция указания члена стр уктуры ста рше ,
чем *. Обе о пе р а ции , " - > "и" .", ассоциируются слева нап раво , так что
конструкции слева и справа экв ивален тны

p- >q- >ment> (p- >q)- >memb


emp.birthdate.month (emp.birthdate).month

Для пол ноты ниже приводится другая функция, mont h day ,


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

rmnth_day(pd) /* S e( rmnth and day from day of year */


struct date *pd;
{
int i. leap;

leap = pd- >year % 4 == О && pd- >year % 100 != О


11 pd- >year % 400 == О ;

pd- >day = pd- >yearday;


юг (i = 1; pd->day > day_tab[leap)[i); i++)
pd->day -= day_tab[leap)[i);
рd- > m:юth = ~
}

Операции р аботы со структурами" -> " и " ." нарЯAV со () для списка
аргументов и [] для индексов находятся на самом верху иер а рхии
cтapUМHCТBa операций и , следовательно , связываются очень крепко.
Если, например, и меется описание

struct {
int х;
'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

int "'у;
} "'р;

то выражение

++ р- > х

увеличивает х , а не р , так как оно э квивалентно выражению ++ (р­


>х) . Для изменения порядка выполнения операций можно
использовать кругл ые скобки: ( + +р) ->х увеличивает р до доступа к х,
а (р++) ->х увеличивает р после. ( Круглые скобки в посл еднем
случае необязательны . Почему? )

Со вершенно аналогично * р->у извлекает то, на что указывает у ; *р­


>у++ увеличивает у после обработки того, на что он указывает (точно
так же, как и * S++) ; (*р->у) ++ увеличивает то, на что указывает у ;
*р ++ ->у увеличивает р после выборки того, на что указывает у .

6.3. 11ассивыструктур

структуры особенно под)[)ДЯт для управления массивами связанных


переменных. Рассмотрим , например, программу подсчета числа
вхождений каждого ключевого слова языка "С". Нам нужен массив
символьных строк для хранения имен и массив целых для подсчета.

одна из возм ожностей состоит в использовании двух параллельных


массивов keyword и keycount :

char *keyword [nkeysJ; int keycount [nkeysJ;

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


другой организации. Каждое ключевое слово здес ь по существу является
парой:

char "'keyword; inr keycourn;

и, следовательно, имеется масс ив пар. описание структуры

struct key {
char "'keyword;
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

int kеусоuш;
} keytab [nkeysJ;

определяет массив keytab структур такого типа и отводит для них


память. Каждый элемент массива является структурой. Это можно было
бы записать и так:

strucl key {
char "'keyword;
int keycounr;
};
strucl key keytab [nkeysJ;

Так как структура keytab фактически содержит постоянный набор


имен, то легче всего инициал и з ировать ее один раз и для всех членов

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


предыдущим инициализациям - за определ ением следует заключенный
в фигурные скобки список инициал изаторов:

struct key {
char "'keyword;
int keycount;
} keytab[] = {
'break", О,
"case", О ,
"char", О,
"continue", О ,
"default", О ,
/* ... "'/
''unsigned'', О ,
"while", О
};

Инициализаторы перечисляются парами соответственно членам


структуры. Было бы более точно заключать в фигурные скобки
инициализаторы для каждой "строки" или структуры следующим
образом:

{ 'bгeak", О },
{ "case", О },
",
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Но ко гда инициал изаторы яв л яются простыми переменными или


символьными строками и все они присутствуют, то во внутренних

4иrypных скобках нет необходимости. Как обычно, компилятор сам


вычислит число элементов массива keytab, если инициализаторы
присутствуют, а скобки (] оставлены пустыми.

Программа подсчета ключевых слов начинается с определения массива


key tab . ВедyI.ЦaЯ программа читает свой файл ввода, последовательно
обращзясь к функции getword, которая изв лека ет из ввода по одному
слову за обраll.fние. Каждое слово Иll.fтся в массиве key tab с
помощью варианта функции бинарного поиска, написанной нами в
ле кции NоЗ. (Конечно, чтобы эта функция работала, список ключевых
слов должен быть расположен в порядке возрастания).

#define maxword 20
#dеfiлe nkеУ' C5izeo~keytab) / 5izeof(5truCl key))

mainQ /* count "с " keywords */


{
int п, t;
char word[maxword];

while CCl = gelword(word,maxword)) != EOF)


if (l == letter)
Щ(п = binary(word,keytab,nkeys)) >= О)
keytab [п). keYC0Unl ++;
for (п =0; n < nkeys; п++)
if (keytab[n).keycoUnl > О)
printQ' %4d %51n",
keytab[n).keyco Unl, keytab[n).keyword);

binary(word, шЬ, п) /* find word in taЬ[О) ... tаЬ[П-l) */


char *word;
5trOCt key [аЬ[];
int п;
{
int low, high. mid, cond;
m
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

low = О;
high = n-l;
while (Iow <= high) (
mi:I = (Iow+high) / 2;
Щ(соnd = stтcmp(word , tab(mid).keyword)) < О)
high = mi:I- 1;
еЕе if(cond > О)
low = mid + 1;
еЕе
relurn (mi:I);
}
relurn(-1);
}

Мы вскоре приведем функцию getword ; пока достато чн о сказать, что


она возвращает let ter каждый раз, как она на)[)Дит слово, и копирует
это слово в свой первый аргумент.

Величина nkeys - это количество ключевых слов в массиве keytab .


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

Но, поскольку размер этого масс и ва полностью определен к моменту

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


элементов просто есть

size о! keytab / size о! strucl key

дел о в том, что в языке "С" предусмотрена унарная операция sizeof,


выполняемая во время компиляции, которая позволяет вычислить

размер любого объекта.

Выражение

sizeof(objeCl)

'"
Б.8. К <'рниган, Д. М. Р ич и Я ЗЫ К "JIOграм""ирования С

выдает целое, равное размеру указанного объекта. (Разме р определяется


в несп еци ф-щированны х единицах. называемых "бай тами", которые
и ме ют тот же размер , что и переменные типа char ). Объект может
б ыть фактич еско й переме нной , массивом и структурой, и ли им е нем
основного т ипа , как i nt или doubl e , или именем прои зво.цного типа,
как ст руктура. В нашем случа е чи сло ключевых слов р авно ра зме р у
масс и ва, деленном у на р азм е р одного эл ем ента масси в а. Это
вычи сле ние используется в ут в е р жде нии #defi n e для установления
значе ния n ke ys :

#dеfiлe nkеУ' (s izeo~keytab) I sizeof(struCl key))

Теперь перейдем к функции g e tword . Мы фактиче ски н апи сали более


общий вариант функции g e t WQ rd , чем необходимо для это й
программы , но он не на много более сл ожен. функция getword
во зв р а щзет следующее "слово" и з ввода, где сло вом считается л ибо
строка букв и ци@, начинающихся с буквы, либо отдельный с имвол.
тип объекта в о зв ращзется в качестве значения функции; это - letter,
если н айдено сло во , EOF для конца фай ла и сам символ, если он не
букве н н ый.

gNword(w, lim) /'" gN next word from inрш "';


сhзг "'w;
intliщ
{
int с , t;
if(type(c=*w++ =gelch()) !=letler) {
"'w='\O';
return(c);

while (--lim > О) {


l = [уре( с = *w++ = gelch());
if (t ! = leller && l ! = digit) {
uлgеlсh( с);
break;
}
}
*(w-l) = '\0';

'"
Б.8. К<'рниган, Д. М. Ричи Язык "JIOграм""ирования С

relurn(lelter);
}

функция getwo r d использует функции getch и u n getc h, ЮJторые Мl


написали в л екции N!!4: ЮJгда набор алфавитных символов прерывается,
функция getwo r d получает один лиlШlИЙ символ. В результате вызова
u n getc h этот символ помещается назад во ввод для следующего
обращения.

функция getword обращзется к функции type для определения типа


каждого отдельного символа из файла ввода. Вот вариант,
справедливый ТОЛЬЮJ дЛЯ алфавита ASClI.

type(c) /* гeturn type of ascli chaгacteг */


int с;
{
if (с >= 'а' && с <= 'z' ll с>= 'А' && с <= 'Z')
гeturn(letteг);
еЕе if (с >= 'о' && с <= '9')
геlurn(dЩit);
еЕе
relurn(c);

С имволические ЮJнстанты l etter и digi t могут иметь любые


значения, ЛИIl.lЬ бы они не вступали в ЮJНфлИКТ с символами,
отличными от буквенно-цифровых, и с EOF ; очевидно возможен
следующий выбор

#define letteг 'а'


#define dЩit 'О'

функция getwo r d могла бы работать быстрее, если бы обращения к


функции t уре были заменены обращениями к соответствующему
массиву t ур е [ ] . в стандартной библиотеке языка "С" предусмотрены
макросы isa l pha и i sdig i t , действующие необходимым образом.

Упражнение 6-1

>80
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Сделайте такую модификацию функции getword и о цените, как


изменится скорость работы программы.

Упражнение 6-2

Нап иши те вариант функци и type , не зав и сящий от конкретного набора


символов.

Упражнение 6-3

Напишите вариант программы подсчета ключевых слов, который бы не


учитывал появления этих слов в заключ енных в кавычки строках.

б.4. УказатеJШ на структуры

Чтобы проиллюстрировать некоторые соображения, связанные с


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

указатели, а не индексы массивов.

BHel.LIНee оп и сание массива key ta b не нужно изменять, но функц ии


main и binary требуют мqд,и4икац ии .

mainО /* count с keyword; pointer version */


(
int t;
char word(maxword];
strucl key *binary(), *р;
while ((l = gelword(word, maxword;) !=EOF)
ti (t==lelter)
ti ((р = Ьiлarу( word,keytab,nkeys)) !=null)
p- >keycount++;
for (p=keytab; p>keytab + nkeys; р + +)
ti(p- > kеУСО Unl > О)
printf{'"o/04d %s/n" , p- > kеусо Lll1t, p- >keyword);
}
struct key *binary(word, тЬ, п) /* find word */
char *word /* in taЬ[Q] ... tаЬ[П-l] */
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

strUC! key !аЬ [];


int п;

int cond;
strUC! key *Iow = &!аЬ[О] ;
strUC! key *high = &!аЬ[П-l];
stпк:t key *mid;
while (Iow <= high) {
mid = Iow + (high-Iow) / 2;
if ((сооо = strcmp(word, mid->keyword)) < О)
high = mid - 1;
else if(coOO > О)
kJw = mid + l ;
else
re!urn(mid);

re!urn( null);
}

Здесь имеется несколько моментов, которые стоит отметить. Во-первы х,


описание функции binary должно указывать, что она возвращает
указатель на структуру типа key, а не н а целое ; это объявляется как в
функции main , таки в binary . Если функция binary находит слово,
то она в о зв ращает указатель на него; если же нет, она возвращает null .

Во- вторы х, все обращения к элементам массива key tab


осуществляются через указатели. Это влечет за собо й одно
существенное изменение в функции binary: с редний эл емент больше
нел ьзя вычислять просто по формуле

mid = (Iow + high) / 2

потому что сложение двух указателей не дает какого-нибудь полезного


ре зультата (даже после дел ения на 2) и в де йств ител ьности является
незако нным. Эту формулу надо замен ить на

mid = Iow + (high-Iow) / 2

в ре зультате которой mid становится указател ем на эле мент,

'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

расположенный посередине между lo w и hig h.

Вам также слеАУет разобраться в инициализации low и h i gh .


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

объекта; именно как мы здесь и постynили.

В функции main мы написали

for (p=keytab; р < keytab + nkeys; р ++ )

Если р является указателем структуры, то любая арифметика с р


учитывает фзктический размер данной структуры, так что р++
увеличивает р на нужную величину, в резynьтате чего р указывает на

слеАУЮЩИЙ элемент массива структур. Но не считайте, что размер


структуры равен сумме размеров ее чле нов, - из-за требований
выравнивания для разл ичных объектов в структуре MOryr возникать
'дыры".

И, наЮJнец, несЮJЛЬЮJ второстепенный вопрос о фJрме записи


программы. Если возвраl.lflемая функцией величина имеет тип, как,
например, в

struct key "'blnary(word, {аЬ , п)

То может оказаться, что имя функции тру,цно выдел ить среди текста. В
связи с эт им иногда используется другой стиль записи:

struct key '"


blnary(word, [аЬ, п)

Это главным образом дело вкуса; выберите ту фJрму, ЮJторая вам


нравится, и приде рживаитесь ее.

б.5 . Структуры, ссылающиеся на себя

Предположим , что нам н адо справиться с более общей задачей,


состоя~й в пqдсчете числа появ лений всех слов в неЮJТОРОМ фзйле
ввода. Так как список слов заранее не известен, мы не можем их
ynорядочить у,цобным образом и использовать бинарный поиск Мы
" 3
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

даже не можем осyrцeств л ять последовательный просмотр при


поступ л ении каждого слова, с тем чтобы установить, не встречал ось л и
оно ранее; такая программа будет работать вечно. (Более точно,
ожидаемое время работы растет как квадрат числа вводимых слов). Как
же нам организовать программу, чтобы справиться со списком
произвольных слов?

Одно из решений состоит в том, чтобы все время хранить массив


поступающих до сих пор слов в упорядоченном виде, помещая каждое

слово в нужное место по мере их поступления. Однако это не следует


делать, перемещая слова в линейном массиве, - это также потребует
слиll.lкDм много времени. Вместо этого мы используем структуру данных,
наз ываемую дво ичным деревом.

Каждому новому слову соответствует один ''узел'' дерева; каждый узел


содержит:

указател ь текста слова

с четчи к числа появлений

указател ь узла левого потомка

указател ь узла правого пото мка

Никакой узел не может иметь более двух детей; возможно отсутствие


детей или наличие только одного потомка.

Узлы создаются таким образом, что левое под,церево каждого узла


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

под,церево только те слова, которые больше. Чтобы определить,


находится ли новое слово уже в дереве, начинают с корня и сравнивают

новое слово со словом, хранящимся в этом узле. Если слова совпадают,


то вопрос решается утвердительно. Если новое слово меныие слова в
де реве , то переXDДЯТ к рассмотрению левого потомка ; в противном

случае исследуется правый потомок. Если в нужном направлении


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

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

следует поместить новое слово. Поскольку поиск из любого узла

'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

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

CyrцeCТBY яв ляется рекурсивным. В соответствии с этим наиболее


естественно использовать рекурсивные процедуры ввода и вывода.

Возвращзясь назад к описанию узла, ясно, что это будет структура с


четырьмя компонентами:

struct tnode { /* the basic node '" /


char "'word; /* points {о the text */
int соunt; /* пumbег of occurтences "'/
struct trюdе *Ieft; /* left child */
struct trюdе *right; /'" right child */
};

Это ''рекурсивное'' описание узла может показат ься рискованным, но на


самом деле оно вполне корректно. структура не имеет права содержать

ссылку на саму себя, но

struct tnode *left;

описывает 1е f t как указатель на узел, а не как сам узел.

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


конечно , иметь в распоряжении набор написанных нами ранее
процедур , обеспечивающих нужные действия. Мы имеем в виАУ
функцию getword для извлечения каждого слова из файла ввода и
функцию a l loc для выделения места для хранения слов.

Ведущая программа просто считывает слова с помощью функции


g e tword и помещзет их в де рево , используя функцию t r e e.

#dеfiлe IТ\Зxwогd 20
mainО /* word freguency CO Lll1t */
{
struct tnode *root, *treeO;
сhзг word[maxword];
int t;
root = null;
while «t = getwoгd(word, IТ\Зxwогd)) != EOF)
if (t == letler)
"5
Б.8. К<'рниган, Д.М. Ричи Язык "JIOграм""ирования С

гоо! = tree(root, word);


treeprint(root);

функция tree сама по себе проста. Сл о в о передается функцией main к


в е рхнему .УРовню (корню) дерева. На каждом этапе это слово
сравнивается со словом, уже хр аня щим ся в этом узле, и с помощью

р екурс ивного обращения к t ree просачивается вниз л ибо к левому,


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

struct tnode "'иее(р, w)


/'" install w at ог below р "'/
stroct tnode "'р;
char "'w;
{
struct tnode "'tallocO;
char "'strsaveO;
int сооо;
if (р == пuП) { 1* а new word
has arrived '" /
Р == tallocO; /'" make а new rюdе "'/
p- >word = strsave(w);
p- >count = 1;
p- >Ieft = p- >righl = пull;
} e~e if«cond = strсщ>(w, p- >word)) == О)
p- >count+ +; /'" repeated word "'/
else if(coOO < 0)/'" Iower goes into left subtree "'/
p- >Ieft = tree(p->Ieft, w);
еБе /'" greater into right subtree '" /
p- >righl = tree(p- >righl, w);
relurn(p);
}

",
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Память для нового узла выделяется ФУНIO.\иеЙ talloc , ЯВЛЯЮI.l.fЙся


адаптацией для данного случая ФУНIO.\ИИ alloc , написанной нами
ранее. Она возвращает указатель свободного пространства , приго.цного
для хранения нового узла дерева. (Мы ВСЮJре обсу,цим это подробнее).
Новое слово ЮJпируется ФУНIO.\иеЙ s t r s а ve в скрытое место, счетчик
инициализируется единицей, и указатели обоих ПОТОМЮJВ полагаются
равными н улю. Эта часть программы выполняется ТОЛЬЮJ при
добавлении нового узла к ребру дерева. Мы здесь опустили проверку на
ошибки возвращаемых функций strsave и talloc значений (что
неразумно для пракrически работающей программы).

ФУНIO.\ия tr e ep r i n t печатает дерево , начиная с левого померева; в


каждом узле сначала печатается левое пqцдерево (все слова, которые
младше этого слова), затем само слово, а затем правое под,церево (все
слова, ЮJторые старше). Если вы неуверенно оперируете с рекурсией,
нарисуйте дерево сами и напечатайте его с помощью функции
treepr i nt ; это одна из наиболее ясных рекурсивных процедур,
ЮJторую можно найти.

treepгinr (р) /* pгint иее р гecuгsively */


stпк:t tnode *р;
{
if (р ! = null)
rreeprint (p- >left);
pгintf( 'o/04d %s\n", р- >count, р- > woгd);
rreeprint (p- >right);
}

ПрактичеСЮJе замечание: если дерево становится


''несбаланс ированным'' из-за того, что слова поступают не в случайном
порядке , то время работы программы может расти слишком быстро. В
»уДшем случае, ЮJгда поступающие слова уже упорядочены, настоящая

программа осytЦf'ствляет дорогостоящую имитацию линейного поиска.


Существуют разл ичные обобщения дво ичного дерева, особенно 2-3
де ревья и а v1 деревья, ЮJторые не ведут себя так ''8 »уДших случаях", но
мы не бу,цем здесь на них останавливаться.

Прежде чем расстаться с этим примером , уместно сделать неБолыlюe

'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

отступ л ение в связи с вопросом о распределении памяти. Ясно, что в


программе желательно иметь только один распределитель памяти , даже

если ему приходится размещать различные виды объектов. Но если мы


хотим использовать один распределитель памяти для обработки
запросов на выделение памяти для указателей на переменные типа c h a:
и для указателей на struct tnode , то при этом возникают два
вопроса. Первый: как выпол нить то существующее на БолыlD1ствеe
реал ьных маll.lИН ограничение, что объекты определеных типов должны
удовлетворять требованиям выравнивания (например, часто целые
должны размещаться в четных адресах )? Второй: как организовать
описания, чтобы справиться с тем, что функция al l oc должна
во зв ращзть разл ичные виды указателей?

Вообще говоря , требования выравнивания легко выполнить за счет


выделения некотороro ЛИl.LIНеro пространства, просто обеспечив то,
чтобы распределитель памяти всегда возвращзл указатель,
удовлетворяющий всем ограничениям выравнивания. Например , на
РDР-ll достаточно, чтобы функция alloc всегда возвращзла четный
указатель, поскольку в четный адрес можно поместить любой тип
объекта. Единственный рас.ход при этом - ЛИl.LIНий символ при запросе
на нечетную длину. АнаЛОЛ1чные действия предпринимаются на других
MallD1Hax. Таким образом, реал изация a l loc может не оказаться
переносимой, но ее использование будет переносимым. функция all о с
из ле кции Nq5 не предусматривает никакого определенного
выравнивания ; в ле кции No8 мы продемонстрируем, как правильно
выпол нить эту задачу.

Вопрос описания типа функции alloc является мучительным для


любого языка, который серьезно относится к проверке типов. ЛУЧllD1Й
способ в языке ''С'' - объявить, что alloc возвращзет указатель на
переменную типа char, а затем явно преобразовать этот указатель к
желаемому типу с помощью операции перевода типов (на дан ный
момент в таких ситуациях в соответствии с наиболее
распространённым стандартом следует возвращать void *, то есть

указатель на void). Таким образом, если описать р в виде

cha.r *р;

то

'"
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

(Struct trюd е *) р

преобразует его в выражениях в указатель на структуру типа tnode .


С ледовательно , функцию ta l loc можно записать в виде:

struct tnode "'tallocO


{
char *allocO;

return ((struct tnode *) a llo c(sizeo~struct tnode)));


}

это бол ее чем достаточно для работающих в настоящее время


компиляторов , но э то и самый безопасный пyrь с учетом 6y,zJ.yщeго.

Упражнение б-4

Напишите программу, которая читает 'С"- программу и печатает в


алфавитном порядке каЖАУЮ групп у имен переменных, которые
совпадают в первых семи символах, но отличаются где-то дальше .

(Сдел айте так, чтобы 7 бы л о параметром).

Упражнение б-5

Напишите программу выдачи перекрестных ссы л ок. Т.е. программу,


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

печатает список номеров строк. в КDTopыe это сл ово входит.

Упражнение б-б

Напишите программу, КDторая печатает сл ова из своего файла ввода,


расположенные в порядке убывания частоты их появ л ения. Перед
каждым сл овом напечатайте число его появ л ениЙ.

б . б . Поиск в таблице

Для иллюстрации дальнейших аспектов использования структур в этом


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

'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

представителем подпрограмм управления символьными таблицами


макропроцессора или юмпилятора. Рассмотрим , например, оператор
#de f in e языка 'С". Когда встречается строка вида

#dеfiлe yes 1

то имя уе s и заменяющий текст 1 помещаются в таблицу. Позднее ,


югда имя yes появляется в операторе вида

inword = yes;

Оно должно быть замеl.Цf'НО на 1.

Имеются две основные процеАУРЫ, юторые управляют именами и


заменяющими их текстами. функция instal l (s t) записывает имя s
I

и заменяющий текст t в таблицу; здесь s и t просто символьные


строки. функция l oo k up ( s) ищет имя s в таблице и возвращает л ибо
указатель того места, где это имя найдено , л ибо null , если этого имени
в таблице не оказалось.

При этом используется поиск по алгоритму хеlШ1рования - поступаЮl.Цf'е


имя преобразуется в маленьюе положительное число, которое затем
используется для индексации массива указателей. Элемент массива
указывает на начало цепочны х блоков, описывающих имена, которые
имеют это значение хеlШ1рования. Если никакие имена при
хеlШ1ровании не получают этого значения, то элементом массива будет
null .

Блоком цепи является структура, содержащая указатели на


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

struct nlist { /"" bask: {аЫе entry "" /


char ""nalП';
char ""def,
struc( nlist ""next; /"" next е пау in chain ""/
};

Массив указателей это просто


>9'
Б.8. К<'рниган, Д. М. Ричи Язык "JIOграм""ирования С

#dеfiлe hashsize 100


statК: struct nlist *hashtab[hashsize] /* poinrer тЫе */

Значение функции хеширования, используемо й обеими функциями


lookup и install , получается просто как остаток от деления суммы
символьных значений строки на размер массива. (Это не самый лучший
во зм ожный алгоритм, но его достоинство состоит в исключительной
простоте).

hash(s) /* fonn hash value for string */


char *s;
{
int has hva~

for (hashval = О ; *s != '\0'; )


hashval += *s+ +;
return(hashval % hash5ize);
}

в реЗ)m.ьтате процесса хеширования выдается начальный индекс в


массиве h as h tab ; если дан ная строка может быть где -то найдена, то
именно в цепи блоков, начало которой указано там. Поиск
осуществляется функцией loo k up . Если функция loo kup находит, что
данны й элемент уже присугствует, то она возвращает указатель на него;
если нет, то она возвращает nu l l .

struct nlist *Iookup(s) /* Iook for s in hashtab */


char *s;

struct nlist * np;


for (пр = hashtab[hasl"(s)]; пр != null;np=np- >nexl)
if (stтcmp(s, пр- > naте) == О)
relurn(np); j* fowxl [* !
}
rerurn(null); /* oot foLll1d */

функция install использует функцию lookup для определения, не


присугствует ли уже вводимое в да нный момент имя; если это так, то
новое определение должно вытеснить старое. В противном случае

'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

создается сове ршенно но в ый элемент. Есл и по какой- л ибо причине для


нового элемента больше нет места, то функция install возвращает
null .

struct nlist "'install(name, def) /'" put (nalП', def) "'/


char "'naIТl", "'def,
(
stпк:t nlist "'пр, "'lookupO;
сhзг "'strsaveO, '" allocO;
int hashvat

Щ(пр = Iookup(nan-..)) == null) \( /* поt fowxl */


пр = (stroct nlist *) alloc(sizeo~*np));
if (пр == null)
return(null);
и((пр->nan-.. = strsave(nan-..)) == null)
return(null);
hashva! = hash(np- >nan-..);
np->next = hashtab[hashva!];
hashtab[hashva!] = пр;
} еЕе /'" already шеге "'/
free((np->def);/'" free previous definition "'/
if ((np->def = strsave(deQ) == null)
return (null);
return(np);
}

функция strsave про сто копирует стро ку, указа нную в качестве
аргумента, в место хранения, пол ученное в результате обращения к
функции a l loc . Мы уже привел и эту функцию в лещ ии NoS. Так как
обращение к функции alloc и free MOгyr происxqцить в любом
порядке и в связи с проблемой выравнивания , простой вариант
функции alloc из лекц ии N2S нам бол ьше не подходи т; смот рите
лещи и N27 и лекц ии No8 .

Упражнение 6-7

Напиши те процеАУРУ, которая будет удалять и мя и определ ение из


табл ицы , управ л яемой функциями lookup и install .

'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Упражнение 6-8

Разработайте простую , основанную на функциях этого раздела , версию


процессора для обработки конструкций #define , ПРИГОДную для
использования с "С"- программами. Вам могуг также оказаться
полезны ми функции getchar и ungetch .

6.7. Поля

Когда вопрос эко номии памяти становится очень существенным, то


может оказаться необ)[)Димым помещзть в одно машинное слово
несколько различных объектов; одно из особенно распространенных
употреблений набор однобитовых признаков в применениях,
подобных символьным таблицам компилятора. BHell.lН.e обусловленные
ф:Jрматы данны.х, такие как интерф:>йсы аппаратных средств также
зачастую предполагают возможность получения слова по частям.

Представьте себе tПJагмент компилятора, который работает с


символьной таблицей. С каждым идентиф1катором программы связана
определенная инф:Jрмация, например, является он или нет ключевым
словом, яв ляется л и он и л и нет внеll.lН.ИМ и/и л и статическим и Т.д.
Самы й компактный способ закодировать такую инф:Jрмацию
поместить набор однобитовых признаков в отдельную переменную типа
char или in t .

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


''масок'', отвечающих соответствующим битовым позициям, как в

#dеfiлe keyword 01
#dеfiлe externa] 02
#dеfiлe staoc 04

(числа должны быть степенями двойки). Тогда обработка битов


сведется к ''жонглированию битами" с помощью операций сдв и га,
маскирования и дополнения , описанных нами в лекции No2.

Некоторые часто встречающиеся идиомы:

flags 1= externa] 1staoc;


>93
Б.8. К<'рниган, Д. М . Ричи Я ЗЫ К "JIOграм""ирования С

включает биты ex ter n al и s ta tic в f lags , в то время как

flags & = - ( exteтa /I static);

их выключает, а

if «flags & (eXlerna/ 1static)) == О) ...

истинно, если оба бита выключены.

Хотя этими идиомами легко овладеть, яз ык "С" в качестве альтернативы


предлагает возможность определения и обработки полей внутри сл ова
непосредственно, а не посредством побитовых логических операций.
Поле - это набор смежных битов внугри одной переменной типа i nt .
С интаксис определения и обработки полей основывается на структура х.
Например, символ ьную таблиц у конструкций #define , приведенную
выше, можно бы было заменить определением трех полей:

struct {
W1Signed is_keyword : 1;
W1Signed is_extern : 1;
W1Signed is_static : 1;
} flags;

Здесь определяется переменная с именем flags , которая содержит три


1-битовых поля . С ледуюl.lf'е за двоеточием число задает llМрину поля в
битах. Поля описаны как uns i gn ed, чтобы подчеркнуть, что они
действительно бy,цyr величинами без знака.

На отдельные поля можно ссылаться, как flags . is static ,


f l ags . i s ex ter n , flags . is keyword И Т.д., то есть точно так
же, как на друтие члены структуры. Поля ведут себя подобно неБОЛЫllИМ
целым без знака и могут участвовать в ари~етических выражениях
точно так же, как и другие целые. Таким образом, предыдущие примеры
более естественно переписать так:

дл я включения битов;

'"
Б.8. К<'рниган, Д. М. Ричи Язык "JIOграм""ирования С

flags.is_eX1ern = flags.is_static = О;

для выключения битов;

if (flags.is_eX!ern == О && flags.is_static == О) ...

для их проверки.

Поле не может перекрывать границу i nt ; если указанная lШ1рина


таЮJва, что это должно случиться, то поле выравнивается по границе

следующего i nt . Полям можно не присваивать имена; неименованные


поля (ТОЛЬЮJ двоеточие и lШ1рина) используются для заполнения
свободного места. Чтобы вынудить выравнивание на границу
следующего i nt , можно использовать специальную lШ1рин у О.

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


обратить внимание. По-видимому наиболее c)'l.lf'CТBeHHblM является то,
что отражая приро.цу разл ичны х аппаратных средств, распределение

полей на некоторых маlШ1нах осyrцeствляется слева направо, а на


некоторых справа налево. Это означает, что хотя поля очень полезны
для работы с внутренне определеными структурами данных, при
разделе нии BHell.lНe определяемых данных следует тщательно

рассматривать вопрос о том, какой ЮJнец nocтynaeT первым.

Другие ограничения, ЮJторые сл едует иметь в виду: поля не имеют


знака; они могут храниться ТОЛЬЮJ в переменных типа i nt (или, что
эквивалентно , типа unsig n ed ); они не являются массивами; они не
имеют адресов, так что к ним не применима операция &.

6.8 . Объединения

Объединения - это переменная, ЮJторая в разл ичные моменты времени


может содержать объекты разных типов и размеров, причем компилятор
берет на себя отслеживание размера и требований выравнивания.
Объединения представляют возможность работать с различными
видами данных в одной области памяти , не вводя в программу никакой
маlШ1нно-зависимой инф:Jрма ции.

"5
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

в качестве при мера , снова из символьной таблицы компи л ятора,


предпол ожим , что константы могуг быть типа int , float или быть
указателями на символы. значение каждой конкретной константы
должно храниться в переменной соответствующего типа , но все же для
ynравления таблицей самым удобным было бы , если это значение
занимало бы один и тот же объем памяти и хранилось в том же самом
месте независимо от его типа. это и является наз начением объединения
- выдел ить отдельную переменную, в которой можно законно хранить
л юбую одн у из переменных нескол ьких типов. Как и в случае пол ей,
синтаксис основывается на структурах.

uniоп u_tag {
in! iva~
Поаt fva ~
cha.r *pva~
} uva~

переменная uvа1 6у,цет иметь достаточно болыlюй размер , чтобы


хранить наиБОЛЫllИЙ из трех типов , независимо от маuшны , на которой
осуществляется компи л яция, программа не будет зависить от
характеристик аппаратных средств. Любой из э тих трех типов может
быть присвоен uva l и затем испол ьзован в выражениях, пока такое
использование совместимо: и з влекаемый тип должен совпадать с
посл едним помещенным типом. Дело программиста - сл едить за тем,
какой тип хранится в объединении в данный момент ; есл и что- л ибо
хранится как один тип , а изв л екается как друтой, то резул ьтаты будуг
зависеть от используемой маuшны.

С интаксически Aocтyn к членам объединения осуществляется


следующим образ ом:

и мя объединения. чле н

или

указател ь объедин е ни я - > член

то есть точно так же, как и в сл учае структур. есл и для отслеживания

",
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

типа, хранимого в данный момент в uval , используется переменная


ut ур е , то можно встретить такой участок программы:

if (utype == iл!)
pгintf{' %d\n", uvaLival);
еЕе if (utype == Воа!)
ргiл!~ '% fIn", uvalfvaQ;
еЕе if (utype == striлg)
pгintf{' %s\n", LNaLpvaI);
еЕе
pгintf{'bad type %d in urype\n", шуре) ;

Объединения могут появляться внугри структур И массивов и наоборот.


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

struc! {
сhзг *natп:';
int flags;
int utype;
tmion {
int iva~
Воа! fva~
сhзг *pya~
}uva~
} syrntab[l15yrn);

на переменную i val можно сослаться как

syrntab[i].uvalival

а на первый символ строки pv а 1 как

*syrttab[ i]. uval pval

в сущности объединение является структурой, в которой все члены


имеют нynевое смещение. Сама структура достаточно велика, чтобы
хранить "самый Il.IИрокиЙ" член, и выравнивание приroдно для всех

'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

ТИПОВ, ВХОДЯЩИХ В объединение. Как и В случае структур,


единственными операциями, которые в настоящее время можно

проводить с объединениями, являются доступ к члену и извлечение


адреса; объединения не MOfyr быть присвоены , переда ны функциям
и л и возвращены ими. указатели объединений можно использовать в
точно такой же манере, как и указатели структур.

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


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

6.9. Определение типа

в языке 'С" предусмотрена возможность, называемая typedef для


введения новых имен для типов данных. Например, описание

typedef inllength;

делает имя length синонимом для int . 'ТИ п" l ength может быть
использован в описаниях. переводов типов и Т.д. Точно таким же
образом, как и тип int :

length len, rrnxlen;


length *lengths[];

АналоП1 чно описание

typedef char *string;

делает s t r i ng синонимом для cha r *, то есть для указателя на


символы, что затем можно использовать в описаниях вида

string р, lineptr[lines], allocO;

Обратите внимание, что объявляемый в конструкции typedef тип


появляется в позиции имени переменной, а не сразу за словом
typedef . Синтаксически констр)'Ю.\ия typedef подобна описаниям
клас са памяти extern, static и т. д. Мы также использовал и

'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

прописные буквы, чтобы яснее выдел ить имена.

в качестве более сложного примера мы используем ЮJнструкцию


typedef для описания узлов дерева, рассмотренных ранее в этой
лекции:

typedef struct trюdе { /* the bask: node */


char *word; /* points to the text */
int count; /* пwтbег of оссurгепсеs */
struct tnode *left; /* left chikJ */
struct tnode *right; /* right chikJ */
} treeoode, *treeptr;

в результате получаем два новых ключевых слова: t r e e node (


структура ) и tr e eptr ( ука з атель на структуру ).
Тогда функцию ta l loc можно записать в виде

treeptr tallocO
{
char *аlЮсО;
return(treeptr) alloc(sizeo~tree node )));
}

Необ)[)Димо подчеркнуть , что описание typede f не приводит к


созданию нового в каЮJм-либо смысле типа ; оно ТОЛЬЮJ добавляет
новое имя для неЮJТОРОГО существующего типа. При этом не возникает
и никакой новой семантики: описанные таким способом переменные
обладают точно теми же свойствами, что и переменные, описанные
явным образом. По существу ЮJнструкция typed e f сходна с #de fi n e
за исключением того, что она интерпретируется ЮJмпилятором и

потому может осуществлять пqд,становки текста, ЮJторые выходят за

пределы возможностей макропроцессора языка 'С". Например,

typedef in! (*рБ) О;

создает тип pf i для "указателя функции, возвраll.flЮщеЙ з начение типа


i nt ", который затем можно было бы использовать в программе
сортировки из ле кции NoS в ЮJнтексте вида

pfi strcmp, пwп:::mp, swap;


'"
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Имеются две основные причины применения описаний typedef .


Первая причина связана с параметризацией программы , чтобы
обле гчить решение проблемы переносимости. Есл и для типов данных,
которые мосуг быть машинно-зависимыми, использовать описание
typedef , то при переносе программы на другую машин у придется
изменить только э ти описания. Одна из типичных ситуаций состоит в
использовании определяемых с помощью typedef имен для
различных целых величин и в посл едующем подходящем выборе типов
short , int и long для каждой имеющейся машины. Второе
наз начение typedef состоит в обеспечении л учшей документации для
программы - тип с именем treeptr может оказаться бол ее удобным
дл я восприятия , чем тип , который описан только как указатель сл ожной
структуры. И наконец , всегда c)'l.Цf'CТByeT вероятность, что в бу,цущем
компилятор и л и некоторая другая программа, такая как lint , сможет
использовать содержащуюся в описаниях t ypede f инqюрмацию для
проведения некоторой допол нительной проверки программы.

200
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Ввод и вывод

Опис ы ваются механизмы ввода и вывод данных. работы со


стандартной библиотекой и доступ к файлам.

7. Ввод и вывод

Средства ввода/вывода не являются составной ч астью языка "с", так что


мы не выделяли и х в нашем предьщуrЦf'М изложении. Однако реальные
программы вза им одействуют со своей окружаюl.Цf'Й средой гораздо
более сложным образом, чем мы видели до сих пор. В этой лекц ии
будет описана" стандартная библиотека ввода/вывода ", то есть набор
функций, разработанных для обеспечения стандартной системы ввода/
вывода для "с"- программ. Эти функции предназначены для у,цобства
программного интерф:>йса , и все же отражают тол ько те операции,
которые MOryr быть обеспечены на Болыl.в1ствеe современных
операционных систем. процеАУРЫ достато чн о эwктивны для того,
чтобы пользователи редко ч увствовали необходимость обойти их ''ради
э wктивности", как бы ни была важна конкретная задача. И, наконец,
эти процедуры задуманы быть ''перенос и мыми'' в том смысле, ч то они
должны существовать в совместимом виде на любой системе, где
имеется язык "с", и что программы, которые ограничивают свои

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


стандартной библиотекой , можно бу,цет переносить с одной с и стемы на
друтую по CyrцeCТBY без изменений.

Мы здесь не будем пытаться описать всю б и бл и отеку ввода/вывода; мы


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

7.1. Обращение к стандартной библиотеке

Каждый исходный файл, который обращается к функции из стандартной


библиотеки , должен вблизи начала содержать строку

# include <s(dlo.h>

20>
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

в файле s tdio . h определяются некоторые макросы и переменные,


используемые библиотекой ввода/вывода. Использование угловых
скобок вместо обычных двойных кавычек - указание компилятору
искать этот файл в справочнике, содержаl.l.fМ заголовки стандартной
ишlюрмации (на системе UNIX обычно /us r / inc1 ude ).

Кроме того, при загрузке программы может оказаться необ)[)Димым


указать библиотеку явно ; на системе PDP-ll UNIX, например , команда
компиляции программы имела бы вид:

сс и сходные файл ы и Т.д. -Is

где -1 s указывает на загрузку из стандартной библ иотеки.

7.2. Стандартный ввод и вывод - функции GETCHAR и


PUTCHAR

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


символу за раз из "стандартного ввода ", обычно с терминала
пользователя, с помощью функции getchar . функция getchar () Прl
каждом К ней обращении возвраl.Lflет следующий вводимый символ. В
БОЛЫ.LIинстве сред, которые подцерживают язык "с", терминал может
быть заменен некоторым файлом с помощью обозначения <: есл и
некоторая программа prog испол ьзует функцию getchar то командна:
строка

р го g< inШе

приведет к тому, что prog бу,цет читать из файла infi1e , а не с


терминала. Переключение ввода делается таким образом, что сама
программа prog не замечает изменения; в частности строка" <in f i l e
"не включается в командную строку аргументов в argv . Переключение
ввода оказ ывается незаметным и в том сл учае, когда вывод nocтynaeT из

другой программы посредством поточного ( pipe ) механизма;


коман дная строка

otherprog I prog

202
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

прогоняет две программы , otherprog и p r og, и организует так, что


стандартным вводом для prog служит стандартный вывод
otherprog.

функция g e tchar возвращзет значение EOF, когда она попадает на


конец фай л а, какой бы ввод она при этом не считывала. Стандартная
библиотека полагает символическую константу EOF равной -1
(посредством #define в файле stdio . h ), но проверки следует
писать в терминах EOF, а не -1 , чтобы избежать зависимости от
конкретного значения.

Вывод можно осуществлять с помощью функции putchar (с) ,


помещзющей символ 'с' в "стандартный вывод ", который по умолчанию
яв л яется терминалом. Вывод можно направить в некоторый файл с
помощью обозначения > если prog испол ьзует putchar, то
командная строка

pгo g>o utille

приведет к записи стандартного вывода в файл outfile , а не на


терминал. На системе UNIX можно также использовать поточный
механизм. Строка

pгo g I а rюtheгрго g

помещзет стандартный вывод prog в стандартный ввод


anotherprog . И опять prog не бу,цет осведомл ена об изменении
направления.

Вывод, осуществляемый функцией printf, также поступает в


стандартный вывод, и обраll.fНИЯ к putc h ar и printf MO~
перемежаться.

Поразител ьное количество программ читает только из одного ЮDДНОГО


потока И пиJJ..Jeт только В один выходной поток; для таких программ ввод
и вывод с помощью функций getchar , putchar и printf может
оказаться вполне адекватным и для начала определенно достаточным.

Это особенно справедливо тогда, когда имеется возможность указания


файлов для ввода и вывода и поточный механизм для связи вывода

203
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

одной программы с вводом другой. Рассмотрим, например, программу


lower , которая преобразует прописные буквы из своего ввода в
строчные:

#include <stdio.h>

mainО /* сопуеп input to lower case */


{
int с;

while ((е = gelehar()) != EOF)


pUlehar(isupper(e) ? lolower(e) : е );

"функции" isupper и tolow er на самом деле являются макросами,


определ еными в s td io . h . Макрос isupper проверяет, является л и
его аргумент буквой из верхнего реrnстра, и возвращзет ненулевое
значение, если это так, и нуль в противном случае. Макрос to l ower
преобразует букву из верхнего реrnстра в ту же букву нижнего реrnстра.
Независимо от того , как э ти функции реализованы на конкретной
маl.LIИне, их внешнее поведение совершенно одинаково, так что

использующие их программы избавлены от з нания символьного набора.

Если требуется преобразовать несколько фзйлов, то можно собрать эти


фзйлы с помощью программы, подобной угилите cat системы UNIX,

cat file l file2 ... I lower>output

и избежать тем самым вопроса о том , как обратиться к этим фзйлам из


программы. (Программа са t привqд,ится позже в этой л екции).

Кроме того отметим , что в стандартной библиотеке ввода/вывода


функции" getchar и putchar на самом деле MOryr быть макросами.
Это по з воляет избежать накладных расходов на обращение к функции
дл я обработки каждого символа. В л екции No8 мы продемонстрируем,
как это делается.

7.3 . Форматный вывод - функция printf


20'
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Две функции: pr i ntf ДЛЯ вывода и scanf для ввода (следующий


раздел) позволяют преобразовывать численные величины в
символьное представление и обратно. Они также позволяют
генерировать и интерпретировать qюрматные строки. Мы уже всюду в
предьщущих лекциях неqюрмально использовали функцию pr i ntf ;
здесь при водится более полное и точное описание. функция

pгintf(co ntro \ aгgl , a гg2, ... )

преобразует, определ яет qюрмат и печатает свои аргументы в


стандартный вывод под управлением строки control . )t)равляющая
строка содержит два типа объектов: обычные символы, I«Jторые просто
копируются в выходной поток, и спецификации преобразований,
каждая из которых вызывает преобразование и печать очередного
аргумента pr i ntf .

Каждая С пецификация преобраз ования начинается с символа % и


заканчивается символ ом преобразования. Между % и символом
преобразования могуг находиться:

• знак минус , который указывает о выравнивании


преобразованного аргумента по л евому краю его поля.
• Строка циtW, задающая минимальную Ll.D1рину поля.
Преобразованное число будет напечатано в поле по крайней мере
этой Ll.D1рины , а если необходимо, то и в более Ll.D1pOКOM. Есл и
преобразованныи аргумент имеет меньше символов , чем
указанная Il.IИрина поля, то он будет дополнен сл ева (или справа,
если было указано выравнивание по левому краю )заполняющими
символами до этой Il.IИрины . Заполняющим символом обычно
является пробел , а если lШ1рина поля указывается с лидирующим
нyn.ем , то этим символом будет нynь (л идирующий нynь В данном
сл учае не означает восьмеричной Il.IИрины пол я).
• Точка , которая отделяет Il.IИрин у поля от сл едующей строки циtW.
• Строка циtW (точность), которая указывает максимал ьное число
символов строки , которые должны быть напечатаны , или число
печатаемых справа от десятичной точки ци<W для переменных
типа floa t double .
или
• Модификатор длины 1 , который указ ывает, что соответствующий
эл емент данных имеет тип lo n g , а не i nt .
205
Б.8. К<'рниган, Д. М . Ричи Я ЗЫ К "JIOграм""ирования С

Ниже приводятся символы преобразования и их смысл:

• d - аргумент преобразуется к десятичному виду.


• о - аргумент преобразуется в беззнаЮJВУЮ восьмеричную фJрму
(без л идируюllJ'ГО нуля).
• х - аргумент преобразуется в беззнаЮJВУЮ JJ..Jeстнадцатеричную
форму (без лидирующих О х ).
• u - аргумент преобразуется в беззнаЮJВУЮ десятичную фJрму.
• С - аргумент рассматривается как отдельный символ .
• s - аргумент является СТРОЮJй: символы строки печатаются до тех
пор , пока не бу,цет достигнуг нулевой символ и л и не будет
напечатано ЮJличество символ ов , указанное в специф1кации
точности.

• е- аргумент , рассматриваемый как переменная типа f 10at или


doubl e , преобразуется в десятичную форму в виде [-
] т . n n n n n ne [ + - ] ХХ , где длина строки из n определяется
указанной точностью. Точность по умолчанию равна 6.
• f - аргумент , рассматриваемый как переменная типа f 10at или
doubl e , преобразуется в десятичную форму в виде [-
] mmm . n n nn n , где длина строки из n определяется указанной
точностью. Точность по умолчанию равна 6. Отметим , что эта
точность не определяет ЮJличество печатаемых в qюрмате f
значащих ци<W.
• 9 Используется или фJрмат %е или %f , каЮJй ЮJроче;
незначащие н ул и не печатаются. Если идущий за % символ не
является символом преобразования , то печатается сам этот
символ; следовательно, символ %можно напечатать, указав %%.

БОЛЫllИНСТВО из форматных преобразований очевидно и было


проиллюстрировано в предыдущих л екциях. Единственным
исключением является то , как точность взаимодействует со строками.
С ледующая таблица демонстрирует влияние задания различных
спецИttикаций на печать" h e 1 10 , wor1 d " (12 символов). Мы
поместили двоеточия вокруг каждого поля для того, чтобы вы мо гл и
видеть его протяженность.

:o/010s: :hello, world:


:o/010-s: :hello, world:
206
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

:o/020s: hello, world:


:% -20s: :hello, world
:o/020.10s: hello, wor:
:% -20. 10s: :hello, wor
:%. 10s: :hello, wor:

Предостережение: pr i nt f и спользует свой первый аргумент для


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

бессмысленные результаты.

Упражнение 7-1

НаПИlШ1те программу, которая будет печатать разумным образом


произвольный ввод. Как мин и мум она должна печатать неграф1 ческие
символы в восьмери чн ом и ли шестнадцатеричном виде (в
соответстви и с принятыми у вас обычаям и ) и складывать дл инн ые
строки.

7.4. Форматный ввод - функция SCANF

Осуществляющая ввод функция scan f является аналогом printf и


позволяет проводить в обратном направлении мноrnе из тех же самых
преобразованиЙ. функция

sсапf{со пtrо L argl , arg2, ... )

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

соответстви и с ф:Jрматом , указанном в аргументе cont r ol , и помещает


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

образом преобраз ованный ввод.

Управляющая строка обычно содержит специф1кации преобразования,


которые испол ьзуются для непосредственной интерпретации входных
последовательностей. Управ л яюЩ1Я строка может содержать:

20 7
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

• пробелы, табуляции или символы новой строки ("с и мволы пустых


промежугков ') , которые и гнорируются.
• Обычные символы (не % ), которые предполагаются
совпадающими со следующими отл ичны ми от символов пустых

промежугков с имв олами входного потока.

• Спец ифи кац ии преобразования, состоящие из символа %,


необязательного с и мвола подавления присваивания
*
необязательного числа, задаюl.Lf'ГО максимальную Il.IИрину пол я и
символа преобразования.

С пециф1кация преобразования ynравляет преобразованием следующего


поля ввода. Нормально результат помещается в переменную , которая
указывается соответствующим аргументом. Если, однако , с помощью
символа * указано подав ление присваивания, то это поле ввода просто

пропускается и никакого присваивания не производится. Поле ввода


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

простых промежугков; оно продолжается л ибо до следующего символа


пустого промежугка, либо пока не бу,цет исчерпана Il.IИрина поля, если
она указана. Отсюда следует, что при поиске нужного ей ввода, функция
scanf будет пересекать границы строк, поскол ьку символ новой строки
входит в число пустых промежугков.

С имвол преобразования определяет интерпретацию поля ввода ;


согласно требованиям основанной на вы зо ве по значению семантики
языка "с" соответствующий аргумент должен быть указателем.
Допускаются следующие символы преобразования:

• d - на вводе ожидается десятичное целое; соответствующий


аргумент должен быть указателем на целое.
• о - На вводе ожи дается восьмеричное целое (с лидирующим
нулем или без него); соответствующий аргумент должен быть
указателем на целое.

• х На вводе ожидается шестнадцатеричное целое (с


л идирующими Ох или без них); соответствующий аргумент
должен быть указателем на цел ое.
• h - На вводе ожидается целое типа short ; соответствующий
аргумент должен быть указателем на целое типа short .
• с - Ожидается отдельный символ; соответствующий аргумент

208
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

должен быть указателем на символы; следующий вводимый


символ помещается в указанное место. Обычный пропуск сим­
волов пустых промежyrков в этом случае подавляется; для чтения

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


промежyrка, пользуйтесь специqмкацией преобразования %1 s .
• s - Ожидается символьная строка ; соответствующий аргумент
должен быть указателем символов, который указывает на массив
символов, который достаточно велик для принятия строки и
добавляемого в конце символа \ О.
• f - Ожидается число с плавающей точкой; соответствующий
аргумент должен быть указателем на переменную типа f lоа t .
• Е - символ преобразования е является синонимом для f . Формат
ввода переменной типа f 1 оа t включает необязательный знак,
строку ци<W, возможно содержащую десятичную точку и
необязательное поле экспоненты, состоящее из буквы е, за
которой слеАУет целое, возможно имеющее знак.

Перед символами преобразования d, о и х может стоять 1, которая


означает , что в списке аргументов должен на)[)Диться указатель на

переменную типа long, а не типа int . Аналогично, буква 1 может


стоять перед символами преобразования е или f , говоря о том, что в
списке аргументов должен на)[)Диться указатель на переменную типа

double , а не типа f1oat .

Например, обращение

int i; flo at х; char пате[50); scanf("%d %f%s", &i, &х. пате);

со строкой на вводе

25 54.32е-1 tlюmpsоп

приводит к присваиванию i з начения 25 , х - значения 5 . 432 и пате


- строки" t hompson ", надлежащим образом законченной символом
\ О. эти три поля ввода можно разделить столькими пробелами,
табynяциями и символами новых строк, сколько вы пожелаете.
Обращение

int i;
209
Б.8. К<'рниган, Д. М. Ричи Язык "JIOграм""ирования С

float х;
char nalП' ( 50 ] ;
scanf{'"%2 d % f %"'d %2s ", &i, &х. natп:');

с вводом

56789012345_72

присвоит i з начение 56, х - 789 . О , пропустит 0 1 23 и поместит в


пате строку .. 4 5 ". При следующем обращении к любой процедуре
ввода рассмотрение начнется с буквы а . В этих двух примерах пате
является указателем и, следовательно, перед ним не нужно помещать

знак &.

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


калькулятор из лекuии N!!4, используя для преобразования ввода
функцию scanf :

# include <stdio.h>
mainО j'" rudmntary desk cak::u1ator "'!
{
double sшn. у;
sum =O;
while (sc_nf('%lf', &v) !=EOF)
print~'\t%.21\n". sum += v);

выполнение функции scanf заканчивается л ибо тогда, когда она


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

присвоенных элементов ввода. Это число может быть использовано


для определения количества найденных элементов ввода. При выходе
на конец файла возвращается EOF ; подчеркнем, что это значение
отлично от О, что следующий вводимый символ не у,цовлетворяет
первой спецификации в управляющей строке. При следующем
обращении к scanf поиск возобнов ляется непосредственно за
посл едним введенным символом.

2>0
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Заключительное предостережение: аргументы функции scanf должны


быть указателями. Несомненно наиболее распространенная ошибка
состоит в написании

scanf{'"%d", п);

вместо

scanf{'"%d", &п) ;

7.5. Форматное прео6разование в памяти

От функции scanf и printf происходят функции sscanf и


spr in tf , которые осуществ л яют аналогичные преобразования, но
оперируют со строкой , а не с ф3Йлом. Обращения к э тим функциям
имеют вид :

sprintf(string, controL argl , arg2, ... )


sscanf(string, controL argl , arg2, ... )

Как и раньше , функция sprintf преобразует свои аргументы argl ,


arg2 и Т.д . В соответствии с qюрматом , указанным в control , но
помещает резул ьтаты в string, а не в стандартный вывод. Конечно,
строка string должна быть достаточно велика, чтобы принять
результат. Например , если пате - э то символьный массив , а n - целое,
то

sргintf(na!П', "temp%d", п) ;

создает в пате строку вида tempnn n, где ппп - з начение п .

функция sscanf выполняет обратные преобразования она


просматривает строку string в соответствии с qюрматом в аргументе
contro l и помещает результирующие значения в аргументы argl ,
arg2 и Т.д . Эти аргументы должны быть указателями. В результате
обращения

ssсапf(na!П', "temp%d", &n);


Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

переменная n получает значение строки циcW, следующих за t e mp в


пате .

Упражнение 7-2

Перепишите настол ьный калькулятор из ле кции No4, используя для


ввода и преобразования чисел scanf и /или sscanf .

7.6. Доступ к файлам

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


писали в стандартный вывод, относительно которых мы предпол агали,
что они маЛ1ческим образом предоставлены программе местной
операционной системой.

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


программы, работающей с фай л ом , который не связан заранее с
программоЙ. Одной из программ , которая явно демонстрирует
потребность в таких операциях, является са t , которая объединяет
набор из нескольких именованных файлов в стандартный вывод.
Программа са t используется для вывода файлов на терминал и в
качестве универсального сборщика ввода для программ, которые не
имеют возможности обращаться к фай л ам по имени. Например , команда

cat Х.С,у.с

печатает содержимое файлов х .с и у . с в стан дартный вывод.

Вопрос состоит в том , как организовать чтение из именованных


файлов , Т.е., как связать внешние имена, которыми мысл ит
пользовател ь, с фактически читающими данные операторами.

Эти правила просты . Прежде чем можно считывать из некоторого


фай л а или записывать в него , э тот фай л должен быть открыт с
помощью функции fope n из стандартной библиотеки. функция fop e n
берет внешнее имя (подобное х . с или у . с ), проводит некоторые
обслуживающие действия и переговоры с операционной системой
(детал и которых не должны нас касаться) и возвращает BHyrpeHHee имя,

ш
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

которое должно использо ваться при посл едующих чтениях из фзйла и л и


записях в него.

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


является указателем структуры, которая содержит инqюрмацию о фзйле,
такую как место размещения 6y<fx:'pa, текущая позиция символа в бу<fx:'ре,
происходит ли чтение из фзйла и л и запись в него и тому подобное.
Пользовател и не обязаны з нать эти детали , потому что среди
определений для стандартного ввода-вывода, получаемых из фзйла
stdio . h, содержится определение структуры с именем file .
gцинственное необходимое для указателя фзйла описание
демо нстрируется примером:

fiIe *fopen(), *fp;

Здесь говорится, что fp является указателем на file и fopen


во зв ращзет указатель на f i l e . Обратите внимание, что f i 1 е является
именем типа, подобным int, а не ярлыку структуры; это реализовано
какt ypede f . (Подробности того, как все это работает на системе
UNIX, приведены в ле кции No8).

Фактическое обращение к функции fopen в программе имеет вид:

fp =fopen( name,mode);

Первым аргументом функции fopen является ''имя'' файла , которое


задается в виде символьной строки. Второй аргумент mode ( " режим " )
также является символьной строкой, которая указывает, как этот фзйл
будет использоваться. Допустимыми режимами яв ляются: чтение (" r ''),
запись (" w '') и добав ле ние (" а '').

Если вы откроете файл , КОТОРЫЙ еще не существует, для записи или


добавления, то такой фзйл будет создан (есл и это возможно).

Открытие существующего фзйла на запись приводит к отбрасыванию


его старого содержимого. Попытка чтения несуществующего фзйла
является ОlШlбкоЙ. ОlШlбки MOryr быть обусловлены и друтими
причинами (например , попыткой чтения из файла, не имея на то
разрешения). При наличии какой-либо ОlШlбки функция возвращзет

ш
Б.8. К<'рниган, Д. М . Ричи Я ЗЫ К "JIOграм""ирования С

нулевое значение указателя nul l (которое для mобства также


определ яется в файле stdio . h ).

Другой необходимой вещью является способ чтения или записи, если


файл уже открыт. Здесь имеется несколько возможностей , из которых
g e tc и putc являются простеЙшими. функция g e tc возвращает
слеАУЮЩИЙ символ из фай л а; ей необ)[)Дим указатель файла, чтобы
знать, из какого файла читать. Таким образом ,

e=ge,e(fp )

помещает в 'С" слеАУЮЩИЙ символ из файла , указанного посредством


fp , и EOF, если достигнут конец файла.

функция putc, являющаяся обращением к функции getc ,

ро'е(е, ф)

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


g e tchar и putchar, getc и putc мосуг быть макросами, а не
функциями.

При запуске программы автоматически открываются три файла,


которые снабжены определеными указателями файлов. Этими файлами
яв л яются стандартный ВВОД, стандартный вывод и стандартный вывод
ошибок; соответствующие указатели файлов на зываются stdin,
stdout и stder r . Обычно все эти указатели связаны с терминалом,
но stdin и stdout могут быть перенаправ л ены на файлы и л и в
поток ( pipe ), как описывал ось в раздел е 7.2. л екции No7.

Функции getc h ar и putc h ar MOryr быть определ ены в терминалах


g e tc , putc , std i n и stdout слеАУЮЩИМ образом: #de fi n e
g e tchar ( ) g e tc ( stdin) #def i ne putchar(c) putc{c ,
stdout) . При работе с файлами для ф:Jрматного ввода и вывода
можно испол ьзовать функции fscanf и fprintf . Они идентичны
функциям sca n f и printf, за исключением того, что первым
аргументом является указател ь файла , определяющий тот фай л, который
будет читаться или куда будет вестись запись; управляющая строка будет
вторым аргументом.

'"
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Покончив С предварительными замечаниями, мы теперь в состоянии


написать программу са t для конкатенации файлов. Используемая здесь
основная схема оказывается удобной во мноrnх программах: есл и
имеются аргументы в командной строке , то они обрабатываются
посл едовательно. Если такие аргументы отсутствуют, то обрабатывается
стандартный ввод. Это позволяет использовать программу как
самостоятельно, так и как часть болыuей задачи.

#include <stdlo.h>
maln(argc, argv) /"'cat: corx::atenate files"'/
int argc;
char *argv[J;
(
fiIe *fp, *fopenO;
if(argc== 1) /"'00 args; со ру standard inрш"' l
fileco py(stdin);
else
while (-- argc > О )
if((fp =fopen(*++argv, Y ))==null) (
printf{"cat:can't о р е п %\n", "'argv);
break;
} else (
file co py(fp );
fclose(fp);
}

filecopy(fp) j*copy fiIe fp to standard output*j


fiIe *fp;
(
int с;
while ((c=getc(fp)) ! = ЕОР )
putc(c,stdout);
}

указатели фай л ов stdin и stdout заранее определены в библиотеке


ввода-вывода как стандартный ввод и стандартный вывод; они могут
быть использованы в любом месте, где можно использовать объект
типа f i 1 е * . Они однако являются константами, а не переменными , так
что не пытайтесь им что-либо присваивать.

ш
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

функция fc l os e является обратной по отношению к fopen ; она


разрывает связь между указателем файла и внешним именем,
установленную фун!Щией fopen , и высвобождает указатель файла для
друтого файла. Большинство операционных систем имеют некоторые
ограничения на число одновременно открытых файлов, которыми
может распоряжаться программа. Поэтому. то как мы поступили в са t ,
освободив не нужные нам более объекты, является хорошей идее й.
Имеется и другая причина для применения функции fc l os e к
выходному файлу - она вы з ывает выдачу информации из буф:>ра, в
котором putc собирает вывод. (При нормал ьном завершении работы
программы функция fclose вызывается автоматически для каждого
открытого файла).

7.7. Обработка ошибок - SТDERR и ЕХП

Обработка 011D1бок в са t неидеальна. Неу,цобство заключается в том,


что если один из файлов по неЮJТОРОЙ при чине оказывается
недоступным, диагностичеСЮJе сообщение об этом печатается в конце
объединенного вывода. Это приемлемо , если вывод поступает на
терминал, но не годится, если вывод поступает в некоторый файл и л и
через ПОТОЧНЫЙ ( pipe l ine ) механизм в дрyryю программу.

Чтобы луч ше обрабатывать такую ситуацию, к программе точно таким


же образом, какstdi n и stdout, присоединяется второй выходной
фай л, называемый stde r r . Если это вообще возможно, вывод,
записанный в файле stderr , появляется на терминале пользователя,
даже если стандартный вывод направляется в д ругое место.

Давайте переделаем программу са t таЮ1М образом, чтобы сообщения


об ошибках писались в стандартный фай л ошибок.

"irк:lude <stdio.h>
main(argc,argv) /*cat: concatenate files* /
int argc;
char *argv(J ;
{
fiIe *ф, *fopenO;

2>'
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

if(argc== l) 1"' 00 args; со ру standard input"'l


filecopy(stdin);
else
while (--argc > О )
if((fp = fo р е n( * ++ а rgv,'Т#))== пull) (
fprintf(stderr,
"cat: can't о ре n, %s\n", argv);
exit(l);
} else (
fileсо ру( ф );
}
exit(O);
}

Программа сообщзет об ошибках двумя способами. диагностическое


сообщение, выдаваемое функцией printf, поступает в stderr и,
таким образом, оказывается на терминале пользователя , а не исчезает в
потоке ( pipeline ) или в вы)[)Дном файле.

Программа также использует функцию exi t из стандартной


библиотеки , обращение к которой вызывает завершение выпол нения
программы. Аргумент функции exi t доступен любой программе ,
обращзющейся к данной функции , так что успеllпюе или неу,цачное
завершение данной программы может быть проверено другой
программой, использующей эту в качестве подзадачи. По согл ашению
величина О в качестве возвращз:емого з начения свидетельствует о том,
что все в порядке, а различные ненулевые з начения являются

признаками ненормал ьных ситуаций.

функция exit вызывает функцию fclose для каждого открытого


выходного фай л а , с тем чтобы вывести всю помещенную в бу~ры
выходную инqюрмацию, а затем вызывает функцию exi t . функция
exi t приводит К немедленному завершению без очистки каких- л ибо
буферов; конечно, при жел ании к этой функции можно обратиться
непосредственно.

7.8 . Ввод и вывод строк

ш
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

Стандартная библиотека содержит функцию fgets , совершенно


аналогичную функции getli n e , которую мы использовали на всем
протяжении книги. В результате обраl.Lf'НИЯ

fgеts(liлe, maxline, [р)

следующзя строка ввода (включая символ новой строки) считывается


из файла fp в символьный массив li n e ; самое Болыlюe ma x l i ne - l
символ будет прочитан. Резул ьтирующзя строка заканчивается
символом \0 . Нормально функция fgets возвращзет l ine ; в конце
файла она возвращзет nu l l . (Наша функция get l ine возвращзет
дл ину строки, а при выходе на конец файла - нуль).

Предназначенная для вывода функция fpu t s записывает строку (котора


не обязана содержать символ новой строки) в файл:

fputs(liлe, [р)

Чтобы показать, что в функциях типа fgets и fputs нет ничего


таинственного, мы при водим их ниже, скопированными

непосредственно из стандартной библиотеки ввода-вывода:

#include < 5 tdЮ.h>


char "'fgets(5,n,iop) /*ger at nЮ 5t n chars fro m iop"'/
char "'5;
int п;
registeг те "'ю р ;
(
registeг int с;
registeг смг *С5;
С5 = 5;
while(--п> О&&(с= gеlс(ioр)) !=EOF)
if (*cs++ = с) == '\n')
bгeak ;
"'С5 = '\0';
гelurn( c == EOF && cs==s) ? null: s);
}
fputs(5,iop) /*put 5tring 5 оп fiJS iop* /
registeг char "'5;

2>'
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

register те *io р ;
{
register in! с;
while (с = *s++)
pUlC(c,iop);
}

Упражнение 7-3

Напишите программу сравнения двух файлов, которая бу,цет печатать


первую строку и позицию символа, где они различаются.

Упражнение 7-4

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


ле кци и NQ5 таЮ1М образом, чтобы в качестве ввода использовался набор
именованных фай л ов или , если никаЮ1е файлы не указаны как
аргументы , стандартный ввод. Следует л и печатать имя файла при
нахождении подходящей строЮ1?

Упражнение 7-5

Напишите программу печати набора файлов, которая начинает каждый


новый файл с новой страницы и печатает для каждого файла заголовок
и счетчик текущих страниц.

7.9 . Несколько разнообразных функций

С тандартная библиотека предостав л яет множество раз нообразных


функций, некоторые из которых оказываются особенно полезными. Мы
уже упоминали функции для работы со строками: str1en, strcpy,
5 trca t и 5 trcmp. Вот некоторые другие.

7.9.1. Проверка вида символ ов и преобразования

Некоторые макросы выполняют проверку символов и преобразования:

isalpha(c) не О, есл и "с" алфав итный симв ол,


Б.8. К<'рниган, Д. М. Ричи Язык "JIOграм""ирования С

О - если нет.
isupper(c) Н е О, есл и "с" буква верхнего регистра ,
О - если нет.
islower(c) Не О , если "с" буква нижнего регистр а,
О - если нет.
isdjgjt(c) Не О , если "с" ци<Wа ,
о - если нет.
isspace(c) Не О, если "с" пробел, табуляция
или новая строка, О - есл и нет.
toupper(c) Пр еоб разует "с" в букву верхнего реги стра .
tolower(c) Преобр азует "с" в букву ни жнего реги ст ра.

7.9.2. Функция UNGETC

Стандартная библиотека содержит довольно ограниченную версию


функции u n getc h, написанной нами в ле кции N!!4; она называется
u n getc . В результате обращения

tmgetc(c,fp)

символ 'С" возвраl.Lflется в фзйл


fp . Позволяется возвраl.LflТЬ в каждый
файл только один символ. функция ungetc может быть использована в
любой из функций ввода и с макросами типа scanf , getc и л и
g e tc h ar .

7.9.3. Обращение к системе

функция s у s t em ( s) выполняет команду, содержащуюся в символьной


строке s , и затем возобновляет выполнение текyI..Ц:'Й программы.
Соде ржимое s сильно зависит от используемо й операционной
системы. В качестве тривиального примера , укажем, что на системе
UNIX строка

sys teIТ("d ate' ,;

приводит К выполнению программы da te , которая печатает дату и


время дня.

220
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

7.9.4. Управление памятью

функция calloc весьма сходна с функцией alloc, использованной


нами в предыдущих лекциях. В результате обращения

calloc(n, sizeof(object))

возвращзется либо указатель пространства, достаточного для


размещения n объектов указанного размера, либо null , если запрос не
может быть у,цо влетворен. Отводимая память инициализируется
н улев ыми значениями.

указатель обладает нужным ДЛЯ рассматриваемых объектов


выравниванием, но ему следует приписывать соответствующий тип,
как в

char *callocQ;
int *ip;
ip = (iлt*) саlloс(n,sizeоf(iлt));

функция cfree ( р ) освобождает пространство , на которое указывает


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

Реализация программы распределения памяти, подобной cal l oc , в


которой размещенные блоки мосут освобождаться в произвольном
порядке , продемонстрирована в лекции No8.
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Интерфейс системы UNIX


Опис ы вается взаимодейств и е программы на языке С с операционной
системой UN IX.

8. Интерфейс системы UNIX

Материал этой л екции относится к интерфейсу между с- программами и


операционной системой UN IX. Так как БОЛbl.LМН СТВО пользовател ей
языка "С" работают на системе UNIX, эта лекция окажется пол езной для
БОЛЫШlн ства читателей. Даже если вы используете с- комп и лятор на
друтой маlШ1не , изучение приводимых здесь примеров должно помочь
вам глубже проникнyrь в методы программирования на языке 'С".

Эта л екция делится на три основные части: ввод/вывод, система фай л ов


и распределение памяти. Первые две части предполагают неБолыlюe
знакомство с внеLLIНИМИ характеристикам и системы UN IX.

в л екции No7 мы имели дело с системным ин терфейсом, который


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

связанных с вводом и выводом точек входа операционной системы


UN IX и проиллюстрируем, как с их помощью мосуг быть реализованы
различные части стандартной библиотеки.

8.1. Дескрипторы файлов

в операционной системе UN IX весь ввод и вывод осуществляется


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

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

222
Б.8. К <'рниган, Д. М. Р ич и Я ЗЫ К "JIOграм""ирования С

необходимо сообщить системе о вашем намерении; этот процесс


наз ывается "открытием" файла. Система выясняет, имеете ли вы право
поступать таким образом (существует ли этот файл? имеется л и у вас
разрешение на обращение к нему?) , и если все в порядке, возвращает в
программ у неБОЛЫlюе пол ожител ьное целое число, называемое
дескр иптором фай ла. Всякий раз, когда этот файл используется для
ввода или вывода, для иде нтификации файла УТlOтребляется дескр иптор
фай ла, а не его имя . (Здес ь существует примерная аналогия с
использованием read (5 , ... ) и wri te (6 , ... ) в фJртране).
Вся инфJрмация об открытом файле содержится в системе; программа
пользователя обращается к файлу только через дескриптор файла.

Для у,цобства выполнения обычных операций ввода и вывода с

помощью терминала пользователя существуют специальные

соглашения. Ко гда интерпретатор КDMaHД (" shell ") прогоняет


программу, он открывает три фай ла, называемые стандартным вводом ,
стандартным выводом и стандартным выводом ошибок, КDTopыe имеют
соответственно числа О , 1 и 2 в качестве дескрипторов этих файлов. в
нормальном состоянии все они связаны с термин алом, так что если

программа читает с дескриптором файла О и пишет с дескр ипторам и


фай ло в 1 и 2, то она м ожет осуществлять ввод и вывод с помощью
терминала, не заботясь об открытии соответствующих файлов.

Пользовател ь программ ы может перенаправлять ввод и вывод на


фай лы , используя операции КDMaHДHoro интерпретатора shell " < "и "
> ":

pгog <infile>outfile

в этом случае интерпретатор КDMaHД shell изменит присваивание по


умолчанию дескрипторов файлов О и 1 с терминала на указанные
фай лы. Нормально дескр иптор файла 2 остается связанным с
терминалом, так что сообщения об ошибках MOryr поступать ту,ца.
Подоб ные замечания справедливы и тогда, КDгдa ввод и вывод связан с
каналом. Сл едует отметить, что во всех случаях прикрепления файлов
изменяются интерпретатором shel l, а не программой. Сама
программа, пока она использует фай л о для ввода и файлы 1 и 2 для
выв ода, не знает ни отку,ца приходит ее ввод, ни куда поступает ее

выдача.

22з
Б.8. К<'рниган, Д. М . Ричи Я ЗЫ К "JIOграм""ирования С

8.2. Низкоуровневый ввод/вывод операторы READ и


WRПЕ

С амый низкий .УРовень ввода/вывода в системе UNIX не


преАУсматривает ни какой-либо буф:>ризации, ни какого-либо другого
сервиса; он по существу является непосредственным ВХОДОМ в

операционную систему. Весь ввод и вывод осуществляется двумя


функциями: read и wri te . Первым аргументом обеих функций
яв л яется дескриптор файла. Вторым аргументом яв л яется БУФ"р в вашей
программе, отку,ца или куда должны поступать данные. Третий аргумент
- это число подлежащих пересылке байтов. Обращения к этим функциям
имеют вид:

n_read = гea d ( fd ,b uf, n) ; п_ wгitten= wr ir.e (fd, b uf, n) ;

При каждом обращении во з вращается счетчик байтов, указ ывающий


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

Количество байтов , подлежащих чтению или записи , может быть


совершенно произвольным. Двумя самыми распространенными
величинами яв л яются "1", которая означает передачу одного символа за
обращение (т.е. без использования бу<fx:'ра) , и "512", которая
соответствует физическому размеру блока на многих пери<fx:'рийных
устройствах. Этот последний размер будет наиболее эфрективным, но
даже ввод или вывод по одному символ у за обращение не будет
необыкновенно дорогим.

Объединив все эти факты , мы написали простую программу для


копирования ввода на вывод, эквивалентную программе копировки

файлов , написанной в л екции No1. На системе UN IX эта программа


будет копировать что угодно ку,ца угодно , потому что ввод и вывод могуг
быть перенаправлены на любой файл или устройство.
Б.8. К<'рниган, Д. М . Ричи Язы к "JIOграм""ирования С

#define bufsize 512 /*best size for pdp-ll unix.*/


mainО /*с ор у input to output*/
{
char buf[bufsize];
int п;
while((n=read(O, Ь иf, bufsize)»0)
write(1,buf,n);
}

Если размер фзйла не будет кратен bufsize, то при неюJТОРОМ


обращении к read бу,цет во з вращено меньшее число байтов, которые
затем записываются с помощью wr i te ; при сл едующем после этого
обращении к read бу,цет возвращен нуль.

Поучительно разобраться, как можно использовать функции read и


write для построения процедур более ВЫСОЮJГО уровня, таких как
g e tchar , putchar и Т.д . Вот, например , вариант функции getchar,
осуществляющий ввод без использования буф:>ра.

#define cmask 0377 /*for making char's > 0*/


gNcharO /*Lll1buffered single character input*/
(
char с;
rе lшп«rеаd(0 ,&с, 1 » 0 ? с & ClТBs k : ЕОР);
}

Переменная "С " должна быть описана как char, потому что функция
r e ad принимает указатель на символы. Возвраl.LflемыЙ символ должен
быть маскирован числ ом 0377 для гарантии его пол ожител ьности; в
противном случае з наЮJВЫЙ разряд может сделать его значение
от рицательным. ( константа 0377 под~щит ДЛЯ эвм РDР-ll , но не
обязательно для других MaI..lМH).

Второй вариант функции g e tchar осyrцeств л яет ввод бол ыl.в1ии


порциями , а выдает символы по одному за обраl.Цf'ние.

#define cmask 0377 /*for making char's>O*/


#dеfiлe bufsize 512
gNcharO /*buffered version*/
225
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

staoc char buf[bufsize J;


staoc char * ьшр = Ь иf;
static int n = О ;
if (п== О) ( /*buffer is empty*/
n=read(O, buf,bufsize );
bufp = Ь иf;
}
ге tшп« --п> = О) ? * ьшр + + & cmask : EO F);
}

8.3 . Открытие, создание, закрытие и расцепление


(UNLINK)

Кроме случая, ЮJгда по умолчанию определ ены стандартные файлы


ввода , вывода и Оl..lfl.1бок, вы должны явно открывать файлы , чтобы
затем читать из них или писать в них. Для этой цел и существуют две
точки в)[)Да: open и crea t .

функция op en весьма сходна с функцией fopen , рассмотренной в


ле кци и NQ7, за исключением того, что вместо возвращения указател я
файла она возвращает дескриптор файла , ЮJторый является просто
целым типа int .

iлt fd; fd = о р е n(name,IWIТЮdе);

Как и в случае fopen , аргумент пате является символьной СТРОЮJЙ,


соответствующей внешнему имени файла. ОднаЮJ аргумент,
определ яющий режим доступа , отличен: rwmode равно: О - для чтения,
1 - для записи, 2 - для чтения и записи. Есл и происходит какая-то
Оl..lfl.1бка, функция open возвращает" -1 "; в противном случае она
во з вращает действительный дескриптор файла.

Попытка открыть файл , ЮJторый не существует, является Оll.lИ.БЮJЙ. Точка


входа crea t предоставляет возможность создания новых файлов или
перезаписи старых. В резyn.ьтате обращения

fd=creat( name, prmde);


226
Б.8. К<'рниган, Д. М . Ричи Я ЗЫ К "JIOграм""ирования С

возвращзет дескриптор файла , если оказалось возможным создать файл


с именем пате , и " -1 "в противном случае. Если файл с таким именем
уже существует, ereat усечет его до нyn.евоЙ длины; создание файла,
который уже существует, не является Оl..lfl.1бкоЙ.

Если файл является совершенно новым, то erea t создает его с


определ еным режимом защиты, специф-щируемым аргументом pmode .
В системе файлов на UNIX с фай л ом связываются девять битов защиты
инф:Jрмации , которые управляют разрешением на чтение, запись и
выполнение для владельца файла, для группы владельцев и для всех
остал ьных пол ьзователей. Таким образом , трехзначное восьмеричное
числ о наиболее у,цобно для специф1кации разрешений. Например,
числ о 0755 свидетельствует о разрешении на чтение, запись и
выполнение ДЛЯ владельца и о разрешении на чтение и выполнение ДЛЯ

ГРУТlПы и всех остал ьных.

Для иллюстрации ниже приводится программа копирования одного


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

#dеfiлe null О
#dеfiлe bufsize 512
#define р пюdе 0644/"'гw for оwnе г, Г for gгo up, othe гs"'l
main(aгgc, argv) I"'c p: со ру f1 to f2"'1
int argc;
char *argv[];
{
int f1, f2, л;
char buf[bufsize];

if (argc ! = 3)
e rгo r(' 'usage :c p from {о", null);
if «11 =open(argv[ 1],O))== -1)
е гго г( "с р :са л' t о ре л %5", argv[ l ]);
if « f2 = crea t(argv[2 ] , p lТJ) d e))== -1)
е гго г( "с р : can't create %5", argv[2 ]);
while « n=read(fl, buf,bufsize))>O)
227
Б.8. К<'рниган, Д. М . Ричи Язы к "JIOграм""ирования С

if (write(f2, buf,n) ! = п)
е гro г( "с р : write егroг", null);
exit(O);
}
е гro Г( 5 1 ,52 )
I"' print e rгo г me55age аnd die"'l
сhзг'" 51, 52;
{
pгint~s l ,s2 );
pгint~'\n'J;
exit( l );
}

С уществует ограничение (обычно 15 - 25) на ЮJличество файлов,


которые программа может иметь открытыми одновременно. В
соответствии с этим любая программа, собирающаяся работать со
многими файлами , должна быть подготовлена к повторному
использованию дескрипторов файлов. процедура close прерывает
связь между дескриптором файла и открытым фай л ом и освобождает
дескриптор файла для использования с неЮJТОРЫМ другим файлом.
Завершение выпол нения программы через exi t или в результате
во з врата из ведущей программы приво.цит к закрытию всех открытых
файлов.

функция расцепления unlink (filename) у,цаляет из системы


файлов файл с именем filename (из данного справочного файла.
Фай л может быть сцеп л ен с другим справочником , возможно, под
другим именем - примеч. перевОДЧИка).

Упражнение 8-1

Перепишите программу са t из л екции No7, используя функции r e ad,


write , open и close вместо их эквивал ентов из стандартной
библиотеки. Проведите э ксперименты для определения относительной
скорости работы этих двух вариантов.

8.4. Произвольный доступ - SEEK и LSEEK

Нормально при работе с файлами ввод и вывод осуществляется


228
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

посл едовательно: при каждом обращении к функциям read и wr i te


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

Isee k( fd, o ffset,o гigin) ;

текyI..ЦaЯ позиция в файле с дескриптором fd передвигается на позицию


offset (смещение), которая отсчитывается от места, указ ываемого
аргументом origin (начало отсчета). Последующее чтение или запись
буАУ" теперь начинаться с этой позиции. Аргумент offset имеет тип
long ; fd и origin имеют тип int . аргумент origin может
принимать значения 0,1 или 2, указ ывая на то , что величина offset
должна отсчитываться соответственно от начал а файла, от текущей
позиции или от ЮJнца файла. Например, чтобы дополнить файл , следует
перед записью найти его ЮJнец:

lsее k(fd ,щ 2);

чтобы вернyrься к началу С'перемотать обратно ' ), можно написать:

Isее k(fd ,що );

обратите внимание на аргумент 01 ; его можно было бы записать и в


виде ( long ) О.

функция 1 see k позвол яет обращзться с файлами примерно так же , как (


БОЛЫ1.D1МИ массивами, правда ценой более медленного доступа .
слеАУЮЩ3:Я простая функция, например , считывает любое ЮJличество
байтов , начиная с произвольного места в файле.

ger(fd,pos,buf,n) J"'read n bytes from position pos"'J


int fd, п;
Iong pos;
char "' ЬШ;
{

229
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

lseek(fd, pos,O); /*get {о pos*/


ге lшп(геаd( fd , Ьuf, п));
}

в более ранних редакциях, чем редакция системы


UNIX, основная
7
точка юцца в систему ввода-вывода наз ывается seek. функция seek
идентична функции l seek, за исключением того , что аргумент of f set
имеет тип int, а не l ong. В соответствии с этим , ПОСЮJЛЬКУ на PDP-ll
целые имеют ТОЛЬЮJ 16 битов , аргумент of fset , указ ываемый функции
seek, о граничен величиной 65535; по э той причине аргумент origin
может иметь значения 3, 4, 5, ЮJторые заставляют функцию seek
умножить заданное значение offset на 512 (ЮJличество байтов в
одном 4изичесЮJМ блоке) и затем интерпретировать origin , как есл и
это О, 1 или 2 соответственно. Следовательно, чтобы достичь
произвольноro места в болыlЮМ файле, нужно два обращения к see k:
сначала одно , ЮJторое выделяет нужный бл ок. а затем второе, где
origin имеет з начение 1 и ЮJторое осуществляет передвижение на
жел аемый байт внyrри блока.

Упражнение 8-2

Очевидно, что seek может быть написана в терминалах l seek и


наоборот. наПИU.Dпе каждую функцию через другую.

8.5. Пример - реализация функций FOPEN и GETC

Давайте теперь на примере реализации функций fopen и getc из


стандартной библиотеки подпрограмм продемонстрируем , как
неЮJторые из описанных элементов объединяются вместе.

Напомним , что в стандартной библиотеке файлы описываются


посредством указател ей файлов, а не дескрипторов. указатель файла
яв л яется указател ем на структуру, ЮJторая содержит несЮJЛЬКО

элементов инфJрмации о файле: указатель буф:>ра, чтобы файл мо г


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

файла.
230
Б.8. К<'рниган, Д. М. Ричи ЯЗЫК "JIOграм""ирования С

О пи с ы вающая фай л ст руктура дан н ых соде ржи тся в файле stdio . h ,


который должен включ аться (п ос редств о м #include ) в лю бо й
и сходный фай л, в которо м используются фун кции из станда рт н о й
библ и отеки. Он также включается функци ям и это й библиотеки. В
при водимо й ни же в ыде ржке из фай ла s tdio . h и ме н а,
пр едн азнач аемые толь ко для и с п ользовани я функциями б и бли оте ки ,
н ачин аются с п од ч е р кивания, с тем чтоб ы уме ны.Iитьь в еро ятность
сов п адения с и менами в пр огр амме п ользователя.

#dеfiлe _ bufsize 512


#define _nШе 20 j"'files that сап Ье handled"'/
typedef S([UC! _ iobuf {
char '" _ptr; /"'next character pasition"'/
int _cnt; j"'пuпiJег of characters Ieft"'/
char '" _base; /* loсаtioп of buffer'" /
int _flag; /"'nюdе af fiIe access*/
int _fd; /"'те descriptor"'/
} fiIe;
ежеrn fiIe _ iаЬLnШе];

#dеfiлe s!diл(&_ iob[OJ)


#dеfiлe s!dou! (&_ iob[1J)
#dеfiлe s!derr (&_ iob[2J)

#define _REЛD 01 /* те ореп far reading "'/


#define _WRIТE 02 /'" fiIe ореп far writing */
#dеfiлe _ UNBUF 04 /* file is unbuffered *1
#dеfiлe _ВIGBUF 010 1* Ьщ buffer alioca!ed *1
#define _EOF 020 j'" EOF has оссшгеd оп this filе */
#define _ERR 040 /'" епог has оссuпеd оп this fiIe "'!
#dеfiлe NULL О
#dеfiлe ЕОР (-1)

#dеfiлe ge!c(p) (--(р)- > _сп!


>= О \
? *(р )- >_ ptr++
& 0377 : _ filebuf(p))
#dеfiлe ge!char() gе!с(s!diл)

#dеfiлe ро!с(х, р) (--(р)- > _СП! >= О \


? *(р)- > _ ptr++ = (х) : _ flushbuf((x),p))
'3>
Б.8. К <'р ниган, Д. М. Ричи Я з ык "JIOграм""ирования С

#dеfiлe putchar(x) putc(x,stdout)

в нормальном состоянии макрос getc просто уменьшает счетчик,


передвигает указатель и возвращает символ. (Если определение
#define слиll.lкDм длинное, то оно продолжается с ПОМОЩЬЮ обратной
косой черты). Если однако счетчик становится отрицательным, то getc
вызывает фунКЦИЮ f i lebu f , которая снова заполняет бу<fx:'р,
реинициализирует содержимое структуры и возвращает символ.

функция может предоставлять переносимый интер<fx:'йс и в то же время


содержать непереносимые конструкции: getc маскирует символ

числ ом 0377, которое подавляет з наковое раСLШ1рение, осуществляемое


на PDP-ll, и тем самым гарантирует положительность всех символов.

Хотя мы не собираемся обсуждать какие-либо детал и , мы все же


включили сюда определение макроса ри tc , для того чтобы показать,
что она работает в основном точно также, как и getc, обращаясь при
заполнении бу<fx:'ра к функции f 1 ushbuf .

Теперь может быть написана функция fopen . Большая часть программь


функции fopen связана с открыванием файла и расположением его в
нужном месте, а также с установлением битов признаков таким
образом, чтобы они указывали нужное состояние. функция fopen не
выделяет какой-либо бу<fx:'рной памяти ; это дела ется функцией
filebuf при первом чтении из файла.

#include <s(dlo.h>
#define prmde 0644 /*r/w for оwnег;г for others*/
fiJe *foреn(name, nюdе) /*ореп file,return fiJe ри*/
regi<;ter char *name, *rmde;
{
regi<;ter in( fd;
regic;ter те *fp;
if(*nюdе !='r'&&*rmde ! = 'w'&&*nюdе ! = 'а' ) {
fprintf(stderr,"illega! nюdе %s opening %s\n",
rmde,name);
exit(l);

юг (ф = _ iob;fp<_iob+_пfilе;fp ++ )
Щ(ф- > _flag & Lread l _ write))==O)
2з2
Б.8. К<'рниган, Д. М. Ричи Язык "JIOграм""ирования С

break; /*fоШld (гее s1ot*/


if(fp>=_iob+_пте) /*00 free s1015*/
return(null);
if(*nюdе == 'w') /*access те* /
fd =creat( пате,рrmdе);
else if(*nюdе ==' а') {
if((fd=open(nan-.., 1))==-1)
fd =creat( nan-..,рnюdе);
lseek(fd,o(2);
} else
fd =open(nan-..,O);
if(fd==-1) /*couldn't access пате*/
return(null);
(р- > _fd =fd;
fp- >_спt= О;
(р- > _base= null;
(р- > _flag &=Lread I_wrrte);
(р- > _flag I = (*nюdе == 'г')?
_read : _wrrte;
return(fp );
}

функция filebuf нескол ько более сложная. Основная тру,цность


заключается в том, ч то f i lebu f стремится разреlШ1ТЬ достyn к фай лу
и в том случае, когда может не оказаться достаточно места в памяти для

буферизации ввода или вывода. если пространство для нового буфера


может быть получено обраl.lf'нием к функци и calloc , то все отли чн о;
если же нет, то filebuf осуществляет небуферизованный ввод!
вывод, используя отдельный символ, помеl.lf'ННЫЙ в л окальном
массиве.

#include <stdio.h>
_fillbuf(fp) /*allocate and fill input buffer*/
register те *fp;
(
static char smallbuf(nfile);/*for Lll1buffered 1/0*/
char *саПосО;
if((fp-> _flag& _read)==O 11 (ср- > _flag&(EOF I_eIТ)) 1=0
return(EO F);
while(fp- >_ base==null) /*find ЬШfeг space*/
2ЗЗ
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Щф- > _flag & _uлЬuf) /*uлbuffегеd* /


(р- > _base=&smallbuf[fp- >_fd];
еЕе Щ(ф- > _base=callocLbufsize,I» == null)
{р- > _flag 1=_uлЬuf; /*сап'! gel blg buf*/
еЕе
{р- > _flag 1=_blgbuf, /*gol blg оne*/
{р- > _ptr=fp- >_base;
{р- > _cnt=read(fp- >_fd, (р- > _ptr,
fp- >_flag& _uлbuf? 1 : _btrfsize);
Щ--ф- > _cnl<O) (
Щф- > _Cnl== -1)
{р- > _flag 1= _EOF;
еЕе
{р- > _flag 1= _ err;
{р- > _cnt = О;
reluтn(EO F);
}
геtшn(*fp- > _ptr++ & 0377); /*make char positlve*/
}

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


оказывается равным нулю , что приводит к обращению к filebuf .
Если функция filebuf найдет, что этот файл не открыт для чтения,
она немедленно возвраl.Lflет EOF. В противном случае она пытается
выделить болыrnй буф:>р, а если ей это не у,цается, то буф:>р из одного
символа. При этом она заносит в flag соответствующую
инф:Jрмацию о буф:>ризации.

Раз буф:>р уже создан, функция filebuf просто вызывает функцию


r e ad для его заполнения, устанавливает счетчик и указатели и
возвраl.Lflет символ из начала буфера.

Единственный оставlШ1ЙСЯ невыясненным вопрос состоит в том, как


все начинается. Массив iob долже н быть определен и
инициализирован для stdin , stdout и stde rr :

FILE _iob[nfile] = {
(null,O,_READ,O), /*Sldin*/
(null,O,_ WRIТE,I), /*SldoUl*/
23'
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

(null,O,null,_ W R1ТE I_UNBUF,2) J*stderr*J


};

из инициализации части f 1 ag этого массива структур видно, что


файл stdin предназначен для чтения, файл stdout - для записи и
файл stderr - для записи без использования буф:>ра.

Упражнение 8-3

Перепишите функции fopen и fi1ebuf , используя поля вместо


явных побитовых операций.

Упражнение 8-4

Раз работайте и напишите функции f1ushbuf и fc1ose.

Упражнение 8-5

С тандартная библиотека содержит функцию

fseek(fp, offset, origin)

которая идентична функции 1seek, исключая то, что fp является


указателем фай л а, а не дескриптором файла. Напишите fs e ek .
Убедитесь , что ваша fseek правильно согласуется с буф:>ризацией,
сдел анной для друтих функций библиотеки.

8.6. Пример - распечатка справочников

Иногда требуется друтой вид взаимодействия с системой файлов -


определ ение информации о фай л е, а не того, что в нем содержится.
Примером может служить команда 1s ("список справочника ' ) системы
UNIX. По этой КDMaHдe распечатываются имена файлов из справочника
и, необязательно, другая информация , такая как размеры , разрешения и
Т.д.

ПОСКDл ьку, по крайней мере , на системе UNIX справочник является


просто файлом , то в таКDЙ КDMaHдe, как 1s нет ничего особенного; она
читает файл и выделяет нужные части из на.хо,цящеЙся там информации.
2з5
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Однако формат информации определ яется системой, так что 15 должна


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

Мы это частично проиллюстр ируем при написании программы f5ize .


Программа f5ize представляет собой спец и альную форму 15 , которая
печатает размеры всех файлов , указанных в списке ее аргументов. Если
один из файлов яв л яется справочником, то для обработки этого
справочника программа f 5 i ze обращается сама к себе рекурсивно. есш
же аргументы вообще отсутствуют, то обрабатывается текущий
справочник.

Для начала дадим краткий обзор структуры системы файлов.


С правочник - это фзйл, который содержит список имен фзйлов и
некоторое указание о том, где они размещаются. Фактически это
указание является индексом для другой таблицы, которую называют '1 -
узловой таблицей". Для фзйла l- узел - это то, где содержится вся
инф:Jрмация о файле, за исключением его имени. Запись в справочнике
состоит только из двух элементов: номера I -узла и имени файла. Точная
спецИttи кация поступает при включении файла 5 У5 / dir . h , КDторый
содержит

#define dirsiz 14 !"'тах length of те пате"'!


struct direct /"'structure of directory entry"'/
{
irю_t& _inо; !*irюdе number"'!
сhзг & _паlП'[dirsiz]; !*те пате"'!
};

'ТИп" ino t - это определяемый посредством typedef тип , КDторый


описывает индекс l-узловой таблицы. На PDP-ll UN IX этим типом
оказывается unsigned, но это не тот сорт инф:Jрмации, КDторый
помещают внутрь программы: на разных системах этот тип может быть
различным. Поэтому и следует использовать typedef . Полный набор
"системных"типов на)[)Дится в фай ле 5Y5/type5 . h .

функция 5tat берет имя файла и возвращает всю содержащуюся в l -узл<


инф:Jрмацию об этом файле (или -1, если имеется Оl..lfl.1бка). Таким
образом, в результате

236
Б.8. К<'рниган, Д.М. Ричи Язык "JIOграм""ирования С

Struct stat stbuf;


char *na1П';
stаt(na1П',&stЬuf);

структура s t bu f наполняется информацией из I -узл а о файл е с им енем


пате . структура , описыв а ющая возвращаемую ФУНIO.\иеЙ stat
информ ацию , на)[)Дит ся в фай л е s у s I s ta t . h и в ыгляди т сл едующи м
обр азом:

struct stat /*structше ге tшned Ьу stat*/


{
dev_t scdev; /* device of irюdе */
ioo_t st_ ioo; /* ioode пuпiJе г */
short st_rmde /* rmde bits */
short st_nlink; / *пuпiJег oflinks to file */
short st_ uid; /* owner's user id */
short st~; /* owner's group id */
dev_t sr_ rdev; /* for special files */
ofCt st_ size; /* fiIe size in characrers */
t:irп>_t st_аt:irп>; /* tirrI" last accessed */
t:irп>_t st_fТItirn:>; /* t.fiп> last rmdified */
t:irп>_t st_сt:irп>; /* tirrI" originaНу cгeated */
}

Большая ч асть этой информ ац и и о бъясняется в ЮJмм ентариях. Э лемент


st . mode содер жи т н аб ор фnагов , описыв а ющи х фай л; для у,цо бст ва
определ ения флаго в также находятся в фай ле s ys I s ta t . h .

#dеfiлe s_ifmt 0160000 /* 'уре 01 file */


#define s_ ifdir 0040000 /* directory */
#define s3chr 0020000 j* character special */
#define s_ilblk 0060000 j* bIock special *!
#define s3reg 0100000 j* regular */
#define s_ isuid 04000 /* set user id оп execur.ion */
#define s_ isgid 02000 /* set group id оп execution */
#define s_ isvtx 01000 /*save swapped text after use*/
#define s_ iread 0400 /* гead регrrrissюп */
#define s_ iwriLe 0200 /* wriLe регrrrissюп */
#define s_ iexec 0100 /* ехес ше регrrrissюп */
2з7
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Теперь мы в состоянии написать программу fsize . Если полученный


от функции stat режим указ ывает, что файл не является справочником,
то его размер уже под рукой и может быть напечатан непосредственно.
Если же он ока з ывается справочником , то мы должны обрабатывать
этот справочник отдел ьно для каждого файла; так как справочник может
в свою очередь содержать подсправочники, этот процесс обработки
яв л яется рекурсивным.

Как обычно , ведyI.ЦaЯ программа главным образом имеет дел о с


коман дной строкой аргументов она передает каждый аргумент
функции fsize в БОЛblШЙ буф:>р.

# iпс lud е <stdiO. h>


# iпс lud е <sysltypes. h» !*typedefs*/
# iпс lud е <sysldir.h> !*directory enuy structure*!
# iпс lud е <syslstat.h> !*stгuc [ше ге tшned Ьу stat*!
#dеfiлe bufsize 256
maiп(аrgс, агgv) !*fsize:р гiпt Ше sizes*/
char *argv[];
(
char buf[bufsize];
if(argc== 1) ( I*default:currern direclory*1
strcpy(buf,".');
fsize(buf);
} else
w hile (- - аrgc > О) (
strcpy(buf, *++argv);
fsize(buf);
}
}

Функция fsize печатает размер файла. Если однако файл оказывается


справочником , то fsize сначала вызывает функцию directory для
обработки всех указанны х в нем фай л ов. Обратите внимание на
использованиеименфлагов s ifmt и ifdir изфаЙла stаt . h .

fsize{пате) /* ргiпt size for пате*!


char *паlП';
{
238
Б.8. К<'рниган, Д. М. Ричи Язык "JIOграм""ирования С

Stnк:t stat stbuf;


if(stat(nalТ<',&s 'buQ == -1) (
fprintf(stderт,"fsize:can't find %s\n",паlП');
return;

if((stЬuf.st_nюdе & s3mt)==s_ifdir)


diree,ory(nalТ<' );
printf(" %81d %s\n",stЬuf.st_s ize,паlП');
}

Функция directory является самой сложной. Однако з начительная ее


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

dirесtoгу(nalП') !"'fsize for а1l ffies in пате"'!


char "'паlП';
(
struct direct dirbuf,
char "'пЬр, "'пер;
int L fd;
nbр = паlП' +strlе n( пате);
"'nbp++='f; !"'add slash to directory паlП''''!
if(nbp+dirsiz+2 >= naтe + bufsize) !"'пате (00 long"'!
return;
if((fd = open(nalТ<',O))== -1)
return;
while(read(fd,(ehar *)&dirbuf,sizeo~dirbuQ»O) \(
if(dirbuf.d_ino==O) !"'slot rюt in U5e"'!
continue;
if(slfemp (dirbuf.d_name,".')==O
11 S lfСЩJ(dirЬuf.d_name,"..')== О
continue; !"'skip self аnd parent"'!
for (i=O,nep=nbp;i<dirsiz;i++)
"'пер++ =dirЬuf.d_nalП'[i];
"'пер++ = ' \О ' ;
fsize(nalТ<' );
}
close(fd);
"'--nbр = '\О'; !"'restore nalП'''' !

239
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Если некоторая дыра в справочнике в настоящее время не используетс я


(потому что фай л был удален), то в соответствующее I-узловое число
равно нуnю, и эта позиция пропускается. Каждый справочник также
содержит запись в самом себе, называемую ".", и о своем родителе, ".. ";
они, очевидно, также должны быть пропущены , а то программа будет
работать весьма и весьма долго.

Хотя программа fsiz e довольно специализированна, она все же


демо нстрирует пару важных идей. во-первых, многие программы не
являются "системными программами ". они только используют

ишlюрмацию , форма или содержание которой определяется


операционной системой. Во-вторых, для таких программ существенно,
что представление этой информации входит ТОЛЬЮJ в стандартные
''заголовочные файлы", такие как st at .h и di r. h, и что программы
включают эти файлы, а не помещают фактические описания внутрь
самих программ.

8.7. Пример - распределитель памяти

В лекции No5 мы написали бесхитростный вариант функции al l oc .


Вариант, ЮJторый мы напишем теперь, не содержит ограничений:
обращения к функциям al l oc и free могут перемежаться в любом
порядке; ЮJгда это необходимо, функция alloc обращается к
операционной системе за до полнительной памятью. Кроме того, что
эти процедуры полезны сами по себе, они также илл юстрируют
некоторые соображения, связанные с написанием маlШ1нно-зависимых
программ относительно маlШ1нно-независимым образом, и показывают
практичеСЮJе применение структур, объединений и ЮJнструкций
typedef .

Вместо того, чтобы выделять память из СЮJмпилированнorо внутри


массива 4иксированного р азме ра , функция a l loc будет по мере
необходимости обращаться за памятью к операционной системе.
ПОСЮJЛ ЬКУ разли чные события в программе M Oryr требовать
асинхронного выделения памяти, то память, управляемая a l loc , не
может быть непрерывной. В силу этого свободная память хранится в
,<о
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

виде цепочки свободных блоков. Каждый блок включает размер,


указатель следующего блока и саму свободную память. Блоки
упорядочиваются в порядке возрастания адресов памяти, причем

посл едний бл ок (с наиболы.LIИМ адресом) указывает на первый , так что


цепочка фактически оказывается кольцом.

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


тех пор, пока не будет найден достаточно БОЛЫIЮЙ блок. Если этот блок
имеет в точности требуемый размер , то он отцепляется от списка и
передается пользователю. Если же этот блок слиl.LIкDм велик, то он
разделяется , нужное количество передается пользователю , а остаток

во зв ращзется в свободный список. Если достаточно БОЛЫIЮ ГО блока


найти не удается, то операционной системой выделяется новый блок,
который включается в список свободных блоков; затем поиск
во зобновляется.

Освобождение памяти также влечет за собой просмотр свободного


списка в поиске подходящего места для введения освобожденного
блока. Если этот освобqд,ивll.IИЙСЯ блок с какой-либо стороны
примыкает к блоку из списка свободных блоков, то они объединяются в
один блок болыuего размера, так что память не становится слиl.LIкDм
раздробленной. Обнаружить смежные блоки просто, потому что
свободный список содержится в порядке возрастания адресов.

Одна из проблем, о которой мы упоминали в лекции NQS, заключается в


обеспечении того, чтобы возвращземая функцией alloc память была
выровнена подходящим образом для тех объектов, которые бy,цyr в ней
храниться. Хотя маll.lИНЫ и различаются , для каждой маll.lИНЫ
cyrцeCТByeT тип, требующий наиболыl.lиx ограничений по размещению
памяти , если данные самого ограничительного типа можно поместить в

некоторый определенный адрес, то это же возможно и ДЛЯ всех


остальных типов. Например, на lВM 360/370, HONEVWELL 6000 и
многих друтих маll.lИнах любо й объект может храниться в границах,
соответствующим переменным типа double ; на PDP-ll будуг
достато чны переменные типа in t .

С вободный блок содержит указатель следующего блока в цепочке,


запись о размере блока и само свободное пространство; управляющзя
инф:Jрма ция в начале называется заголовком. Для упрощения

'"
Б.8. К<'рниган, Д.М. Ричи Язык "JIOграм""ирования С

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

typedef int align; /"'forces аlignrп:'пt оп pdp-ll "'/


tmion header { /"'free bIock header"'/
struct {
uniоп header *ptr; /"'next free block"'/
unc;igned size; /*size oft:his free block"'/
} 5;
аligп х; /"'force аlignrп:'пt ofblocks*/
};
typedef uniоп header header;

Функция alloc округляет т ребуемый разме р в с имволах до нужного


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

s i ze загол о вка. указатель, возвращаемый функцией alloc , указывает


на сво бодное простран ство, а не н а сам заголовок.

statК: header base; /*empty list (о get started"'/


statК: header "'allocp=null; /"'last allocated block*/
char "'alloc(nbytes)/"'general-purpose storage allocator"'/
W1Signed nbytes;
{
header "'nюгесorеQ;
register header "'р, "'g;
register int пunits;
пuni1s= 1+(nbytes+sizeof(header)- l )/sizeo~header);
if «g=allocp )== null) \( /*00 free list yet*/
base.s pu=allocp=g=&base;
base.s.size=O;
}

for (p=g>s.ptr; ; g=p, p=p- >s.ptr) {


if (p- >s.size>=nunits) { /"'Ьщ enough"'/
Б.8. К<'рниган, Д. М. Ричи Я з ы к "JIOграм""ирования С

if (p- >s.size==nunit5) l*exactly"1


g- >s. ptr=p- >s.ptr;
еЕе { J"'allocate tail еnd* !
p- >s.size- =nunits;
p+=p- >s.size;
p- >s.size=nunits;
}
allocp=g;
re ruтn(char * )(р + 1));
}
if(p==allocp) J"'wrapped aroLll1d free ша"' /
if((р = IТXJгecoгe( nunit5 ))= = null)
return( null); J*oone left* J
}

Переменная base используется для начала работы. Если allocp имеет


значение nu l l , как в случае первого обращения к alloc , то создается
вырожденный свободный список: он состоит из свободного блока
размера нуnъ и указателя на самого себя. В любом случае затем
исследуется свободный список. Поиск свободного бл ока подходящего
размера начинается с того места ( al l ocp ), где был найден последний
блок; такая стратегия помогает сохран и ть однородность диска. Если
найден слиI..LIюм БОЛЫ1ЮЙ блок, то пользователю предлагается его
хвостовая часть; это привqцит к тому. что В заголовке исходного блока
нужно изменить только его размер. во всех случаях возвращаемый
пользователю указатель указывает на действительно свободную
область, лежащую на единицу дальше заголовка. Обратите внимание на
то, что функция al l oc перед возвращением" р " преобразует его в
указатель на символы.

Функция morecor e получает память от операционной системы.


Детали того, как это осуществляется, меняются, конечно, от системы к
системе. На системе UN IX точка входа sbrk ( n) возвращает указатель
на " n " дополнительных байтов памяти.( указатель удовлетворяет всем
ограничениям на выравнивание). Так как запрос к системе на
выделение памяти является сравнительно до рогой операцией, мы не
хотим делать это при каждом обращении к функц и и a l loc . Поэтому
функция mo r ecor e округляет затребованное число един иц до большегс

'"
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

значения; этот БОЛbl.LМЙ блок бу,цет затем разделен так, как необ)[)Димо.
Масшгабирующая величина является параметром, который может быть
подобран в соответствии с необходимостью.

#define nalloc 128 /*#units to allocate at опсе*/


static header *nюге со ге(пu) /*ask system for IП'nюry* /
unsigned nu;
{
char *sbrkO;
register char *ср;
register header *up;
register int rnu;
rnu=nalloc*(( nu+nalloc-l )/nalloc);
cp=sbrk( rnu*sizeo f(header»;
if «int)cp==-I) 1*00 space at all*1
return( null);
up=(header *)ср;
up- >s.size=mu;
free«char *)(ор + 1));
return(allocp);
}

Если больше не осталось свободного пространства, то функция sbr k


возвращает" - 1 ", хотя nu11 был бы лучшим выбором. Для надежности
сравнения" - 1 " должна быть преобразована к типу i n t . Снова
при)[)Дится многократно использовать явные преобразо вания (перевод)
типов, чтобы обеспечить определенную независимость функц и й от
детал ей представления указателей на различных маll1Инах.

и посл еднее - сама функция f ree . Начин ая с а 1 1оср , она просто


просматривает свободный список в поиске места для введения
свободного блока. Это место находится либо между двумя
сyrцeствующими блоками, л ибо в одном из концов списка. В любом
случае, если освобqд,ивши йся блок примыкает к одному из соседн и х,
смежные блоки объединяются. Следить нужно только затем, чтобы
указатели указывали на то, что нужно, и чтобы разме ры были
установлены правильно.

free(ap) /*put blocke ар in free list*/

'"
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

cha.r *а р ;
{
register header * р , *g;
p=(header*)ap-l; /*point {о header*/
for (g=allocp; !(p>g & & p>g- >s. ptr);g=g- >s. ptr)
if (g>=g->s. ptr & & (p>g 11 p<g- >s.ptr))
break; /*а! о ne е nd or other* /
if (p+p->s.size==g- >s. ptr) { /*join to upper nbr*/
p- >s.size += g->s. ptr->s.size;
p- >s. ptr = g- >s. ptr- >s. pu;
} ебе
p- >s. ptr = g- >s. ptr;
if (g+g->s.size== p) { /*join to lower nbr*/
g->s.size+=p- >s.size;
g->s. pu =p->s.рu;
} ебе
g->s. pu =p;
а По с р = g;
}

Хотя распределение памяти по своей суги зависит от используемой


маlllИНЫ , приведенная выше программа показывает, как эту зависимосгь

можно ре гул ировать и о граничить весьма небольuюй частью


программы. Использование typedef и union позвол яет справиться с
выравниванием (при условии, что функция sbrk обеспечивает
подходящий указатель ). Переводы типов организуют выпол нение
явного преобразования типов и даже справляются с неу,цачно
разработанным сисгемным интерфейсом. И хотя рассмотренные здесь
подробности связаны с распределением памяти , общий подход равным
образом применим и к другим ситуациям.

Упражнение 8-6

функция из стандартной библиотеки calloc (п , s iz e) возвращает


указатель на "n " объектов размера size , причем соответствующая
память инициализируется на нуль. НаПИll.lИ.те программу для cal l oc ,
используя функцию al l oc либо в качесгве образца , либо как фующию,
к которой происходит обращение.
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Упражнение 8-7

функция al l oc принимает затребованный размер, не проверяя его


правдоподобности; функция f ree полагает, что тот блок, который она
должна освободить , содержит правильное значение в поле размера.
Усовершенствуйте э ти процеАУРЫ, затратив больше усилий на проверку
Оl.LlИбок.

Упражнение 8-8

НаПИl.LIИте функцию bfree (р , n) , которая включает произвольный


бл ок" р" из " n " символов в список свободных блоков , управляемый
функциями al l oc и free . С помощью функции bfree пользовател ь
может в любое время добавлять в свободный список статический или
внеll.lНИЙ массив.

",
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Справочное руководство ПО языку С

Дается исчерпывающее описание языка С.

9.1 . Справочное руководство по языку "С"

Это руководство описывает язык 'С" для компьютеров DEC PDP-ll,


HONEYWELL 6000, IВM система/370 и lNТERDAJA 8/32. там, где есть
расхождения, мы сосредотачиваемся на версии для PDP-ll, стремясь в
то же время указать детал и , которые зависят от реализации. За малым
исключением, эти расхождения непосредственно обусловлены
основными свойствами используемого аппаратного обор у,цо вания;
различные компиляторы обычно вполне совместимы.

9.2 . Лексические соглашения

Имеется шесть классов лексем: идентиф1каторы , ключевые слова,


константы , строки, операции и другие разделител и. Пробелы, табуляции
, новые строки и комментарии (совместно , "пустые промежyrки'), как
описано ниже, игнорируются, за исключением тех случаев, когда они

служат разделителями лексем. Необходим какой-то пустой промежyrок


для разделения идентиф1каторов , ключевых слов и констант, которые в
противном случае сольются.

Если сделан разбор ЮDДНОГО потока на лексемы вплоть до данного


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

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

Комментарий открывается символами /* и закан чивается символами


* / . Комментарии не вкладываются друг в друга.

9.2.3 . Идентификаторы (имена)


Б.8. К<'рниган, Д. М . Ричи Я ЗЫ К "JIOграм""ирования С

Идентиф1катор - это последовательность букв и циiW; первый с им вол


должен быть буквой. Подчеркивание _ считается буквой. Буквы
нижнего и верхнего регистров разл ичаются . значащими яв л яются не

более , чем первые восемь символов, )[)тя можно использовать и


больше. На внеl.LlНие идентиф1каторы , которые используются
различными ассемблерами и загрузчиками, накладываются бол ее
жесткие ограничения:

Табл ица 9. 1.
DEC PDP-ll 7 символ ов 2 регистра
HONEYWELL 60006 символ ов 1 регистр
lВM 360/370 7 символ ов 1 регистр
l NТERDAJA 8132 8 символ ов 2 регистра

9.2.4. Ключевые слова

Сл едующие идентиф1каторы зарезервированы для использования в


качестве ключевых сл ов и не MOryr использоваться иным обра зом:

• int
• e x ter n
• else
• cha r
• r egister
• for
• float
• typede f
• do
• doubl e
• static
• wh i le
• struct
• goto
• sw i tc h
• un i on
• r etur n
'"
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

• case
• long
• sizeof
• defau l t
• short
• break
• entry
• unsigned
• continue
• *auto
• if

Ключевое слово entry в настоящее время не используется каким- л ибо


компилятором ; оно зарезервировано для использования в 6y,zJ.yщeм. В
некоторых реализациях рез ервируется также слова fortran и asm

9.2.5. Константы

Имеется несколько видов констант , которые перечислены ниже. Далее


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

размеры.

9.2.5.1 . Целые константы

Целая константа, состояЩ1Я из последовател ьности ЦИ@ , считается


восьмеричной , если она начинается с О (ци@а н ул ь), и десятичной в
противном случае . Ци@ы 8 и 9 имеют восьмери чные значения 1О и
11 соответственно. последовательность ЦИ@ , которой предшествуют
символы Ох (нуль, х-мал енькое) или ОХ (нуль Х-болыrnе),
рассматривается как шестнадцатеричное целое. Шестнадцатеричные
ци@ы включают буквы от а (маленькое) или А (больuюе) до f
(мал енькое) или F (больuюе) со з начениями от 10 до 15 . Десятичная
константа, величина которой превышает наибольшее маl.LIИнное целое
со з наком, считается длинной; восьмеричная и л и шестнадцатеричная
константа, которое превышает наибольшее маl.LIИнное целое без знака,
также считается длинной.

'"
Б.8. К <'рниган, Д. М. Р ич и Я з ык "JIOграм""ирования С

9.2.5.2. Я в ные длинные константы

Десятичная, восьмеричная или шестнадцатеричная константа, за


которой непосредственно следует 1 (эл ь-маленькое) или L ( эл ь­
большое), является длинной константой. Как обсуждается ниже, на
некоторых маlШ1нах целые и длинные значения мосуг рас смат риваться

как иденти чные.

9.2.5.3. Символьные константы

С и мволь ная константа - это символ, заключенный в одиночные


кавычки, как, напри м ер, ' х ' 3начением символьной константы
является численное знач ение этого символа в маlШ1ННОМ

представ ле нии набора символов.

Некоторые неграф1ческие символы, одиночная кавычка ' и обратная


косая черта \ MOfyr быть представлен ы в соответствии со следующей
таблицей условных посл едовательностей:

Таблица 9.2.
новая строка NULF! \п

горизонтальная табуляция нт \t
символ возврата на одну по з ицию BS \Ь
возврат каретки CR \r
переход на новую страницу FF \f
обратная косая черта \ \\
одиночная кавычка \ '
комбинация битов 000 \ddd

Условная последо вательность \ ddd состоит из обратной косой черты ,


за которой следуют 1, 2 или 3 восьмеричных циiWы , которые
рассматриваются как задающие значение желаемого символа.

С пеци альным случаем это й конструкции является по следовательность


\0 (за н улем не следует циiWа), которая определяет символ nul . Если
следующий за обратной косой чертой с имвол не совпадает с одним из

250
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

указанных. то обратная косая черта игнорируется.

9.2.5.4. ПЛавающие константы

ПЛавающая константа состоит из целой части, десяти чной точки,


дробной части, буквы е (маленькая) или е (БОЛЫl.Iая) и целой
экс поненты с необязательным знаком. Как целая, так и дробная часть
являются последовательностью цифр. Либо целая, л ибо дробная часть
(но не обе) может отсутствовать; либо десятичная точка, л ибо е
(маленькая) и экспонента (но не то и другое одновременно) может
отсутствовать. Каждая плавающзя константа считается имеющей
дв ойную точность.

9.2.6. Строки

Ст рока - это последовател ьность символов, заключенная в дво йные


кавычки, как. например, " ... ". Строка имеет тип" массив символов"
и класс памяти sta tic . Ст рока инициализирована указанными в ней
символами. Все строки, даже иде нтично записанные , считаются
различными. Компи лятор помещзет в конец каждой строки н улево й
байт \ О, с тем чтобы просматривающзя строку программа могла
определить ее конец. Перед стоящим внутри строки символом двойной
кавычки" должен быть постав л ен символ обратной косой черты \ ;
кроме того, могут использоваться те же условия последовательности,

что и в символьных константах. И последнее, обратная косая черта \, за


которой непосредственно слеАУет символ новой строки, игнорируется.

9.2.7. Характеристики аппаратных средств

Следую щая ниже таблица суммирует некоторые свойства аппаратного


оборудования, которые меняются от машины к машине. Хотя они и
вл ияют на переносимость программ , на практике они представляют

меньшую проблему, чем это может казаться заранее.

Таблица 9.3.
25>
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

DEC PDP-ll HONEYWELL lВM 370 I NТERDAТA8/32

ASC ll ASC ll EBCDlC ASC ll


char 8 BlТS 9 ВlTS 8ВlТS 8ВlТS

in t 16 36 32 32
short 16 36 16 16
l ong 32 36 32 32
float 32 36 32 32
doubl e 64 72 б4 64
range -38/+38 -38/+38 -76/+76 -76/+76

9.3. С интаксич еская нотация

в используемой в этом руководстве синтаксической нотации


синтаксические катеroрии выделяются курс и вом (прим. перев.: в
настоящее время синтаксические категории вместо курсива выделяются

подчеркиванием), а л итерные сл ова и с и мволы - жирным шрифrом.


Альтернативные категории перечисляются на отдельных строчка х.
Необязательный символ, терминальный или нетерминальный,
указ ывается индексом" необ ", так что

{ в ыр ажени е
-- ---- --- н ео б }

указ ывает на необязательное выражение , заключенное в фигурных


скобках. Синтаксис суммируется в пункте 18.

9.4. Что в имени тебе моем?

Язык "С" основывает интерпретацию идентиф1катора на двух


признаках идентификатора: еro классе памяти и его типе. Класс памяти
определ яет место и время хранения памяти, связанной с
идентификатором; тип определяет смысл величин , наXDДЯЩИХСЯ в
памяти , определ енной под идентификатором.

Имеются четыре класса памяти: автомати ч еская, статическая, внеlШlЯЯ и


ре гистровая. Автоматические переменные являются локальными ДЛЯ

252
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

каждого вызова бл ока и исчезают при выходе из этого блока.


Статич еские переменные яв л яются л окальными , но сохраняют свои
значения для следуюl.Ц"ГО входа в блок даже посл е того, как управление
передается за предел ы блока. Внешние переменные существуют и
сохраняют свои з начения в течение выполнен ия всей программы и
мосут и спользоваться для связи между функциями, в том числе и между
незав исимо скомп и лированными функциями . РеЛ1стровые переменные
хранятся (если это возможно) в быстрых реЛ1страх маlШ1НЫ; подобно
автоматическим переменным они являются локальными для каждого

блока и исчезают при выходе из этого блока.

в языке "С " предусмотрено несколько основных типов объектов:


объекты, написанные как символы ( cha r ), достаточно велики, чтобы
хранить любой член из соответствующего данной реализации
внутреннего набора символов, и если действительн ый символ из этого
набора с и мволов хранится в символьной переменной , то ее значение
эквивалентно целому коду этого с и мвола. В символьных переменных
можно хранить и другие вел ичины , но реализация бmет маlШ1ННО­
зависимой.

Можно использовать до трех размеров целых, описываемых как short


int, int и long int . Длинные целые занимают не меньше памяти,
чем короткие, но в конкретной реал и зации может оказаться, что л ибо
короткие целые, либо дл инн ые целые, либо те и другие будуг
эквивалентны простым целым. 'Пр остые" целые имеют естественный
размер , предусматриваемый архитектурой используем ой маlШ1НЫ;
друтие размеры вводятся для у,цо влетворения специальных

потребностей.

Целые без з нака, описываемые как unsigned, подчиняются законам


арифметики по модулю 2 * * n , где n число битов в их
представ ле нии. (На PDP-ll длинные величины без знака не
предусмотрены).

ПЛавающие одинарной точности ( float ) и плавающие двойной


точности ( double ) в некоторых реализациях могут быть синонимами.

Поскол ьку объекты упомянутых выше типов могут быть ра зумно


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

253
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

арифметическими. т ипы char и in t всех размеров совместно будуг


называться целочисленными . типы float и double совместно будуг
называться плавающими типами.

Кроме основных арифvtетических типов c)llЦf'CТByeT концептуально


бесконечный КJJacc производных типо в, которые образуются из
ОСНОВНЫХ типов слеАУЮЩИМ образом:

• массивы объектов большинства типов;


• функци и , которые возвращзют объекты заданного типа;
• указатели на объекты дан ного типа;
• структуры, содержащие последовательность объектов различных
типов;

• объединения, способные содержать один из нескольких объектов


разл ичных тип ов.

Вообще говоря, эти методы построения объектов MOryr применяться


рекурсивно.

9.5. Объекты и L-значения

Объект является доступным обработке участком памяти; L-значение -


это выражение , ссылающееся на объект. Очевидным при мерам
выражения L- значения является и дентификатор. С)llЦf'ствуют операци и ,
резynьтатом которых являются L- значе ния ; если, например , е
выражение указанного типа, то *е яв ляется выражением L-значен ия,
ссылающимся на объект е. Название 'L-значение" проис.ХОДИТ от
выражения присваивания el=e2 , в котором левая ч асть должна быть
выражением L-значения. При послеАУЮщем обсуждении каждой
операции бу,цет указываться, ожидает л и она операндов L- значения и
выдает л и она L-значение.

9.6. Преобразования

ряд операций может в зависимости от своих операндов вызывать


преобразование значение операнда из одного типа в другой. В этом
разделе объясняются резynьтаты, которые следует ожидать от таких
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

преобразованиЙ. В ОДНОМ из разделов, распол оженных ниже подводятся


итоги преобразований, требуемые БолыlD1ствомM обычных операций;
эти сведения дополняются необ)[)Димым образом при обсуждении
каждой операции.

9.6.1. СИМВОЛЫ и целые

С имвол или короткое целое можно использовать всюду. где можно


использовать цел ое. Во всех случаях значение преобразуется к целому.
Преобраз ование бол ее короткого целого к бол ее длинному всегда
сопровождается знаковым раСLШ1рением; целые являются величинами

со знаком. Осуществляется или нет знаковое раСllD1рение для символов,


зависит от используемой маLШ1НЫ, но гарантируется, что член
стандартного набора символ ов неотрицател ен. из всех MallD1H,
рассматриваемых в этом рутводстве, только PDP-ll осуществляет
знаковое раСLШ1рение. Область значений символьных переменных на
PDP-ll меняется от -128 до 127; символы из набора ASCl1 имеют
положительные з начения. Символьная I'Dнстанта , заданная с помощью
восьмеричной усл овной посл едовательности , подвергается знаковому
раСLШ1рению и может оказаться отрицательной; например, '\377' имеет
значение -1.

Когда более длинное целое преобразуется в бол ее короткое или в cha r ,


оно обрезается слева; ЛИlllН.ие биты просто отбрасываются.

9.6.2. Типы FLОШ и DOUBLE

Вся плаваюl.LflЯ ариф\1етика в "С" выполняется с двойной точностью


каждый раз, когда объект типа float появляется в выражении , он
удлиняется до double посредством добавления н ул ей в его дробную
часть. когда объект типа double должен быть преобра зован к типу
f l oat , например , при присваивании, перед усечением double
окрутляется до длины float .

9.6.3 Плавающие и целочисленные величины

255
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

Преобразование плавающих значений к цело численному типу имеет


тенденцию быть до нею)Торой степени маlШ1нно-зависимым; в
частности направление усечения отрицательных чисел меняется от

маlШ1не к маlШ1не. Результат не определен, если значение не помещается


в предоставляемое пространство.

Преобразование цело численных значений в плавающие выполняется


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

9.6.4. УказатеIП1 и целые

Целое или длинное целое может быть прибавлено к указателю или


вычтено из него; в этом случае первая величина преобразуется так, как
указывается в разделе описания операции сложения.

Два указателя на объекты одинакового типа мосут быть вычтены; в этом


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

9.6.5. Целое без знака

Всякий раз, когда целое без знака объединяется с простым целым,


простое целое преобразуется в целое без знака и результат оказывается
целым без знака. Значением является наименыuее целое без знака,
соответствующее целому со знаком (по модулю 2...... размер слова). В
дв ои чном дополнительном представ лении это преобразование является
чисто умозрительным и не изменяет фактическую комбинацию битов.

Когда целое без знака преобра зуется к типу lo n g , значение результата


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

9.6.6 Арифметические преобразования

Подавляющее болыш1ствоo операций вызывает преобразование и


256
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

определ яет типы резyn.ьтата аналогичным образом. Приводимая ниже


схема в дальнейшем будет называться "обычными арифметическими
преобразо ваниями". Сначала любые операнды типа c h ar или short
преобразуютс я в int , а лю бые операнды типа f l oat преобразуются в
double . Затем, если какой- л ибо операнд имеет тип double , то другой
преобразуется к типу double , и это будет типом результата. В
противном случае, если какой-либо операнд имеет тип long, то другой
операнд преобразуется к типу long, и это и будет типом результата. В
противном случае, если какой-либо операнд имеет тип uns i gn ed, то
другой операнд преобразуется к типу unsig n ed, и это будет типом
резynьтата. В противном случае оба операнда БWJ.YТ иметь тип i nt , и этс
будет типом резynьтата.

9.7. Выражения

Ста рJ.LП.1НСТВО операций в выражениях совпадает с порядком следования


основных подразделов настоящего раздела, начиная с самого высокого

уровня стаРJ.LП.1нства. Так, например, выражениями , указываемыми в


качестве операндов операции +, являются выражения, определеные в

разделах ниже. Внутри каждого подраздела операции имеет одинаковое


старJ.LП.1НСТВО. В каждом подразделе для описываемых там операций
указывается их ассоциативность слева или справа. СтарJ.LП.1НСТВО и
ассоциативность всех операций в выражениях рез юмируются в
грамматической сводке, распол оженной ниже.

В противном случае порядок вычислений выражений не определен. В


частности , компилятор считает себя в праве вычислять пqд,выражения в
том порядке, который он находит наиболее эфtx:>ктивным, даже если эти
подвыражения привqд,ят к побочным эфtx:>ктам. Порядок, в котором
происходят побочные э lfxlEкты, не специфицируется. Выражения,
включающие коммутативные и ассоциативные операции ( *, +, &, !, л ),

мосут быть переупорядочены произвольным образом даже при наличии


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

При вычислении выражений обработка переполнения и проверка при


дел ении являются маJ.LП.1нно-зависимыми. Все существующие

25 7
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

реализации языка "С" игнорируют переполнение целых; обработка


ситуаций при делении на О и при всех особых случаях с п л авающими
числами меняется от маJ.LП.:IНЫ к маJ.LП.:Iне и обычно выполняется с
помощью библиотечной функции.

9.7.1 Первичные выражения

Первичные выражения, включающие ., ->, индексацию и обращения к


функциям , группируются сл ева направо.

Пер вичн ое вы раже ни е:


иде н ти ф1като р
константа

ст р ока

(в ыражени е)
п е р в ичн ое-в ыр аже ни е [ в ыражени е]
п е р в ичн ое-в ыр аже ни е (с пи с о к- в ыр ажений н ео
п е р в ичн ое-l-зн ачени е. Иде н т и4и катор
п е р в ичн ое-в ыр аже ни е -> и ден т иф1 катор
спи сок- в ыраже ний :
в ы р ажение

спи сок- вы раже ний , в ыр ажение

Идентиф1катор явл яется первичным выражением при условии, что он


описан подходящим образом , как э то обсуждается ниже. тип
идентификатора определяется его описанием. Если, однако , типом
идентификатора является " массив ... ", то значением выражения,
состоящего из этого иденти4икатора , является указатель на первый
объект в этом массиве, а типом выражения будет" указатель на ... ".
Более того , идентиф1катор массива не является выражением L-
значения. подобным образом идентификатор , который описан как "
функция, во з вращающая ... ", за исключением того случая, ко гда он
используется в по з иции имени функции при обращении , преобразуется
в" указатель на функцию , которая во з вращает ... ".

константа является первичным выражением. В зависимости от ее


ф:Jрмы типом константы может быть in t , long или double .

258
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Ст рока является первичным выражением. Исходным ее типом является


" массив символов "; но следуя тем же самым правилам , которые
приведены выше для и дентификаторов, он модифицируется в
указатель на символы", и результатом является указатель на первый
символ строки. (имеется исключение в некоторых инициализато рах; см.
ниже)

Выражение в крутлых скобках является первичным выражением, тип и


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

Первичное выражение, за которым следует выражение в квадратных


скобках, является первичным выражением. Интуитивно ясно, что это
выражение с индексом. Обычно первичное выражение имеет тип "
указатель на ... ", индексное выражение имеет тип int , а типом
результата является Выражение е l [е2] по определению
идентично выражению * ({ еl) + (е2» . Все, что необходимо для
понимания этой записи, содержится в этом разделе; вопросы,

связанные с понятием идентификаторов и операций * и +


рассматриваются в да нной лекции выводы суммируются ниже.

Обращение к функции яв ляется первичным выражением, за которым


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

Перед обращением любые фактические аргументы типаfloat


преобразуются к типу double , любые аргументы типа char и ли shor
преобразуются к типу int , и, как обычно, имена массивов
преобразуются в указатели. Никакие другие преобразования не
выпол няются автоматически; в частности, не сравнивает типы

259
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

фактических аргументов с типами ф:Jрмальных аргументов. Если


преобразование необ>DДИМО, используйте явный перевод типа ( cast ).

При подготовке к вызову фующии делается копия каждого фактического


параметра; таким образом, все передачи аргументов в языке 'С"
осуществляются строго по значению. функция может изменять значения
своих ф:Jрмальных параметров, но эти изменения не влияют на
значения фактических параметров. С другой стороны имеется
во зм ожность передавать указатель при таком условии, что функция
может изменять значение объекта, на который этот указатель указывает.
Порядок вычисления аргументов в языке не определен; обратите
внимание на то, что различные компиляторы вычисляют по разному.

Допускаются рекурсивные обращения к любой функции.

Первичное выражение , за которым следует точка и иде нтиф1катор,


является выражением. Первое выражение должно быть L-значением,
именующим структуру и л и объединение, а иденти4икатор должен быть
именем члена структуры или объединения. Результатом является L-
значение, ссылающееся на поименованный член структуры или
объединения.

Первичное выражение, за которым следует стрелка (составленная из


знаков - и > ) и идентификатор, является выражением. первое
выражение должно быть указателем на структуру и л и объединение, а
идентификатор должен именовать член это й структуры или
объединения. Результатом является L- з начение, ссылающееся на
поименованный член структуры или объеди нения, на который
указывает указатель ное выражение.

Следовательно, выражение el->mos является тем же самым, что и


выражение (*el) . mos. структуры и объединения рассматриваются
далее. Приведенные здесь правила использования структур и
объединений не навязываются строго, ДЛЯ того чтобы иметь
во зм ожность обойти механизм типов.

9.7.2. Унарные операции

260
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Выражение с унарными операциями группируется справа налево.

Унарное-выражение:

• * выражение
• & L- з начение
• - выражение

• ! выражение
• - выражение
• ++ L-значение
• - - L-значение
• L- з начение ++
• L- з начение --
• (имя- типа) выражение
• sizeof выражение
• sizeof имя- типа

Унарная операция * означает косвенную адресацию: выражение должно


быть указателем, а результатом является L-значение, ссылающееся на
тот объект, на который указывает выражение. Если типом выражения
является" указатель на.,.", то типом результата бу,цет ".. ,",

Результатом унарной операции & является указатель на объект, к


которому ссылается L-значение. Если L-значение имеет тип ", .. ", то
типом результата бу,цет" указатель на .. ,",

Результатом унарной операции - (минус) является ее операнд, взятый с


противоположным знаком. Для величины типа unsigned результат
получается вычитанием ее значения из 2 * * n (два в степени n ), где n -
числ о битов в in t . Унарной операции + (плюс) не существует.

Результатом операции лоrnческого отрицания ! является 1, если


значение ее операнда равно О , и О , если значение ее операнда отл ично
от нуля. Результат имеет тип int . Эта операция применима к любому
арифметическому типу и л и указателям.

Операция - дает обратный код, или дополнение до единицы, своего


операнда. Выпол няются обычные ариф\1етические преобразования.

'6>
Б.8. К <'рниган, Д. М. Р ич и Я з ык "JIOграм""ирования С

Операнд должен быть цело численного типа.

Объект, на который ссылается операнд L-значения преф1ксной


операции + + ,увеличивается. Значением является новое значение
операнда, но это не L- з начение. Выражение ++ х эквивалентно х+ =l .
ИнфJрмацию о преобразованиях смотри в разбо ре операции сложения
и операции присваивания.

Преqмксная операция -- аналоrnчна пре4иксной операции ++, но


приводит К уменьшению своего операнда L-значения.

При применении постфиксной операции ++ к L-значению результатом


является значение объекта, на который ссылается L-значение . Посл е
того, как результат принят к св едению, объект увеличивается точно
таким же образом, как и в случае префиксной операции ++. Результат
и меет тот же тип, что и выражение L-значения.

При применении постфиксной операции -- к L-значению результатом


является значение объекта, на который ссылается L-значение . По сл е
того, как ре зультат принят к св едению, объект уменьшается точно таким
же образом, как и в случае префиксной оп е рации -- о Результат имеет
тот же тип, что и выражение L- з начения.

Заключенное в круглые скобки имя типа данных,стоящее пер ед


выражением, вызывает пр еоб разование з начения этого выражения к
указанному типу. Эта ЮJнструкция называется перевод ( cast ). Имена
типов описываются ниж е.

Операция sizeof выдает разме р своего операнда в байтах. (Понятие


байт в языке не определено, разве ТОЛЬЮJ как значение оп е р а ции
sizeof . ОднаЮJ во всех С.YJ.Цeствующих р еал изациях байтом является
пространство , необходи м ое ДЛЯ хран ени я объекта типа cha r ). При
применении к массиву ре зультатом яв ляется полное число байтов в
массиве. Размер определяется из описаний объектов в выражении. Это
выражени е сема нтически является целой ЮJнстантой и может быть
использов ано в любом месте, где требуется ЮJнстанта. Основное
применение эта операция н аходит при связях с процедурами , подобны м
р ас предел и телям памяти , и в системах ввода -выв ода.

262
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Операция sizeof может быть также применена и к заключенному в


круглые скобки имени типа. В э том случае она выдает размер в байтах
объекта указанного типа.

конструкция sizeof (тип) рассматривается как целое, так что


выражение sizeof (тип) - 2 эквивал ентно выражению ( sizeof
(тип» - 2.

9.7.3. МУЛЬТИГUIИкативные операции

Мультипликативные операции *, / и % группируются сл ева направо.


Выполняются обычные арифvlетические преобразования.

Мультипликативное-выражение:

• выражение * выражение
• выражение / выражение
• выражение % выражение

Бинарная операция * означает умножение. Операция * ассоциативна , и


выражения с несколькими умножениями на одном и том же уровне

мосут быть перегруппированы компилятором.

Бинарная операция / означает деление . При делении положительных


целых ОС)ll..l.fствляется усечение по направ л ению к нулю , но если один

из операндов отрицателен, то фJрма усечения зависит от используемой


маlllИНЫ. На всех маlllИнах. охватываемых настоящим руководством,
остаток имеет тот же з нак , что и делимое . Всегда справедливо, что
(а/Ь) *Ь+а%Ь равно а (если Ь не равно О ).

Бинарная операция % выдает остаток от деления первого выражения на


второе. Выпол няются обычные арифvlетические преобразования.
Операнды не должны быть типа f loa t .

9.7.4. Аддитивные операции

263
Б.8. К <'рниган, Д. М. Р ич и Я з ык "JIOграм""ирования С

Адцитивные операции + и группируются слева направо.


Выполняются обычные арифметические преобра зования. Для каждой
операции имеются неЮJторые до полнительные возможности,

связанные с типами операндов.

Адцитивное-выражение:

• выражен и е + выражение
• выражен и е - выражение

Результатом операции + является сумма операндов. Можно складывать


указатель на объект в массиве и значение любого целочисленного типа.
во всех случаях посл еднее преобразуется в адресное смещение
посредством умножения его на длину объекта, на ЮJТОРЫЙ указывает
этот указатель. Результатом является указатель того же самого типа, что
и исходный указатель, ЮJторый указывает на ДРУГОЙ объект в том же
масс иве, смещенный соответствующим образом относительно
первоначал ьного объекта. Таким образом , если р является указателем
объекта в масс и ве, то выражение р+ 1 яв ляется указателем на
слеАУЮЩИЙ объект в это м массиве.

Никакие другие ЮJмб ин ации типов для указателей не раз решаются.

Операция + ассоц и ативна, и выражение с несЮJЛ ЬКИМИ сложениями на


том же самом уровне MOryr быть переупорядо ч ены ЮJмпилятором.

Результатом операции - является разность операндов. Выполняются


обычные арифметические преобразования. Кроме того, и з указателя
может быть вычтено значение любого целочисленного типа, причем ,
прово,цятся те же самые преобразования, что и при операции сл ожения.

Если вычитаются два указателя на объекты одинаЮJВОГО типа, то


результат преобразуется (делением на длину объекта) к типу in t ,
представляя собой чи сло объектов, разделяющих указываемые объекты.
Если эти указатели не на объекты и з одного и того же массива, то таЮJе
преобразование, вообще говоря, даст неожи данные результаты , потому
что даже указатели на объекты о,цинаЮJВОГО типа не обязаны отличаться
на величин у, кратную длине объекта.

'"
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

9.7.5. Операции сдвига

Операции сдвига « и » группируются слева направо. Для обеих


операций проводятся обычные арифметические преобразования их
операндов , каждый из которых должен быть цел очисленного типа.
Затем правый операнд преобразуется к типу in t ; результат имеет тип
левого операнда. Результат не определен, если правый операнд
отрицателен или БОЛ Ыllе или равен , чем длина объекта в битах.

Выражение-сдвига:

• выражение << выражение

• выражение >> выражение

Значением выражения еl«е2 яв л яется еl (интерпретируемое как


комбинация битов), сдвинутое влево на е2 битов; освобождающиеся
биты заполняются н ул ем. Значением выражения е l »е2 является еl ,
сдвинутое вправо на е2 битовых позиций. Если еl имеет тип
unsigned, то сдвиг вправо гарантированно будет л о гическим
(заполнение нулем); в противном случае сдвиг может быть (и так и есть
на РDР-ll) ариф\1етическим (освобождающиеся биты заполняются
копией знакового бита).

9.7.6. Операции отношения

Операции отношения группируются сл ева направо, но этот факт не


очень полезен; выражение а<Ь<с не означает того , что оно казал ось бы
должно о з начать.

Выражение-отношения:

• выражение < выражение

• выражение > выражение

• выражение <= выражение

• выражение >= выражение

265
Б.8. К<'рниган, Д. М . Ричи Я ЗЫ К "JIOграм""ирования С

Операции < (меньше), > (больше), <= (меньше или равно) и >= (больше
или равно) все дают О , есл и указанное отношение л ожно, и 1, если оно
исгинно. Резynьтат имеет тип in t . Выпол няются обычные
арифметические преобразования. Мосут сравн и ваться два указателя;
резynьтат завис ит от относительного расположения указываемых

объектов в адресном просгрансгве. Сравнение указател ей переносимо


ТОЛЬЮJ в том сл учае, если указатели указ ывают на объекты из одного и
того же массива.

9.7.7. Операции равенства

Выражение-равенсгва:

• выражен и е == выражен и е

• выражен и е != выражен и е

Операции == (равно) и != (не равно) в точносги аналогичны


операциям отношен ия, за исключением того , ч то они имеют бол ее
низкий .УРовень сгаР l.шш сгва. (Поэтому значение выражения
a<b == c<d равно 1 всякий раз, когда выражение а<Ь и c<d имеют
одинаЮJвое значение исгинносги).

указатель можно сравнивать с целым, но резynьтат бу,цет машин но­


независимым ТОЛЬЮJ в том сл учае , если целым яв л яется ЮJнсганта О .
Гарантируется, что указател ь, ЮJторому присвоено значение О, не
указ ывает ни на какой объект и на самом деле оказывается равным О ;
общепринято считать таЮJЙ указатель нynем.

9.7.8. Побитовая операция 'и'

Выр ажени е-и:


в ыраже ни е & в ыр аже н и е

Операция & яв л яется ассоциативной, и включающие & выражения


мосут быть переynорядочены. Выполняются обычные арифметические
преобразования ; резynьтатом является побитовая функция 'и ' операндов.

266
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Эта операция применима только к операндам цело чи сленного типа.

9.7.9. Побитовая операция исключающего 'или'

Выр ажени е-искл ючаю щего-ил и :


в ы р ажение л в ыр аже н и е

Операция л яв л яется ассоциативной, и включающие л выражения

мосут быть переупорядочены. выполняются обычные арифмети ческие


преобразования ; резyn.ьтатом является побитовая функция
исключающего 'или ' операндов. Операция применима только к
операндам целочисленного типа .

9.7.10. Побитовая операция включающего 'или'

Выр ажени е-вкл ючаю ще го -или:


в ы р ажение I Выр ажение

Операция I является ассоциативной, и содержащие I выражен ия мосут


быть переупорядочены. выполняются обычные арифметические
преобразования ; резyn.ьтатом является побитовая функция включающего
' и ли' операндов. Операция применима только к операндам
цело числ енного типа.

9.7.11. Логическая операция 'и'

Выр ажени е-л о rn ч еского- и :


в ы р ажение && в ыр ажен и е

Операция && группируется слева направо. Она возвраl.Lflет 1, если оба


ее операнда отличны от нуля , и О в противном сл учае. В отл ичи е от &
операция & & гарантирует вычисление слева направо ; более того , есл и
первый операнд равен О , то з начение второго операнда вообще не
вычисляется.

267
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

Операнды не обязаны быть одинакового типа, но каждый из них


до лжен быть л ибо одного из основных типов, л ибо указателем.
Результат всегда имеет тип int .

9.7.12. Операция логического 'или'

Выражение- л оrnческого-и л и:
выражение 11 выражение

Операция II Группируется слева направо. Она возв ращзет 1, если


один из операндов отличен от нуля , и О в противном случае. В отличие
от операции I Операция II Гарантирует вы чи сление слева направо;
более того, если первый операнд отл ичен от нуля, то значение второго
операнда вообl.Цf' не вычисляется.

Операнды не обязаны быть одинакового типа, но каждый из них


долже н быть л и бо одного из основных типов, л ибо указателем.
Результат всегда имеет тип int .

9.7.13. Условная операция

Условное-выражение:
выражение? в ыраже ние: выражение

Условные выражен ия группируются слева направо. Вычисляется


значение первого выражения, и есл и оно отлично от н уля, то

результатом будет значение второго выражен и я; в противном случае


результатом будет значен и е третьего выражения. Если это возможно,
проводятся обычные арифvlети ч еские преобразования, с тем, чтобы
привести второе и третье выражения к обl.Цf'МУ типу; В противном
случае, если оба выражения являются указателями одинакового типа, то
результат имеет тот же тип ; в противном случае одно выражение

должно быть указателем, а другое - константой О , и результат будет


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

268
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

9.7.14. Операция присваивания

Имеется ряд операций присваивания, каждая из которых грynпируется


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

Выражение-присваивания:

• L- з начение = выражение
• L- з начение += выражение
• L- з начение - = выражение
• L- з начение * = выражение
• L- з начение 1= выражение
• L- з начение %= выражение
• L- з начение > >= выражение
• L- з начение « = выражение
• L- з начение & = выражение
• L- з начение л = выражение
• L- з начение I= выражение

Когда производится простое присваивание С ' = " значение выражения


заменяет значение объекта, на которое ссылается L-значение. Если оба
операнда имеют арифvlетический тип, то перед присваиванием правый
операнд преобра зуется к типу левого операнда.

о свойствах выражения вида еl оп = е2 , где оп - одна из


перечисленных выше операций, можно сделать вывод, если учесть, что
оно эквивалентно выражению еl = еl оп (е2) ; однако

выражение еl вычисляется только один раз. В случае операций += и -


= лев ый операнд может быть указателем, причем при этом
(целочисленный) правый операнд преобразуется таким образом, как
объяснено далее; все правые операнды и все отличные от указателей
лев ые операнды должны иметь арифvlетический тип.

269
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Используемые в настоящее время ЮJмпи л яторы допускают

присваивание указателя цел ому, целого указателю и указател я указателю

друтого типа. таЮJе присваивание является чистым ЮJпированием без


каких-либо преобразованиЙ. ТаЮJе ynотребление операций
присваивания яв л яется непереносимым и может привqцить к

указателям , ЮJторые при испол ьзовании вызывают ошибки адресации.


Тем не менее гарантируется , что присваивание указател ю константы О
дает нулевой указатель, ЮJторый можно отл ичать от указателя на любой
объект.

9.7.15. Операция запятая

Выр ажени е-с-зап ято й:


в ы р ажение, в ы раже н ие

Пара выражений , раздел енных запятой, вычисляется слева направо и


значение левого выражения отбрасывается. типом и з начением
результата является тип и з начение правого операнда. Эта операция
грynпируется слева направо. В ЮJнтексте , где запятая имеет
спец и альное з нач ение, как. например , в списке фактических аргументов
функций. Или в списках инициал изаторов. Операц ия запятая,
описываемая в этом разделе , может появ ляться ТОЛЬЮJ в кругл ых СЮJбках
; например, функц ия

~ a,(t=3,t+ 2 ) ,с)

имеет три аргумента, второй из которых имеет значение 5.

9.8. Описания

Описан ия испол ьзуются для указания интерпретации , ЮJторую яз ык 'С"


будет давать каждому и дентшlJ1катору; они не обязательно резервируют
память , соответствующую и дентшlJ1l<aТОРу. описан ия имеют qюрму

Описание:
с п ецифи като ры-о пи сани я с пи сок- о п исателей
н еоб;

270
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Описатели в списке описателей содержат описываемые


идентификаторы. Специф1каторы описан ия представляют собой
последовательность специqмкаторов типа и специф1каторов класса
памяти.

С пециф1каторы- описания:

Спецификаторы-описания:
специф1катор-типа спецификаторы-описания
необ
специф1катор-класса-памяти с пецификатор-опи сания
необ

список должен быть самосогласованным в смысле, оп и сываемом ниже.

9.8.1. С пецификаторы класса памяти

Ни же перечисл яются спецификаторы класса памяти :

С пециф1катор-класса-памяти:

• auto
• static
• e x ter n
• r egister
• typede f

С пециф1катор t ypede f не реализует памяти и называется


"спецификатором класса памяти" только по синтаксическим
соображениям. См ысл различных классов памяти был обсужден выше.

Описания auto , static и register служат также в качестве


определений в том смысле, что они вызывают резе рвирование нужного
количества памяти. В случае e x te r n должно присyrствовать внеlШlее
определение, указываемых идентиф1каторов где-то вне функции, в
которой они описаны.

Описание r e g is t er лучше всего представлять себе как оп и сание аи t<


Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

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


переменные бy,цyr часго использоваться. ЭФРективны только несколько
первых таких описаний. Кроме того, в реrnсграх MOryr храниться только
переменные определеных типов; на PDP-ll это int , char или
указатель. Сущесгвует и другое ограничение на использование
ре гисгровых переменных: к ним нельзя применять операцию взятия

адреса &. При разумном использовании реrnсгровых описаний можно


ожидать получения меньших по размеру и более бысгрых программ, но
улучшение в БWJ.Yl..ЦE'М генерирования кодов может сделать их
ненужными.

Описание может содержать не более одного спецификатора класса


памяти. Если описание не содержит спецификатора класса памяти, то
считается, что он имеет значение auto , если описание находится

внутри некоторой функции , и ex ter n в противном случае .


исключение: функции нико гда не бывает автоматическими.

9.8 .2. Спецификаторы типа

Ниже перечисл яются спецификаторы типа.

С пециф1катор- типа:

• char
• short
• int
• long
• unsigned
• float
• double
• спецификатор- структуры -и л и-объединения
• определяющее-тип-имя

С лова long, short и unsigned можно рассматривать как


прилагательные; допусгимы следующие комбинации:

short iш
272
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Iong irn
W1Signed int
Iong float

Последняя комбинация означает то же, что и doubl e . В остальном


описание может содержать не более ОДНОГО спецификатора типа. Есл и
описание не содержит спецификатора типа , то считается , что он имеет
значение int .

С пециф1каторы структур и объединений обсуждаются в разделе ниже


описания с определяющими тип именами typedef обсуждаются
дал ее.

9.8.3 .0ПИСi!Гели

В)[)ДЯщий в описание список описател ей представляет собой


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

С пи с о к-о пи сателей:
иници ал и зируемый -о пи сатель
иници ал и з ируемый -о пи сатель, с пи со к-опи сателе й
ини циализ ируе мы й -о писатель:
о писател ь-иници ал и затор

н еоб

Инициал изаторы описываются в данной лекции. Спецификаторы и


описания указ ывают тип и класс памяти объектов, на которые
ссылаются описатели. Описатели имеют сл едующий синтаксис

о пи сатель:

идентиф1като р
( о писатель )
>1< о пи сатель

о писатель О
о писатель [константное- в ы р ажение
н ео б]

27з
Б.8. К<'рниган, Д. М. Ричи Язык "JIOграм""ирования С

Группирование такое же как и в выражениях.

9.8.4. Смысл описателей

Каждый описатель рассматривается как yrверждение того, что когда


конструкция той же самой ф:Jрмы , что и описатель, появляется в
выражении , то она выдает объект указанного типа и указанного КJJacca
памяти. Каждый описатель содержит ровно один идентиф1катор; это
именно тот идентификатор , который и описывается.

Если в качестве описателя появ ляется просто идентификатор, то он


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

Описатель в круглых скобках идентичен описател ю без круглых скобок,


но кругл ые скобки MOгyr изменять связи в составных описателях.
При меры смотри ниже.

Представим себе описание

t di

где t - спецИttикатор типа (подобный int и т.д.), а di - описатель.


Предположим , что это описание приводит к тому, что соответствующий
идентификатор имеет тип" ... t ", где" ... " пусто, если di просто
отдельный идентификатор (так что тип х в " int х " просто int ).
Тогда, если di имеет ф:Jрму

*d

то содержащийся иденти4икатор бу,цет иметь тип указатель

на t ".

Если di имеет ф:Jрму

dO
то содержащийся идентификато р имеет тип функция,

возвращающая t ".

'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Если di имеет форму

d[константное-выражение]

или

d[ ]

то содержащийся идентификатор имеет тип" . .. массив t ". В


первом случае константным выражением является выражение, значение

которого можно определить во время компиляции и которое имеет тип

int . (Точное определение константного выражения дано ниже.) Когда


несколько спецификаций вида массив из" оказываются
при мыкающими, то создается многомерный массив ; константное
выражение, задающее границы массивов, может отсутствовать только у

первого члена этой последовательности. ТаЮJе оп ускание полезно, когда


массив является внешним и его фактическое определение, ЮJторое
выделяет память, приводится в друтом месте. Первое ЮJнстантное
выражение может быть опущено также тогда, ЮJгда за описателем
слеАУет инициализация. В этом случае размер определяется по числу
приведенных инициализируемых элементов.

Массив может быть образован из элементов одного и з основных типов,


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

Не все возможности, ЮJторые разре шены с точки з рения указанного


выше синтаксиса, фактически допустимы. Имеются слеАУЮщие
ограничения: функции не могут возвращзть массивы, структуры,
объединения или функции, хотя они могут возвращзть указатели на
такие вещи; не c)ll..ЦeCТByeT массивов функций, хотя могут быть массивы
указателей на функции. Аналогично, структуры или объеди нения не
могут содержать функцию , но они могут содержать указатель на
функцию.

в качестве при мера рассмотрим описание

iлl4 *ф, Ю, *БрО, (*pfi)();

в ЮJтором описывается целое i, указатель ip на целое, функция f,


275
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

возвращающая цел ое , ФУНIЩИЯ fip , возвращающая указатель на цел ое , и


указатель pfi на фунlЩию , которая возвращает целое . Особенно
полезно сравнить два последних описателя. Связь в * fip () можно
представить в виде * (fip () ) , так что описанием предпол агается, а
такой же конструкцией в выражении требуется обраl.Цf'ние к функции
fip и послеДУЮl.Цf'е использование косвенной адресации для выдачи с
помощью полученного результата ( указателя ) цел ого. В описател е
(* pfi) () дополнительные скобки необходимы , поскольку они точно
так же , как и в выражении, указывают, что косвенная адресация через

указатель на фунlЩИЮ выдает функцию , которая затем вызывается; эта


вызванная функция возвращает цел ое.

в качестве другого примера приведем описание

Поаl fa [17], *afp [17];

в котором описывается массив чисел типа float и массив указател ей


на числа типа f 1 оа t. Наконец,

staoc inl хЗ d[ З ][ 5 ][ 7];

описывает статический трехмерный массив цел ых размером 3"'5"'7.


Более подробно, x3d является массивом из трех эл ементов; каждый
элемент является массивом пяти массивов; каждый последний массив
яв л яется массивом из семи целых. Каждое из выражений x3d,
x3d [i] , x3d[i ] [ j ] и x3d[i ] [j ] [k ] может разумным образом
появляться в выражениях. Первые три имеют тип" массив ", последнее
имеет тип int .

9.8.5 . Описание структур и объединений

С труктура - это объект, состоящий из последовател ьности именованных


чл енов. Каждый член может быть произвол ьного типа. Объединение -
это объект, который в данный момент может содержать любой из
нескольких членов. Спецификаторы и объединения имеют одинаковую
<jюрму.

С п ецифи катор- ст руктуры- и л и -объеДин е ни я


276
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

стр уктура- ил и- объединение { спи со к- о пи саний- ст р уктур ы }

и ден т иф1катор ст руктуры -и л и- объединения


{ сп и сок- о пи сан ий- ст руктур ы}
и ден т иф1 катор ст руктур ы- ил и- объедин е ни я

С труктура-или-объединение:

• 51RUCT
• UNЮN

С писок- описаний - структуры является последовательностью описаний


чл енов структуры или объединения:

С пи с о к-о пи саний -ст р уктур ы :


о пи сани е-ст р уктур ы

о пи сани е-ст р уктур ы список-о пи са ний -ст руктуры


о писание - структур ы :

с п е циф1 като р- тип а спи сок-о писателе й- ст руктур ы


с пи с о к-о пи сателей - структур ы :
о пи сатель- структур ы

о пи сатель- структур ы , с пи с о к- о пи сателей - структур ы

в обычном сл учае описатель структуры яв л яется просто описателем


чл ена структуры или объединения. Член структуры может также
состоять из специфицированного числа битов. Такой член называется
также полем; его длина отдел яется от имени поля двоеточием.

Описатель- ст руктур ы :
о пи сатель

о пи сатель: ко н стан тно е выр ажени е

: ко нстан т н о е в ыражени е

Внyrри структуры описанные в ней объекты имеют адреса, которые


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

277
Б.8. К <'р ниган, Д. М. Ричи Я з ык "JIOграм""ирования С

полями, помещаются в машинные целые; они не перекрывают границы

слова. Поле, которое не умещается в оставшемся в данном слове


пространстве, помещается в следуюl.Lf'е слово. Поля выделяются справа
налево на PDP-ll и слева направо на других машинах.

Описатель структуры, который не содержит описателя, а только


двоеточие и ширин у, указывает неименованное поле, полезное для

заполнения свободного пространства с целью соответствия задаваемых


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

границу слова. При этом предполагается, что "следующее поле"


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

автомати чески.

Сам язык не накладывает ограничений на типы объектов, описанных


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

Объединение можно представить себе как структуру, все члены которой


начинаются со смещения О и размер которой достато чен , чтобы
содержать любой из ее чле нов. В каждый момент объединение может
содержать не более одного из св оих чле нов.

С пециф1катор структуры или объединения во второй фJрм е, Т.е. Один


из

struct идентификатор {список-описаний-структуры}


tmion идентиф1катор {список-описаний-структуры}

описывает идентификатор в качестве ярлыка структуры (или ярлыка


объединения) структуры, специфицированной этим списком.
По следующее описание м ожет затем и с пользовать третью фJрму
специ4икатора, один из

struct идентификатор
278
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

tmioп идент иф1 като р

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


ссылаются на самих себя ; они также позволяют неоднократно
использовать приведенную тол ью один раз длинную часть описания.

Запрещается описывать структуру или объединение, ЮJТорые содержат


образец самого себя , но структура и л и объединение MOryr содержать
указатель на структуру или объединение такого же вида, как они сами.

Имена членов и ярлыков мосуг совпадать с именами обычных


переменных. Однако имена ярлыков и членов должны быть взаимно
различными.

Две структуры мосуг иметь общую начальную посл едовательность


чл енов; это означает, что тот же самый член может появиться в двух
различных структурах, если он имеет одинаковый тип в обеих
структурах и если все предыдущие члены обеих структур одинаковы.
(Фактически компилятор тол ько проверяет, что имя в двух различных
структурах имеет одинаЮJВЫЙ тип и одинаковое смещение , но есл и
предшествующие члены отличаются , то ЮJнструкция оказывается

непереносимоЙ).

Вот простой пример описания структуры:

struct mode {
char (word[20];
int count;
struct mode "'left;
struct mode "' right;
};

Такая структура содержит массив из 20 символов, целое и два указател я


на подобные структуры. Как ТОЛ ЬЮJ приведено таЮJе описание ,
описание

struct mode s, "'sp;

говорит о том, что s является структурой указанного вида, а sр


яв л яется указател ем на структуру указанного вида. При наличии этих
описаний выражение

279
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Sp- >count

ссылается к полю cou n t ст р уктуры, на ЮJторую указывает sp


выражение

s.left

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

S. гighl- > twоrd(Оj

ссылается на первый си мвол члена two r d правого подцерева из s.

9.8.6. Инициализация

Описатель может указывать начальное значение описываемого


идентификатора. Инициализатор состо и т из выражения или
заключенного в ф1гурные СЮJбки сп и ска значений, перед ЮJторыми
ставится знак =.

Инициализатор:
= выражение

= {список-инициализатора}
= {список-инициализатора,}
с пи со к-инициали зато р а:

выражение

с пи со к-инициали зато ра ,список- иници ал и зато ра

{cnисок- инициализатора }

Все выражения, входящие в иниц и ализатор статичесЮJЙ или внеl.LIН.еЙ


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

описанные переменные и функц ии .

Гарантируется, ч то неинициализированные статические и внеl.LIН.ие


переменные получают в качестве начальных значений О·,
280
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

неинициализированные автоматические и регистровые переменные в

качестве начальных значений содержат мусор.

Когда инициализатор применяется к скаляру ( указателю или объекту


арифметического типа ), то он состоит из одного выражения, возможно
заключенного в ф1гурные скобки. Начальное значение объекта
находится из выражения; выпол няются те же самые преобразования,
что и при присваивании.

Когда описываемая переменная является агрегатом ( структурой или


массивом ), то инициализатор состоит из заключенного в фигурные
скобки и разделенного запятыми списка инициал изаторов для чл енов
агрегата. Этот список состав л яется в порядке возрастания индекса или в
соответствии с порядком членов. Если агрегат содержит подагрегаты, то
это прави л о применяется рекурсивно к членам агрегата. Есл и
количество инициализаторов в списке оказывается меньше числа

чл енов агрегата , то остаВlШ1еся чл ены агрегата запол няются н ул ями.

Запрещается инициализировать объединения или автомати ческие


агрегаты.

4игурные скобки могут быть опущены следующим образом. Если


инициализатор начинается с левой ф1гурной скобки, то послеАУЮЩИЙ
разделенный запятыми список инициализаторов инициализирует
чл ены агрегата; будет ОlШ1бкой, если в списке окажется бол ыuе
инициализаторов , чем членов агрегата . Если однако инициализатор не
начинается с левой ф1гурной скобки, ТО из списка берется только
нужное для членов данного агре гата число эл ементов ; остаВll.IИеся

э лементы используются для инициализации следующего члена агрегата,

частью которого является настоящий агрегат.

Последнее сокращение доп ускает во з можность инициализации массива


типа char с помощью строки. В этом сл учае члены массива
посл едовательно инициализируются символами строки.

Например,

iлl х[] = {1,3,S };

описывает и инициализирует х как одномерный массив ; поскольку


размер массива не специф1цирован, а список инициализитора

'"
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

содержит три элемента, считается, что массив состоит из трех членов.

Вот при мер инициализации с полным использованием фигурных


скобок:

Поаt у( 4]( З] = {
{ 1, 3,5 },
{2, 4,6 },
{З,S, 7} ,
};

3десь 1, 3 и 5 инициал изируют первую строку массива у [ О] , а именно


у [ О] [О ] , у [ О ] [1 ] и у [ О] [ 2] . Аналогичным образом следующие
две строчки инициализируют у [1] и у [2]. Инициализатор
заканчивается преждевременно, и, следовательно массив у [3]
инициализируется н улями. В точности такого же эфректа можно было
бы достичь, написав

Поаt у( 4]( З] = {
1, 3, 5, 2, 4, 6, 3, 5, 7
};

Инициализатор для у начинается с левой фигурной скобки, но


инициализатора для у [ О] нет. Поэтому используется 3 э лемента из
списка. Аналогично следующие три элемента используются
посл едовательно для у [1] и у [2 ] . сл едующее описание

Поаt у(4]( З] = {
{l ], {2}, {З } , {4 ]
};

инициализирует первый стол бец у (если его рассматривать как


двумерный массив), а остальные элементы заполняются н ул ями.

и наконец , описание

char msg[ ] = "syntax e rтo г о п line %s\п";

демонстрирует инициализацию эл ементов символьного массива с

помощью строки.

282
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

9.8 .7. Имена типов

в двух случаях (для явного указания типа преобразования в конструкции


перевода и для аргументов операции s i z ео f ) желательно иметь
во зм ожность задавать имя типа данных. Это ОС)'l.Lf'ствляется с помощью
''имени типа ", которое по существу является описанием объекта такого
типа, в котором опyrцeно имя самого объекта.

Имя типа:
с пециф.iкатор-типа абстрактный-описатель
абстрактный-описатель:
п усто

(абстрактный-описатель)
'" абстрактный описатель
абст рактный-опи сатель О
абстрактный-описатель [константное выражение
неоБJ

Во избежании двусмысленности в констр)'Ю.\ии

(абстрактный описатель)

требуется, чтобы абстрактный-описатель был непуст. При этом


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

iлl
int'"
iлl *[3]
iлl (*)[3]
iлl *0
iлl (*)()

именуют соответственно типы ''целый'', " указатель на целое", " массив


из трех указателей на целое", " указатель на массив из трех целых", "
функция, возвращзющая указатель на целое" и " указатель на функцию,
во зв ращающую целое".

283
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

9.8.8 . ТYPEDEF

описания, в ЮJторых ' 'класс памяти" специф'щирован как typedef , не


вызывают выделения памяти. вместо этого они определяют

идентификаторы , ЮJторые позднее можно использовать так. словно они


являются ключевыми словами, имеющими основные или производные

типы.

Определяющее-тип-имя
идентиф1катор

в пределах области действия описания со специф1катором typedef


каждый идентиф1катор , являющийся частью любого описателя в этом
описании, становится синтаксически эквивалентным ключевому слову,

имеюl.Ц"МУ тот тип, ЮJторый ассоциирует с идентиqмкато ром в


описанном выше разделе смысле. Например, после описаний

typedef in! miles, "'klicksp;


typedef struct ( double се, im; ) complex;

ЮJнструкции

miles dlstance;
ежеrn klicksp metricp;
complex Z, "'zp;

становятся заЮJННЫМИ описаниями ; при этом типом distance


является int, типом metr icp - " указатель на in t ", типом z -
специф1цированная структура и типом zр - указатель на такую
структуру.

С пециф1катор typ edef не вводит каких-либо совершенно новых


типов, а ТОЛЬЮJ определяет синонимы для типов, ЮJторые можно было
бы специфицировать и другим способом. Так в приведенном выше
примере переменная distance считается имеющей точно таЮJЙ же
тип, что и любой другой объект, описанный в int.

9.9 . Операторы

'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

За исключением особо оговариваемых случаев, операторы

выполняются последовательно.

9.9.1 . Операторное выражение

БОЛЫllИНСТВО операторов являются операторными выражениями,


которые имеют qюрму

выражение;

обычно операторные выражения являются присваиваниями или


обращениями к функциям.

9.9.2 . Составной оператор (или блок)

с тем чтобы допустить возможность использования нескольких


операторов там, где ожидается присyrствие только одного,

предусматривается составной оператор (который также и эквивалентно


наз ывают ''блоком'):

составной оператор:
{список-описаний список-операторов
необ необ}
список-описаний:
описание

описание список-опи са ний


список-операторов:

оператор

оператор с пи со к-операторов

Если какой-либо иденти ф1катор из списка- описаний был описан


ранее , то во время выполнения блока BHell.lНee описание подавляется и
снова вступает в силу после выхода из блока.

Любая инициализация автоматических и регистрационных переменных


проводится при каждом входе в блок через его начало. В настоящее
время раз решается (но это плохая практика) передавать управление

285
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

внутрь бл ока; в таком сл учае э ти ини циализации не выполняются.


Инициал изации статических переменных проводятся только один раз,
когда начинается выполнение программы.

Находящиеся внутр и блока внешние описания не резерв ируют памяти,


так что их ин ици ализация не разрешается.

9.9 .3. Условные операторы

Имеются две qюрмы условных операторов:

1. IF (выражен и е) оператор
2. IF (выражение) оператор ELSE оператор

в обоих сл учаях вычисляется выражение и , если оно отлично от нуля,


то выпол няется первый подоператор. во втором случае, есл и
выражение равно нулю , выполняется второй подоператор. Как обы чн о,
двусмысленность" e l se "разрешается связыванием else с последним
встречающимея if , у которого нет else.

9.9.4. Оператор WНILE

Оператор while им еет фJрму

while (в ы раже н ие ) о п ер ато р

Подоператор выполняется повторно до тех пор, пока значение


выражения остается отличным от н ул я. проверка производится перед

каждым выполнением оператора.

9.9.5. Оператор 00

Оператор do имеет фJрму

do о пе р атор w hile (в ы ражения )

286
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Оператор выполняется повторно до тех пор, пока значение выражения


не станет равным нynю. Проверка производится после каждого
выполнения оператора.

9.9 .6. Оператор FOR

Оператор for имеет ф:Jрму

(в ыр аже ни е -l ; в ы р ажение - 2; в ы раже н ие-З )о п е ратор


н еоб н ео б необ

Оператор for эквивалентен следующему

в ыр ажени е- l ;

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


о п е ратор

в ы ражение - З

Таким образом , первое выражение определяет инициализацию цикла;


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

при ращение параметра, которое провqд,ится после каждой итерации.

Любое выражение или даже все они мосут быть опущены. Если
отсутствует второе выражение, то предложение с while считается
э квивалентным whi l e ( l ) ; другие отсутствующие выражения просто
оп ускаются из приведенноro выше расширения.

9.9 .7. Оператор SWIТCH

Оператор swi tch (переключатель), вы з ывает передачу управления к


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

выражения. оператор имеет ф:Jрм у

switch (в ыражени е) о п ератор

287
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

В выражении проводятся обычные арифметические преобразования, но


резynьтат долже н иметь тип in t . оператор обычно является составным.
Любой оператор внутри этого оператора может быть помечен одним
или более вариантным префиксом сазе , имеющим qюрму:

case константное в ыражение:

где константное выражение должно иметь тип int . Никакие две


вариантные константы в одном и том же переключателе не могут иметь

одинаковое з начение. Точное определение константного выражения


приводится в данной лекции.

Кроме того, может присутствовать самое БОЛЫllее один операторный


префикс вида

default:

При выполнении оператора sw i tch вычисляется в)[)ДЯщее в него


выражение и сравнивается с каждой вариантной константой. Если одна
из вариантных констант оказывается равной значению этого
выражения, то управление передается оператору, который следует за
совпадающим вариантным префиксом. Если ни одна из вариантных
констант не совпадает со з начением выражения и если при этом

присутствует префикс defau l t , то управление передается оператору,


помеченному этим префиксом. Если ни один из вариантов не подходит
и префикс de f аu 1t отсутствует, то ни один из операторов в
переключателе не выпол няется.

Сами по себе префиксы сазе и defaul t не изменяют поток


управления, которое беспрепятственно проходит через такие префиксы .
Для выхода из пере ключателя смотрите оператор b r ea k, раздел,
находящийся ниже.

Обычно оператор, который входит в переключатель, является


составным. описания могут появляться в начале этого оператора, но

инициализации автоматических и регистровых переменных будуг


неэфfx:'ктивными.

9.9.8 . Оператор BREAК


288
Б.8. К<'рниган, Д. М. Ричи Я зык "JIOграм""ирования С

Оператор

bгeak;

вызывает завершение выполнения наименыuего охватывающего этот

оператор оператора wh i le , do , for или swi tch ; управление


передается оператору, следующему за завершенным оператором.

9.9.9. Оператор CONТINUE

Оператор

сопtiпuе;

приводит к передаче управления на продолжающую цикл часть

наименьшего охватывающего этот оператор оператора wh i le , do или


for ; то есть на конец цикла. Более точно, в каждом из операторов

while( ... ) { do { for( ... ) {

со пtiп: ; со пtin: ; со пtin: ;


} while(... );

Оператор cont i nue эквиваленте н оператору goto contin . (За


contin : следует пустой оператор.

9.9.10. Оператор возврата

Возвращение из функции в вызывающую программу осуществляется с


помощью оператора return , который имеет одну из следующих форм

Ге[ШП;

Ге[ШП в ыражение;

В первом случае возвраl.lflемое значение неопределе но. во втором


случае в вызывающую фующию возвраl.lflется значение выражения.
Если требуется, выражение преобразуется к типу функции, в которой оне

289
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

появляется, как в случае присваивания. Попадание на юнец функции


эквивалентно возврату без возвраl.Lflемого з начения.

9.9.11. Оперi!ГОР GOTO

Управление можно переда вать безусловно с помощью оператора

goto иденти qмкаторl

идентификатор долже н быть метюй, локализованной в данной


функции.

9.9.12. Помеченный оперi!ГОр

Перед л юбым оператором может сто ять помеченный преф1кс вида

идентиф1катор:

юторый служит для описания идентиqмкатора в качестве метки. Метки


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

оператором goto . Областью действ ия метки является данная функция,


за исключением любых подблоюв , в юторых тот же идентиqмкатор
описан снова.

9.9.13. Пустой оперi!ГОр

Пустой оператор имеет qюрму:

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


метку перед закрывающей сюбкой } составного оператора или указать
п устое тело в операторах цикла , таких как whi l e.

9.10. Внешние определения

290
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

с- программа представляет собой последовательность внеlllН.ИХ


определ ений. BHell.lНee определение описывает идентиф1катор как
имеющий КJJacc памяти e x t er n (по умолчан и ю) , ил и возможно
s ta t i с , и специфицированный тип. Спецификатор типа Также может
быть пустым ; в этом сл учае считается, что тип является тип ом i nt .
Область действия внеlllН.ИХ определений распространяется до конца
файла , в котором они приведены , точно так же , как влияние описан ий
простирается до конца блока. Синтаксис внешн и х определений не
отличается от синтаксиса описаний , за И СКJJючением того, что только на
этом уровне можно при водить текст функций.

9.10.1. Внешнее определение функции

Определение функции имеет фJрму

о пр еделение -функции :

с п е циф1 като ры- о пи сан ия оп исател ь-функции тело- функции


н еоб

gцинственными спец и4и каторами КJJacca памяти , доп ускаемыми в


качестве специф1каторов- описания , являются e x tern и ли static ;
о различии между ними смотри раздел в данной лекции. Описател ь
функции подобен описателю для " функции , возвращающей ... ", за
исключением того , что он перечисляет фJрмальные параметры
определ яемой функции.

О писател ь-функции :
о пи сатель (с п исок- п а р а мет р ов
н еоб)
с пи со к п а рамет р о в :

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


и ден т иф1 катор ,с п исок- п а р а мет р о в

тело- функции имеет фJрму

тел о-функции :
с пи со к-о пи саний состав но й -о пе р ато р

'"
Б.8. К <'р ниган, Д. М. Ричи Я з ык "JIOграм""ирования С

Идентиф1каторы из списка параметров и только они могут быть


описаны в списке описаний. Любой идентификатор, тип ЮJторого не
указан, считается имеющим тип in t . Единственным до пустимым здесь
спецИttикатором КJJacca памяти является register ; если таЮJЙ КJJacc
памяти специфицирован, то в начале выполнения функции
соответствующий фактический параметр ЮJпируется, если это
во зм ожно, в perncтp.

Вот простой пример полного определения функции:

int maх(а , Ь , с)
int а, Ь, с ;
(
int Щ
т = (а > Ь) ? а:Ь;
[Nшп((m> с)? т:с);
}

Здесь in t - спецшJJ.1катор- типа, тах (а , Ь , с) - описатель- функции,


i nt а, Ь, с ; - список- описаний ф:Jрмальных параметров , { }
- блок, содержащий текст оператора.

в языке 'С" все фактические параметры типа float преобразуются к


типу double , так что описания формальных параметров, объявленных
как f l oat , приспособлены прочесть параметры типа double .
Аналоrnчно, ПОСЮJл ьку ссылка на массив в любом ЮJнтексте (в
частности в фактичесЮJМ параметре) рас сматр ивается как указатель на
первый эле мент массива, описания формал ьных параметров вила "
массив ... " приспособлены проче сть : " указатель на ... ". и наЮJнец ,
ПОСЮJл ьку структуры, объединения и функции не м огут быть переда ны
функции, бессмысленно описывать ф:Jрмальный параметр как
структуру, объединение или ФУНIO.\ИЮ ( указатели на такие объекты,
ЮJнечно, допускаются).

9.10.2. Внешние определения данных

BHel.LIНee определение данных имеет форму

292
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

определение-данных:

описание

Классом памяти таких дан ных может быть ex ter n (в частности, по


умолчанию) или s ta tic , но не аи to или regis ter .

9.11. Правила, определяющие область действия

Вся с- программа необязательно компилируется одновременно;


ИСХОДНЫЙ текст программы может храниться в нескольких файлах и
ранее скомпилированные процеАУРЫ MOryr загружаться из библиотек.
С вязь между функциями может осyrцeствляться как через явные
обращения, так и в резynьтате манипynирования с внешними да нными.

Поэтому слеАУет рассмотреть два вида областей действия: во-первы х,


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

9.11.1 . Лексическая область действия

Лексическая область действия иде нтификаторов , описанных во


внешних определениях, простирается от определения до конца

исходного файла, в котором он находится. Лексическая область действия


идентификаторов , являющихся qюрмальными параметрами,
распространяется на ту функцию, к которой они относятся. Лексическая
область действ ия идентификаторов, описанных в начале блока,
простирается до конца этого блока. Лекси ческой областью действия
меток является та функция, в которой они на)[)ДЯтся.

Поскол ьку все обращения на один и тот же внеll.lНИЙ идентификатор


обращзются к одному и тому же объекту, компилятор проверяет все

293
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

описания одного и того же внеlШlего идентиф1катора на


совместимость; в действ ительности их область действ ия
распространяется на весь файл, в котором он и находятся .

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


описан в начале блока, включая и блок, который обра зует функцию , то
действ ие любого оп и сания этого идентификатора вне блока
приостанав л ивается до конца этого блока.

Напомним также, что и дентшlJ1каторы, соответствующие обычным


переменным , с одной стороны, и идентификаторы, соответствующие
членам и ярлыкам структур и объединен ий , с друтой стороны,
ф:Jрмируют два непересекающиxr::я класса, которые не вступают в
противоречие. Члены и ярлыки подчиняются тем же самым правилам
определения областей действ ия , как и другие идентификаторы. Имена,
спец и<l:t.щируе мые с помощью t ypede f , входят в тот же класс, что и
обычные и дентшlJ1каторы. Они мосут быть переопределены во
внутренних блоках. но во внутреннем описании тип должен быть указан
явно:

typedef float di<aance;

{
auto int d.istance;

Во втором описании спецификатор типа in t долже н присутствовать,


так как в противном случае это описание бу,цет принято за описание без
описателей с типом distance (прим. автора: согласитесь, что лед
здесь тонок.).

9.11.2. Область действия внешних идентификаторов

Если функция ссылается на и дентшlJ1катор, описанный как ex ter n, то


где-то среди файлов и ли б и бл и отек, образующи х полную программу,
должно содержаться внеlШlее определение этого идентиф1катора. Все
функции данной программы , которые ссылаются на один и тот же
внеlШlИЙ идентификатор, ссылаются на один и тот же объект, так что

'"
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

слеАУет позаботиться, ч тобы специф1цированные в этом определении


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

Появление ключевого слова extern во BHell.lНeM определении


указывает на то, что память для описанных в нем идентификаторов
будет выделена в другом файле. Следовательно, в сосгоящей из многих
файлов программе BHell.lНee определение иде нтификатора , не
содержащее спецшlJ1катора extern, должно появляться ровно в одном
из этих файлов. Любые другие файлы, которые желают дать BHell.lНee
определение этого и денти4икатора, должны включать в это
определение сло во ex ter n. Идентификатор может быть
инициали з ирован только в том описании, которое приводит к

выделен ию памяти.

Идентиф1 каторы, внешнее определе ние которых начинается со сл ова


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

9.12. Строки управления компилятором

Компилятор языка 'С" содержи т препроцессор, который позволяет


осущесгвлять макроподсгановки, усл овную компиляцию и включение

именованных фай л ов. Строки, начинающиеся с #" , общаются с эт им


препроцессором. Синтаксис этих сгрок не связан с осгальным языком;
они могуг появляться В любом месге и их влиян и е распросграняется
(незав и симо от обласги дейсгвия) до конца исходного программного
фай ла.

9.12.1. Замена лексем

Управляющая компилятором сгрока вида

#define идентиф1катор сг рока- лексем

(Обратите внимание на отсугсгвие в конце точки с запятой) приводит К


тому, что препроцес со р заменяет последующие вхождения этого

295
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

идентификатора на указанную строку лексем. Строка вида

#define иде нтиф.iкатор


(и денти ф.i катор, ... , иденти ф.i като р )строка лексем

где меЖАУ первым идентиф.iкатором и открывающейся СЮJБЮJЙ ( нет


пробела, представ ляет собой макро определение с аргументами.
Последующее вхождение первого идентиф.iкатора, за ЮJторым следует
открывающая СЮJбка ' ( " последо вательность разделенных запятыми
лексем и закрываюЩ1Я СЮJбка ' ) " заменяются СТРОЮJЙ лексем из
определения. Каждое вхождение идент ификатора , ynомянугого в списке
ф:Jрмальных параметров в определении, заменяется соответствующей
СТРОЮJЙ лексем из обращения. Фактическими аргументами в обращении
являются строки лексем, разделенные запятыми; однаЮJ запятые,

входящие в закавыченные строки или закл юченные вкрутлые СЮJбки, не


разделяют аргументов. Количество ф:Jрмальных и фзктических
параметров должно совпадать. Текст внутри строки или символьной
ЮJнстанты не подлежит замене.

в обоих случаях замененная строка просматривается снова с целью


обнаружения других определеных иде нтиф.iкаторов. В обоих случаях
слиll.lкDм ДJlинная строка определения может быть продолжена на
друтой строке, если поместить в ЮJнце продолжаемой строки обратную
ЮJсую черту \.

Описываемая возможность особенно полезна ДJlЯ определения


"объявляемых ЮJнстант ", как, например,

#dеfiлe tabsize 100


iлt table[tabsize];

Управляющая строка вида

# Ш1d е f идентиф.iкато р

приводит К отмене препроцессорного определения данного

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

9.13. Неявные описания

296
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

Не всегда является необходимым специфицировать и класс памяти и


тип идентификатора в описании. Во внеll.lНИХ определениях и описания
х ф:Jрмальных параметров и членов структур класс памяти определяется
по контексту. Если в наXDДЯщемся внугри функции описании не указан
тип, а только класс памяти , то предполагается, что идентификатор
имеет тип in t ; если не указан класс памяти, а только тип, то
идентификатор предполагается описанным как auto . Исключение из
последнего правила дается для функций, потому что спецификатор
auto для функций является бессмысленным (язык "С" не в состоянии
компилировать программу в стек ); если иденти4икатор имеет тип "
функция, возвращзющзя ... ", то он предполагается неявно описанным
как extern.

В)[)ДЯщий в выражение и неописанный ранее идентификатор, за


которым следует скобка {, считается описанным по контексту как "
функция, возвращающзя in t ".

9.14. Снова о типах

В этом разделе обобщзются св едения об операциях, которые можно


применять только к объектам определеных типов.

9.14.1. Структуры и объединения

Только две вещи можно сделать со структурой или объединением:


назвать один из их членов (с помощью операции .) и л и извлечь их адрес
( с помощью унарной операции & ). Дрyruе операции, такие как
присваивание им или из них и передача их в качестве параметров ,

привqд,ят к сообl.Lf'НИЮ об Оll.lИбке. В буAYJ.Цf'М ожидается, что эти


операции, но не обязательно какие-либо другие, бу,цуг разре шены.

Выше говорится, что при прямой или косвенной ссылке на структуру (с


помощью . или -> ) имя справа должно быть членом структуры,
названной или указанной выражением слева. Это ограничение не
навязывается строго компилятором, чтобы дать возможность обойти
прави ла типов. В действительности перед' .' допускается любое L-
значение и затем предполагается, что это L- значение имеет ф:Jрму

297
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

структуры, для которой стоящее справа имя является членом. Таким же


образом, от выражения, стоящего перед' -> " требуется только быть
указателем и ли целым. В случае указателя предполагается, что он
указывает на структуру, для которой стоящее справа имя является
членом. В случае целого оно рассматривается как абсолютный адрес
соответствующей структуры, заданный в единицах маlllИННОЙ памяти.

Такие структуры не являются переносимыми.

9.14.2. Функции

Только две вещи можно сделать с функцией: вы з вать ее или извлечь ее


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

iлt Ю;

g(Q;

Тогда определение функции g могло бы выглядеть так:

g(funcp) iлt(*funcр)();
{

(*funcр )();

Обрати те внимание, что в вызывающей процедуре функция f должна


быть описана явно, потому что за ее появлением в g ( f) не следует
скобка (.

9.14.3. Массивы, укаЗi!Гели и индексация

Каждый раз, когда иденти4икатор, имеющий тип массива, появляется в

298
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

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


массива. Из-за этого преобразования массивы не являются L-
значениями. По определению операция индексация []
интерпретируется таким образом, что е 1 [ е2 ] считается идентич ным
выражению * ( (е 1 ) + (е2) ) . Согласно правилам преобразований,
применяемым при операции +, если е 1 - массив, а е2 - целое, то
е 1 [ е2 ] ссылается на е2 -й член массива е1 . Поэтому несмотря на
несимметричный вид операция индексации является коммyrативноЙ.

в случае многомерных массивов применяется последовательное


правило. Если е является n -мерным массивом размера i *j * ... * k,
то при появлении в выражении е преобразуется в указатель на (n -1) -
мерный массив размера j * ... * k. Если операция * либо явно, л ибо
неявно , как резуnътат индексации, применяется к этому указателю, то

результатом операции будет указанный (n -1) -мерный массив,


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

Рассмотрим,например, описание

iлt x[3][5J;

Здесь х массив целых размера 3 * 5. При появлении в выражении х


преобразуется в указатель на первый из трех массивов из 5 целых. В
выражении х [i ], которое эквивалентно * (х + i ) , сначала х
преобразуется в указатель так, как описано выше; затем i преобразуется
к типу Х, что вызывает умножение i на длину объекта, на который
указывает указатель, а именно на 5 целых объектов. Результаты
складываются, и применение косвенной адресации дает массив (из 5
целых) , который в свою очередь преобразуется в указатель на первое из
этих целых. Если в выражение входит и другой индекс, то таже самая
аргументация применяется снова; результатом на этот раз бу,цет целое .

из всего этого следует, что массивы в языке 'С" хранятся построчно


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

299
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

9.14.4. Явные преобразования указателей

Разрешаются определеные преобразования, с испол ьзованием

указателей , но они имеют некоторые зависящие от конкретной


реал изации аспекты. Все эти преобразования задаются с помощью
операции явного преобразования типа; см. выше.

указатель может быть преобразован в л юбой из целочисл енных типов,


достаточно БОЛЫ1Ю Й для его хранения. Требуется ли при э том i n t или
long , зависит от конкретной маlШ1НЫ. Преобразующая функция также
яв л яется маlШ1нно-зависимой, но она бу,цет вполне естественной для
тех, кто знает структуру адресации в маlШ1не. Детали для некоторых
конкретных маlШ1Н приводятся ниже.

Объект целочисленного типа может быть явным образом преобразован


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

указатель на один тип может быть преобразован в указатель на друтой


тип. Если преобразуемый указатель не указ ывает на объекты, которые
подходящим образом выровнены в памяти , то результирующий
указатель может при использовании вызывать ОlШ1бки адресации.
Гарантируется, что указател ь на объект заданного размера может быть
преобразован в указатель на объект MeHblllero размера и снова обратно,
не претерпев при этом изменения.

Например, процеАУРа распредел ения памяти могла бы принимать


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

еже rn char *аlЮ с О ;


double *dp;
dp=(double*) alloc(sizeof(doub le));
*dp=22.017.0;

функция alloc должна обеспечивать (маlШ1ННО-зависимым способом),


что возвращаемое ею значение бу,цет пoдmдящим для преобразования в
указатель на double ; в таком случае использование этой функции
зоо
Б.8. К <'рниган, Д. М. Р ич и Я з ык "JIOграм""ирования С

будет переносимым.

Представ л ение указателя на PDP-ll соответствует 16-битовому целому


и измеряется в баЙтах. Объекты типа cha r не имеют никаких
ограничений на выравнивание; все остальные объекты должны иметь
четные адреса.

На HONEYWELL 6000 указатель соответствует 36-битовому целому;


слову соответствует 18 левых битов и два непосредственно
примыкающих к ним справа бита, которые выделяют символ в слове.
Таким образом, указатели на символы измеряются в единицах 2 в
степени 16 байтов; все остальное измеряется в единицах 2 в степени 18
маJ.LП.1ННЫХ слов. Величины типа double и содержащие их агрегаты
должны выравниваться по четным адресам слов (О по модулю 2 в
степени 19). Эвм lВM 370 и INТERDAТA 8/32 оюдны меЖАУ собой. На
обеих машинах адреса измеряются в байтах; элементарные объекты
должны быть выровнены по границе, равной их длине, так что
указатели на short должны быть кратны двум, на in t и float -
четырем и на double - восьм и . Агрегаты выравниваются по самой
строгой границе, требуемой каким-либо из их элементов.

9.15. Константные выражения

в нескольких местах в языке "С" требуются выражения, которые после


вычисле ния становятся константами: после вариантного префикса
case , в качестве границ массивов и в инициализаторах. В первых двух
случаях выражение может содержать только целые константы,

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


либо бинарными операциями

+ - '" / . % & I «» == != <> <= >=

л ибо унарными операциями

- -
либо тернарной операцией

7:
зm
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

круглые сюбки MOryr использоваться для группировки, но не для


обращен ия к функциям.

в случае инициализаторов допускается большая (у,царение на букву о)


свобода; кроме перечисленных выше константных выражений можно
также применять унарную операцию & к внеlШlИМ или статическим

объектам и к внеlШlИМ и ли статическим массивам, имеющим в качестве


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

массивов и функций. Основное правило заключается в том, что после


вычисления инициализатор должен становится л и бо константой, л и бо
адресом ранее описанного внеlШlего или статического объекта плюс
или минус константа.

9.16. Соображения о переносимоcrи

Неюторые части языка 'С" по своей сути машинно-зависимы.


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

Как показала практика , вопросы, целиком связанные с аппаратным


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

Число переменных типа register , юторое фактически может быть


помещено в реЛ1СТРЫ, меняется от машины к маll.lИне, также как и набор
до пустимых для них типов. Тем не менее все юмпиляторы на своих
маl.LIИНах работают надлежащим образом; ЛИlШlие и ли недо п уст имые
ре гистровые описания игнорируются.

Неюторые трудности возникают только при использовании


сомнительной практики программирования. ПИсать программы,
302
Б.8. К<'рниган, Д. М. Ричи Я ЗЫ К "JIOграм""ирования С

которые зависят от каких-либо этих свойств, является чрез вычайно


неразумным.

Языком не указывается порядок вычисления apryмeHToB функций; они


вычисляются справа налево на PDP-ll и УАХ-ll и слева направо на
остальных машинах. порядок. в котором происходят побочные эwкты,
также не cnециф1цируется.

Так как символьные константы в действительности являются объектами


типа int , до пускается использование символьных констант, состоящих
из нескол ьких символов. Однако, поскольку порядок, в котором
символы приписываются к слову, меняется от машины к машине,

конкретная реализация оказывается весьма машинно-зависимой.

При сваи вани е полей к словам и символов к целым осуществляется


справа налев о на PDP-ll и УАХ-ll и слева направо на других машинах.
Эти разл ичия незаметны для изолированных программ, в которых не
разрешено смешивать типы (преобразуя, например , указатель на i nt в
указатель на char и затем проверяя указываемую память) , но должны
учитываться при согласовании с накладываемыми извне схемами

памяти.

Язык. принятый на различных КDмпиляторах, отличается только


незначительными деталями. Самое заметное отличие состоит в том, что
используем ый в настоящее время КDмпилятор на PDP-ll не
инициализирует структуры, которые содержат поля битов, и не
до пускает HeКDTopыe операции присваивания в определе ных

контекстах. связанных с использованием значения присваивания.

9.17. Анахро НИ3МЫ

Так как язык "С" является раз вивающимся языком, в старых программах
можно встретить HeКDTopыe уста ревшие КDнструкции. Хотя
болы.LIИН СТВО версий КDмпилятора поддерживает такие анахронизмы,
они в КDHцe КDHЦOB исчезнут, оставив за собой только проблемы
переносимости.

в ранних версиях 'С" для пробле м присваивания использовалась фJрма


=on, а не on =, приводя к двусмысленностям, типичным примером

зоз
Б.8. К<'рниган, Д. М. Ричи Я зы к "JIOграм""ирования С

которых является

х= -1

где х фзктически умеНЫllается, поскол ьку операции = и - примыкают


друт К друту, но что вполне могло рассматриваться и как присваивание

-1 к х.

С интаксис инициал изаторов изменился: раньше знак равенства, с


которого начинается инициализатор, отсугствовал, так что вместо

intx = l ;

использовалось

int х 1;

изменение было внесено из- за инициализации

iлt f (1+2)

которая достаточно сильно напоминает определение функции, чтобы


смугить компиляторы.

9.18. Сводка синтаксических правил

Эта сводка синтаксиса языка 'С" предназначена скорее для облегчения


понимания и не яв л яется точной qюрмулировкой языка.

9.18.1. Выражения

Основными выражениями яв ляются следующие:

выражение:

первичное-выражение

* выражение
& выражение
- выражение

! Выр ажен ие
3"'
Б.8. К<'рниган, Д. М. Ричи Язык "JIOграм""ирования С

Л выражение

++ l-значение
-- l-значение
l-значение ++
l-значение --

sizeof выражение
(имя типа) выражение
выражение бинарная-операция выражение
выражение ? Выражение: выражение
l-значение операция-присваивания выражение
выражение , выражение

первичное выражение:

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

строка

(выражение)
первичное-выражение (с писок выражений
необ)
первичное-выражение [выражение]
l-значение . Идентификатор
первичное выражение - > идентификатор
l-значение:
иде нтификатор
первичное-выражение [выражение]
l-значение
. Идентификатор
первичное-выражение -> идентиф1катор
* выражение
(l-значение)

Операции первичных выражений

о [] . - >
имеют самый высокий приоритет и группируются слева направо.
Унарные операции

*& - ! '" ++ -- sizeоf(Имя типа)

имеют более низкий приоритет, чем операци и первичных выражений,


305
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

но более высокий, чем приоритет л юбой бинарной операции. Эти


операции группируются справа налево. Все бинарные операции и
условная операция (прим. перевод.: усл овная операция группируется
справа налево; э то изменение внесено в язык в 1978 г.) ГРУТlПируются
слева направо и их приоритет убывает в слеАУЮщем порядке:

Бинарные операции:

• * / %
• + -
• » «
• < > <= >=
• -- !=

·• -
• &

• &&
• II
• ?:

Все операции присваивания имеют одинаковый приоритет и


ГРУТlПируются справа нал ево. Операции присваивания:

= += -= * =?= %= »= «= & = '" = 1=

Операция запятая имеет самый низкий приоритет и ГРУТlПируется слева


направо.

9.18.2. Описания

Описание:
с п е циqм каторы - о пи сан ия с пи сок-и ници ал и зируемых- о пи сателей
н еоб;

с п е ци qмкатор ы- о пи сан ия :

306
Б.8. К<'рниган, Д. М. Ричи Язык "JIOграм""ирования С

специ4икатор-типа специф1каторы-описания
необ
специ4икатор-класса-памяти с пециф1каторы-опи сан ия
необ
с пециф1катор-класса-памяти:
ашо

static
extem
regi<;ter
typedef
с пециф1катор-типа:
char
soort
int
long
W1Signed
float
double
специф1катор-структуры-или-объединения
оп ределяющее-тип имя -
с пи со к-инициали зи руемы х-опи сателе й:
инициализируемый-описатель
инициализируемый-описатель,
список-инициализируемых-описателей
инициали зируемы й-описатель
описатель-инициализатор

необ
описатель:

идентиф1катор
(опи сател ь)
* описатель
описатель О
описатель (константное выражение
необ ]

с пециф1катор-структуры-или-объединения:
struct список-описателей-структуры
struct иде нтиф1катор {список-описаний-структуры)
struct иде нтиф1катор
307
Б.8. К<'рниган, Д.М. Ричи Язык "JIOграм""ирования С

uniоп {список-описаний-структуры}
uniоп иденти4икатор {список-описаний-структуры}
uniоп иденти4икатор
список-описаний-структуры:
описание-структуры

описание-структуры список-описаний-структуры
описание структуры:

специ4икатор-типа список-описателей-структуры:
список-описателей-структуры
описатель-структуры

описатеЛЬ-СТРУКТУРЫ,список-описателей-структуры
описатель-структуры:

описатель

описатель: константное выражение

:константное-выражение

инициализатор:

= выражение

= {список-инициализатора}
= {список-инициализатора}
список инициализатора:

выражение

список-инициализатора,список-инициализатора

{список-инициализатора}
имя-типа:

специ4икатор-типа абстрактный-описатель
абстрактный-описатель:
пусто

{абстрактный-описатель}
* абстрактный-описатель
абстрактный-описатель О
абстрактный-описатель [константное-выражение
необ]
определяющее-тип-имя:

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

9.18.3. Операторы

составной-оператор:
ЗО8
Б.8. К<'рниган, Д. М. Ричи Язык "JIOграм""ирования С

{список-описаний список-операторов
необ необ}
список-описаний:
описание

описание список-описаний
список-операторов:

оператор

оператор список-операторов

оператор:

составной оператор
выражение;

if (выражение) оператор
if(выражение) оператор ебе оператор
while (выражение) оператор
do оператор while (выражение);
foг(выражение-l ;выражение-2 ;выражение-З )
необ необ необ
оператор

switch (выражение) оператор


case константное-выражение: оператор
defauh: оператор
break;
conUnue;
геtшn;

return выражение;
goto идентификатор;
иде нтификатор: оператор

9.18.4. Внешние определения

Программа:
внешнее-определение

внешнее-определение программа

внешнее-определение:

определение-функции
определение-данных

309
Б.8. К<'рниган, Д.М. Ричи ЯЗЫК "JIOграм""ирования С

определение-функции:
специф1катор-типа описатель-функции тело-функции
необ
описатель-функции:
описатель (список-параметров)
необ
список-параметров:

идетиф1катор
идентиф1катор , список-параметров
тело-функции:
список-описаний-типа оператор-функции
оператор-функции:
{список описаний список-операторов}
необ
определение данных:

ежеrn спецификатор типа список


необ необ
инициализируемых описателей
необ
static спецификатор типа список
необ необ
инициализируемых описателей
необ;

9.18.5. Препроцессор

#define идентификатор строка-лексем


#dеfiлe
#define идентификатор(идентификатор, ... ,идентификатор)стр
# Ш1dеf идентиф1катор
#include ''имя-фзйла''
#include < имя-фзйла >
# и константное- выражен ие
#ifdef идентиф1катор
#ifndef идентиф1катор
#else
#еnd"
#line константа идентификатор
3>0
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

Последние изменения языка "С" (15 ноября 1978 г.)

9.19. Присваивание структуры

С труктуры могуг быть присвоены , переданы функциям в качестве


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

В реализации возвращения структур функциями на PDP-ll имеется


коварный деф:>кт: если во время возврата проис.ю.цит прерывание и та
же самая функция реентерабельно вызывается во время этого
прерывания, то з начение во з вращаемое из первого вызова , может быть
испорчено. Эта трудность может возникнуть только при наличии
истинного прерывания, как из операционной с истемы, так и из
программы пол ьзователя, прерывания , которое существенно для

использования сигнал ов; обычные рекурсивные вызовы совершенно


безопасны.

9.20. Тип перечисления

Введен новый тип даННЫХ,анал оrnчный скалярным типам языка


паскаль. К спецификатору- типа в е го синтаксическом описании в
начале л екции сл едует добавить

с п е цифи като р-п еречисле ни я

с с и нтакс и сом

п е ци фикато р-п е р еч и слен ия:

enum с пи с о к- п еречислени я

enum идентификатор с пи сок-пе р ечислени я

enum идентификатор
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

с пи со к- пе ре чи сле ния :

п е ре ч исл яемо е

с пи со к- пе ре чи сле НИЯ, п е р е чи сля ем о е

п е ре ч исляемое:

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

идентиф1като р = ЮJ н ста нтно е в ы р ажение

Роль идентиф1катора в специф1каторе-перечисления пол ностью


аналогична роли ярлыка структуры в cnециф1каторе- структуры ;
идентификатор обозначает определ енное перечисление. Например,
описание

enum color {red, white, black, blue };

enum color *с р , co~

Объявляет идентиф1катор color ЯРЛЫЮJм перечисления типа,


описывающего различные цвета и затем объявляет ер указател ем на
объект этого типа, а col - объектом этого типа.

и,центиф1каторы в cnиске-перечисления описываются как ЮJнстанты и


мосут появиться там , где требуются (по ЮJнтексту) константы. Если не
используется вторая форма перечисляемого (с равеством = ), то
величины ЮJнстант начинаются с О и возрастают на 1в соответствии с
прочтением их описания слева на право. Перечисл яемое с присвоением
= придает соответствующему идентиф1катору указанную вел ичин у;
посл едующие иденти4икаторы продолжают прогрессию от
приписанной величины.

Все ярл ыки перечисления и ЮJнстанты могут быть различными и


непо)[)жими на ярлыки и члены структур даже при условии

использования OAHoro и того же множества идентификаторов.

Объекты данного типа перечисления расс матриваются как объекты ,


ш
Б.8. К<'рниган, Д. М . Ричи Я зы к "JIOграм""ирования С

имеющие тип, отличный от любых типов и контролирующая программа


lint сообщает об ошибках несоответствия типов. В реал изации на
РDР-ll со всеми перечисляемыми переменными оперируют так. как
если бы они имели тип INт.

9.21. Таблица изображений непечатных символов языка


rrc rr •

в данной таблице приведены изображения некоторых символов (


4иrypные скобки и т.д. ) яз ыка 'С", которых может не оказаться в
знаковом наборе дисплея или печатающего устройства.

Таблица 9.4.
Значение Изображение" в тексте

Фиrypная открывающаяся скобка {


Фиrypная закрывающаяся скобка

Вертикальная черта

Апостороф \ '
Волнистая черта

Изображения приведены для операционной системы UNIX. При работе


компилятора 'С" под управлением любой друтой операционной
системы , необходимо воспол ьзоваться соответствующим рутводством
дл я данной системы.
Б.В. К."ниган. Д. М . Р"чн ЯзЬ/к nf'O.!p"-II_IIUJювtlm", С

Содержание

Титульная страница 2
Выход ные данные 3
Лекция о. Аннотация и введ ение 4
Лекция 1. Учебное введение 12
Лекция 2. Типы , операции и выражения 52
Лекция з. Управление потоком 78
Лекция 4. Функции и структура программ 96
Лекция 5. Указатели и массивы 129
Лекция 6. Структуры 169
Лекция 7. Ввод и вывод 201
Лекция 8. Интерфейс системы ИNIХ 222
Лекция 9. Справочное руководство по я з ыку С 247

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