Язык программирования C (ANSI C)
1988, Вильямс 2010, 2-е издание, выдержки Дятлова Н. С. от 05.05.2022x2, 1–47/304=85%
1. С момент выхода книги «Язык программирования C» в 1978 году в мире вычислительных технологий произошли революционные изменения. За это время язык С также изменился, хотя и в небольшой степени. А главное — он вышел далеко за пределы своего первоначального узкого применения в виде языка для операционной системы Unix.
2. В 1983 году Американский национальный институт стандартов (ANSI) создал комитет, целью которого была разработка «четко сформулированного и системно-независимого определения языка С» при сохранении самого духа и стиля этого языка. Результатом стало появление стандарта ANSI языка C. В нем определяется состав стандартной библиотеки функций, содержащей обширный набор средств для ввода-вывода, управления памятью, операций со строками и т.п. Стандарт дает точное определение тех средств, для которых в первоначальном описании языка такого определения дано не было, и в то же время указывает, какие аспекты языка остались системно-зависимыми.
3. Наиболее заметное изменение — это новая форма объявления и определения функций. Современные компиляторы уже поддерживают большую часть нового стандарта.
4. Язык С невелик по объему, и нет большого смысла писать о нем толстые книги. Мы улучшили изложение основных вопросов — таких как указатели, являющиеся центральным моментом в программировании на С.
5. Справочник задумывался как легкодоступное пособие для программистов, но отнюдь не как строгое определение языка для разработчиков и компиляторов — эта роль по праву принадлежит собственно стандарту.
6. Язык С становится все удобнее по мере, как приобретается опыт работы с ним.
7. С представляет собой язык программирования общего назначения, характеризующийся краткостью выражений, современными управляющими конструкциями и структурами данных, а также богатым набором операций. С — это язык не слишком высокого уровня, не слишком объемный и не приспособленный специально к какой-либо конкретно области приложений. Но зато отсутствие в нем каких-либо ограничений и большая общность делают его более удобным и эффективным для решения многих задач, чем языки, даже считающиеся по разным причинам более мощными.
8. Первоначально язык С был разработан для операционной системы Unix на ЭВМ DEC PDP-11 и реализован в этой системе Денисом Ритчи.
9. Язык С не привязан к какой-либо конкретной аппаратной или системной платформе, и на нем легко писать программы, которые будут выполняться безо всяких изменений в любой системе, поддерживающей этот язык.
10. Изложение большей частью построено на чтении, написании и исправлении примеров программ, а не на простом описании правил и конструкций. В основном рассматриваются примеры законченных и реально работающих программ, а не отдельные искусственные фрагменты. Кроме демонстрации эффективного применения языка, мы постарались по возможности дать понятие о ряде полезных алгоритмов и принципов хорошего стиля и разумного написания программ.
11. Предполагается, что читатель знаком с основными понятиями программирования — такими как переменные, операторы присваивания, циклы и функции.
12. Вдумчивая критика и предложения многих наших друзей и коллег очень помогли нам в написании этой книги и сделали процесс работы на ней очень приятным.
13. Вы, читатель этой книги, и есть главный ее критик и комментатор. Мы ценим ваше мнение и хотим знать, что было сделано нами правильно, что можно было сделать лучше и что еще вы хотели бы увидеть изданным нами.
14. С является языком программирования общего назначения. Язык не привязан жестко к одной определенной системе или аппаратной платформе. И хотя С называют «языком системного программирования», поскольку на нем удобно писать компиляторы и операционные системы, он столь же удобен и для написания больших прикладных программ в самых разных областях применения.
15. Многие ключевые идеи С пришли из языка BCPL. BCPL оказал влияние на С опосредованно — через язык B.
16. В языках BCPL и B отсутствует типизация данных. В отличие от них язык С предлагает широкий ассортимент типов. Фундаментальные типы включают в себя символьный, а также целый и вещественный (с плавающей точкой) нескольких различных размеров. Кроме того, существует целая иерархия производных типов данных, создаваемых с помощью указателей, массивов, структур и объединений. Из операндов и знаков операций формируются выражения; любое выражение, в том числе присваивание и вызов функции, может являться оператором. Благодаря указателям поддерживается механизм системно-независимой адресной арифметики.
17. В языке С имеются все основные управляющие конструкции, необходимые для написания хорошо структурированной программы: группировка операторов в блоки, принятие решения по условию (if-else), выбор одного из нескольких возможных вариантов (switch), циклы с проверкой условия завершения в начале (while, for) и в конце (do), а также принудительный выход из цикла (break).
18. Локальные переменные функции обычно являются «автоматическими», т.е. создаются при каждом ее вызове. Определения (тела) функций нельзя вкладывать друг в друга, однако переменные можно объявлять в блочно-структурированном стиле. Функции программы на С могут находиться в отдельных файлах исходного кода, компилируемых также отдельно. Переменные могут быть внутренними по отношению к функции, внешними и при этом видимыми только в одном файле кода или же видимыми во всем пространстве программы.
19. На этапе препроцессорной обработки в тексте программы выполняются макроподстановки, включение дополнительных файлов исходного кода и условная компиляция.
20. С — это язык сравнительно «низкого» уровня. Ничего уничижительного в этом определении нет; это всего лишь значит, что язык С работает с теми же объектами, что и большинство компьютерных систем, а именно с символами, числами и адресами. Эти данные можно комбинировать разными способами с помощью арифметических и логических операций, которые реализованы реальными аппаратными и системными средствами.
21. В языке С нет операций для прямого манипулирования составными объектами, например строками символов, множествами, списками или массивами. Язык не содержит специальных средств распределения памяти — только статическое определение и стек, в котором хранятся локальные переменные функций; не предусмотрена ни системная куча (heap), ни сборка мусора (garbage collection). Наконец, в самом языке нет и средств ввода-вывода наподобие операторов READ или WRITE, а также отсутствуют встроенные механизмы обращения к файлам. Все эти операции высокого системного уровня выполняются путем явного вызова функций. В большинстве реализация языка С имеется более или менее стандартный набор таких функций.
22. Аналогично, в С имеются только несложные, однопоточные управляющие структуры: проверки условий, циклы, блоки и подпрограммы, однако отсутствует мультипрограммирование, распараллеливание операций, синхронизация и сопрограммы.
23. Поскольку С невелик, его описание занимает немного места, а изучение отнимает немного времени.
24. Одной из целей стандартизации С было обеспечить совместимость с большинством уже написанных программ или по крайней мере добиться от компиляторов корректных предупреждений, если там будут встречаться новые средства.
25. Операции с плавающей точкой теперь можно выполнять с одинарной точностью.
26. Программы, которые пользуются стандартной библиотекой для взаимодействия с операционной системой, являются гарантированно совместимыми с разными системными средами. Функции из стандартной библиотеки всегда вызываются только явным образом, так что их применения легко избежать, если это необходимо. Большинство функций написаны на С и полностью переносимы, за исключением некоторых подробностей работы операционных систем, которые скрыты на более низком уровне.
27. Хотя язык С соответствует по своей структуре аппаратным возможностям многих компьютеров, он не привязан к какой-либо конкретной машинной архитектуре. При некотором усердии на нем легко написать полностью переносимую программу, т.е. программу, которая будет работать безо всяких изменений на самых разных компьютерах. В стандарте вопросы переносимости четко определены и выведены на явный уровень. Стандартом предписывается использование набора констант, характеризующего архитектуру системы, в которой работает программа.
28. Языку С не свойственен строгий контроль типов, хотя по мере его эволюции развивались средства проверки соответствия типов.
29. В С сохраняется традиционное отношение к программисту, как к человеку, который сам должен знать и решать, что ему делать; от него просто требуется выражать свои намерения в явной и четкой форме.
30. Как и любых языков, у С есть свои недостатки: некоторые операции имеют нелогичный приоритет; некоторые синтаксические конструкции можно было бы организовать и получше. И тем не менее язык С доказал свою высочайшую эффективность и выразительность для самого широкого круга приложений.
31. Мы убеждены, что лучший способ изучить новый язык — это сразу начать писать программы на нем.
32. Глава 4 посвящена функциям и структуре программы: внешним переменным, области видимости, распределению кода по исходным файлам и т.п.
33. Глава 7 содержит описание стандартной библиотеки функций, представляющей собой стандартный интерфейс для взаимодействия с операционной системой. Эта библиотека определена стандартом ANSI и должна поддерживаться во всех системах, в которых реализован язык С, так что программы, которые пользуются ее функциями для ввода, вывода и других операций на уровне операционной системы, должны обладать свойством переносимости на другие платформы безо всяких изменений.
34. В главе 8 описывается взаимодействие программ на С и операционной системы Unix. Среди прочего это взгляд изнутри на реализацию одной из версий стандартной библиотеки, а также рекомендации по переносимости программ.
35. Начнем изложение с быстрого введения в язык С. Нашей целью будет продемонстрировать основные элементы языка в действии — в реально работающих программах — и при этом не утонуть в подробностях, правилах и исключениях. В таком подходе есть и свои недостатки. Самый значительный из них — это то, что ни одно конкретное средство языка не описано здесь во всей полноте. Поскольку в примерах не используются все возможности С, они написаны не столь кратко и изящно, как это можно было бы сделать. В следующих главах вынужденно повторяется часть материала этой главы. Надеемся, что это повторение окажется скорее полезным, чем надоедливым.
36. Программа на С независимо от ее размера состоит из функций и переменных. Функция содержит операторы — команды для выполнения определенных вычислительных операций, а в переменных хранятся числа и другие данные, используемые в этих операциях. Функции С аналогичны подпрограммам и функциям языка Fortran или процедурам и функциям языка Pascal. В любой программе на С обязательно должна присутствовать функция main.
37. Последовательность символов в двойных кавычках, такая как “hello, world\n”, называется символьной строкой или строковой константой. Для обозначения всех символов, которые трудно или невозможно изобразить на письме, используются управляющие последовательности (escape sequences), аналогичные \n.
38. В языке С все переменные необходимо объявить до их использования. Свойства переменных описываются в объявлении, которое состоит из наименования типа и списка переменных, например: int fahr, selsius;
39. Диапазон значений как типа int, так и типа float зависит от аппаратной и системной платформы.
40.
Тело цикла while может состоять из одного или нескольких операторов,
заключенных в фигурные скобки, или из одного оператора без скобок: while (i< j)
i = 2 * i;
41. В любом случае оператор или операторы, выполняемые в цикле while, выделяются отступом в виде табуляции, так что на глаз сразу видно, что именно входит в цикл. Отступы подчеркивают логическую структуру программы. Расстановка отступов и пробелов очень важна для того, чтобы сделать программу удобочитаемой. Рекомендуется писать по одному оператору в строке, окружать их пробелами и выделять пустыми строками для подчеркивания группировки. Расположение фигурных скобок не так важно, хотя многие программисты лелеют привязанность к той или иной их расстановке. Мы выбрали один из нескольких популярных стилей. Рекомендуется придерживаться в этом вопросе единой системы: выберите один стиль раз и навсегда.
42. В языке С, как и во многих других языках, при делении целых числе усекается (отбрасывается) дробная часть результата.
43. Кстати говоря, функция printf не являются частью определения языка С. В самом языке не определены стандартные конструкции ввода-вывода, так что printf — это просто полезная функция из стандартной библиотеки, к которой обычно имеют возможность обращаться программы на С. Однако поведение и свойства функции printf регламентировано в стандарте ANSI C, поэтому она должна одинаково работать во всех библиотеках и компиляторах, соответствующих этому стандарту.
44. Если знак математической операции соединяет два целочисленных операнда, то выполняется целочисленная операция. Однако если один из операндов вещественный, а другой — целый, то перед выполнением операции целое число будет преобразовано в вещественное. Для удобства чтения программы всегда лучше записать вещественную константу с десятичной точкой, чтобы подчеркнуть ее вещественную природу, даже если она имеет целое значение без дробной части.
45. Для каждой конкретной задачи можно написать множество разных версий программы, которая эту задачу решает.
46. В любом контексте, где можно использовать переменную определенного типа, можно также употребить и более сложное выражение того же типа.
47. Оператор for — это оператор цикла, обобщение оператора while.
48. Наводнять программу явными числовыми константами типа 300 или 20 — это дурной тон, поскольку они практически ничего не говорят постороннему человеку, взявшемуся читать такую программу. К тому же эти константы трудно выискивать и изменять по всему тексту единообразно в случае необходимости. Один из подходов к решению проблемы числовых констант состоит в том, чтобы давать им значащие имена. Строка #define определяет символическое имя или символическую константу в виде строки символов. Имена символических констант обычно записывают прописными буквами, чтобы легко отличать их от имен переменных в нижнем регистре. Использование символической константы гарантирует, что никакие компоненты программы не окажутся зависимыми от ее конкретного значения.
49. В языке С любое присваивание является выражением и имеет значение: c = getchar() — значением этого выражения является значение переменной слева от знака равенства после выполнения присваивания. Отсюда следует, что выражение с присваиванием может фигурировать в составе других выражений.
50. Программа записывается более компактно, и ее становится удобнее читать (при условии, что читатель знаком с данной устойчивой конструкцией). Такой стиль будет встречаться часто. Конечно, им можно чрезмерно увлечься и написать совершенно неудобочитаемый код, но мы постараемся избежать этой тенденции.
51. Здесь использована новая операция ++, представляющая собой инкремент, или увеличение на единицу. Вместо нее можно было бы записать nc=nc+1, но ++nc пишется короче и, как правило, работает быстрее. Пока что будем придерживаться префиксной формы записи.
52. Переменные типа long имеют длину не менее 32 бит.
53. Это требование удовлетворяется наличием пустого оператора — точки с запятой.
54. Программы должны действовать корректно, встретив поток ввода нулевой длины. Операторы while и end помогают безошибочно обрабатывать разного рода предельные случаи.
55. И снова подчиненность или вложенность операторов в программе подчеркнута с помощью отступов.
56. Символ, записанный в одинарных кавычках, представляет числовое значение, равное коду символа в символьном наборе системы. Такой символ называется символьной константой, хотя на самом деле это еще один способ записи небольшого целого числа.
57. Четвертая программа из нашей серии полезных утилит для обработки текстовых потоков подсчитывает строки, слова и символы.
58. В маленьких программах наподобие приведенной это не так важно, однако в больших улучшение удобочитаемости, достигаемое за счет применения символических констант, более чем окупает те скромные усилия, которые нужно приложить, чтобы с самого начала писать в этом стиле. Вы скоро убедитесь сами, что вносить глобальные изменения в программы гораздо легче, если числовые константы фигурируют в них только под символическими именами.
59. Выражения, в которых аргументы соединяются знаками && и ||, вычисляются слева направо, причем вычисление всегда прекращается в том месте, в котором гарантированно достигается значение ПРАВДА или ЛОЖЬ.
60. Каждый из этих операторов может быть и блоком операторов в фигурных скобках.
61. Как бы вы протестировали программу подсчета слов? Какого рода входной поток скорее всего выявит ошибки в программе (если таковые есть)?
62. Задача довольно искусственная, но позволяет проиллюстрировать несколько аспектов языка С в одной программе.
63. В языке С индексы массива всегда начинаются с нуля.
64. По определению величины типа char являются всего лишь короткими целыми числами, так что в арифметических выражениях переменные и константы этого типа ведут себя так же, как int.
65. Что касается стиля, то лучше форматировать эту конструкцию так, как было показано. Если каждый if сдвигать относительно предыдущего else вправо, то длинная цепочка проверок условий «уедет» за правый край страницы.
66. Функция в С — это эквивалент понятий подпрограммы и функции в языке Fortran или процедуры и функции в языке Pascal. Функции — это удобный способ свести в одно место (инкапсулировать) некоторые вычислительные операции, а затем обращаться к ним много раз, не беспокоясь об особенностях их реализации. Если функция написана правильно, нет нужды знать, как она работает; достаточно знать, что именно она делает. Часто можно увидеть, как какая-нибудь короткая функция определяется для того, чтобы ее вызвали один раз, — только потому, что это проясняет смысл и структуру кода.
67. Если программа состоит из нескольких файлов исходного кода, необходимо выполнить больше операций по ее компилированию и загрузке, чем в случае одного файла. Но это уже особенности работы с операционной системой, а не атрибуты языка.
68. Будем называть параметром переменную из заключенного в скобки списка в заголовке функции, а аргументом — значение, подставляемое при вызове функции. Иногда в том же смысле используют термины формальный и фактический аргумент.
69. Если записать оператор return без выражения после него, то он передаст в вызывающую функцию управление, но не значение, точно так же, как это делается при достижении конца функции — закрывающей фигурной скобки.
70. Можно заметить, что оператор return стоит и в конце функции main. Поскольку main — такая же функция, как и другие, она может возвращать значение в ту точку, откуда ее вызывают. А это фактически та операционная среда, в которой выполняется программа. Обычно возвращение нуля обозначает нормальное завершение программы, а ненулевого значения — нестандартные или ошибочные условия завершения. В целях упрощения мы не ставили return в конце наших функций main до этого момента, но теперь начнем это делать, чтобы подчеркнуть, что программам полезно посылать отчет о корректности своего завершения в операционную среду.
71. int power(int m, int n); — сообщает, что power является функцией с двумя аргументами типа int, возвращающей также значение типа int. Эта декларация называется прототипом функции. Если определение функции отличается от ее прототипа или формы вызова, возникает ошибка. Не обязательно даже указывать имена параметров в прототипе функции. Однако хорошо выбранные имена являются своеобразной документацией к программе, так что они будут часто использоваться.
72. Новый синтаксис прототипов функций позволяет компилятору очень легко выявлять ошибки в количестве аргументов или их типах.
73. В языке С все аргументы функций передаются «по значению». Это означает, что вызываемая функция получает значения своих аргументов в виде временных переменных, а не оригиналов. Отсюда следуют несколько другие свойства функций, чем в языках, где происходит «передача по ссылке», — как в Fortran или в блоке var в языке Pascal, где вызывемая процедура имеет доступ к исходным аргументам, а не локальным копиям. Основное различие между этими подходами заключается в том, что в С вызываемая функция не может модифицировать переменные в вызывающей функции; она вправе изменять только свои локальные, временные копии этих переменных. Передача по значению — это не ограничение, а благо. Обычно благодаря ей программы пишутся компактнее и с меньшим количеством лишних переменных, поскольку в вызываемой функции параметры можно воспринимать как удобно инициализированные локальные переменные. По необходимости можно сделать так, чтобы функция модифицировала переменные в вызывающем модуле программы. Для этого вызывающая функция должна передать адрес переменной (в понятиях С — указатель на переменную), а вызываемая — объявить параметр указателем и обращаться к переменной косвенно, по ссылке через указатель.
74. Если имя массива используется в качестве аргумента, то передаваемое в функцию значение как раз и является местонахождением (адресом) начала массива — копирования элементов массива не происходит. Применяя к переданному аргументу индекс, функция может обращаться к любому элементу массива и модифицировать его.
75. Наиболее распространенный тип массивов в С — это массив символов.
76. Раз уж имеется такое четкое разделение обязанностей, программу надо написать так, чтобы это было отражено. Соответственно, в начале напишем отдельную функцию getline для считывания следующей строки входного потока. Попробуем написать ее так, чтобы она оказалась полезной и для других приложений.
77. В любой текстовой строке есть как минимум один символ; даже строка, содержащая только символ конца строки, имеет длину 1.
78. Функция getline помещает в конец созданного ею массива символ ‘\0’ (нулевой символ, числовой код которого равен нулю), чтобы обозначить конец строки символов. Этот подход в языке С принят за стандарт. При всем этом подразумевается, что символ ‘\0’ не может встречаться в нормальном тексте.
79. Каждая локальная переменная в функции начинает свое существование при вызове функции и прекращает его при выходе из неё. Вот почему такие переменные обычно называют автоматическими, как и в других языках. Если их не инициализировать, они будут содержать случайный «мусор».
80. В определенных обстоятельства объявление extern можно опустить. Если определение внешней переменной находится в файле исходного кода перед обращением к ней в какой-нибудь функции, то в этой функции объявление со словом extern не обязательно. Обычно все extern-объявления переменных и функций собирают во внешний файл, по традиции именуемый заголовочным (header), И подключают его в строке #include в начале каждого файла исходного кода. Для имен заголовочных файлов традиционно используется расширение .h.
81. Поскольку специальные версии функций getline и copy не имеют аргументов, по логике их прототипы в начале файла должны выглядеть как getline() и copy(). Однако для совместимости со старыми программами на С в стандарте предполагается, что пустой список аргументов — это объявление в старом стиле и что компилятор отключает все проверки списка аргументов. Поэтому для описания действительно пустого списка аргументов следует использовать слово void.
82. Следует отметить, что слова объявление и определение в этом разделе употребляются с особой тщательностью, когда речь идет о внешних переменных. «Определение» — это место, в котором переменная фактически создается с выделением памяти для нее. «Объявление» — это место, где указывается тип и характеристики переменной, но никакого выделения памяти не происходит.
83. Кстати, существует тенденция делать внешними (extern) [глобальными — Н.Д.] переменными все, что попадается на глаза, потому что это якобы упрощает обмен данными: списки аргументов становятся короче, а переменные всегда под рукой в нужный момент. Но, к сожалению, внешние переменные всегда «крутятся» под рукой и в ненужный момент. Слишком интенсивное использование внешних переменных чревато опасностями, поскольку в таких программах обмен данными далеко не очевиден — переменные могут модифицироваться неожиданно и даже непреднамеренно, а доработка кода сильно усложняется.
84. Фундаментальные объекты данных, с которыми работает программа, — это переменные и константы. Используемые в программе переменные перечисляются в объявлениях или декларациях, в которых указывается их тип, а также иногда их начальные значения. Манипуляции и преобразования над данными выполняются с помощью знаков операций. Переменные и константы объединяются в выражения, чтобы таким образом порождать новые значения. Тип объекта всегда определяет, какой набор значений может иметь этот объект и какие операции могут над ним выполняться. Все эти «кирпичики» программы и рассматриваются в этой главе.
85. Знак подчеркивания (_) считается буквой и часто оказывается полезным для улучшения удобочитаемости длинных имен. Однако не следует начинать с него имена переменных, потому что такие имена часто используются библиотечными функциями. Традиционно в С принято записывать имена переменных строчными буквами, а имена символических констант — прописными.
86. Ключевые слова наподобие if, else, int, float и т.п. зарезервированы — их нельзя использовать в качестве имен переменных.
87. Разумно выбирать имена переменных так, чтобы они описывали назначение самих переменных и чтобы одно не могло легко превратиться в другое из-за опечатки. Существует тенденция использовать совсем короткие имена для локальных переменных, счетчиков, циклов и т.п. и имена подлиннее для глобальных переменных.
88. Компилятору разрешено самостоятельно выбирать размер в соответствии с характеристиками аппаратуры и следующими ограничениями: числа типа short и int должны иметь длину не менее 16 бит, long — не менее 32 бит; тип short Должен быть не длиннее int, а int — не длиннее long. Стандартные заголовочные файлы <limits.h> и <float.h> содержат символические константы для всех этих размеров, а также других свойств системы и компилятора.
89. Целочисленная константа наподобие 1234 имеет тип int. Длинную целую константу записывают со строчной или с прописной буквой l (L) на конце, например 123456789L; если целое число слишком велико, чтобы поместиться в переменную типа int, оно также будет восприниматься как long. Константы без знака записываются с u или U на конце, а суффикс ul или UL обозначает длинную целую константу без знака. Вещественные константы содержат десятичную точку (123.4), степенную часть (1e-2) или и то и другое. Они имеют тип double, если с помощью суффикса не указано другое. Суффикс f или F обозначает константу типа float, а l или L — константу типа long double.
90. Строковая константа, строковой литерал, или просто литерал — это последовательность из нескольких (в частном случае ни одного) символа, заключенных в двойные кавычки. Функция strlen и другие функции для работы со строками объявлены в стандартном заголовочном файле <string.h>. Будьте внимательны и не путайте символьные константы со строками, содержащими один символ: ‘x’ — это не то же самое, что “x”. Первая величина — это целое число, используемое для представления кода буквы x в символьном наборе системы, тогда как вторая — массив символов, содержащий один символ (букву x) и завершающий нуль ‘\0’.
91. Если переменная не автоматическая, то ее инициализация выполняется один раз — до начала выполнения программы. Инициализирующее выражение в этом случае должно быть константным. Автоматическая переменная, инициализируемая явным образом в объявлении, инициализируется всякий раз, когда управление передается в функцию или блок, где она объявлена. Ее инициализатором может быть любое выражение. Внешние и статические переменные по умолчанию инициализируются нулями. Автоматические переменные, для которых инициализирующие выражения не указаны явно, содержат случайные значения («мусор»).
92. Декларация const может употребляться с аргументами-массивами, указывая тем самым, что функция не изменяет передаваемый ей массив: int strlen(const char[]);
93. Направление округления при операции / или знак результата при операции % для отрицательных аргументов зависят от системы, равно как и действия, предпринимаемые при переполнении или возникновении машинного нуля.
94. Более интересны логические операции && и ||. Выражения, связанные этими знаками операций, вычисляются слева направо, и вычисление прекращается, как только установлена гарантированная истинность или ложность результата. Большинство программ на С использует это свойство. Приоритет операции && выше, чем ||, но обе имеют приоритет ниже, чем операции сравнения и отношения.
95. Одноместная операция отрицания (!) превращает ненулевой операнд в 0, а нулевой — в 1. Обычно она используется для того, чтобы записывать такие конструкции: if (!valid). Без нее этот оператор выглядел бы так: if (valid == 0). Трудно сказать, какая из двух форм лучше. Конструкции типа !valid удобно читать («если не valid», т.е. «если не годится»), но более сложные могут оказаться малопонятными.
96. Если операнды некоторой операции имеют различные типы, они преобразуются к одному типу с использованием нескольких правил. В целом единственные корректные автоматические преобразования — это те, в которых более «узкий» тип преобразуется в более «широкий» без потери информации, например при преобразовании целого числа в вещественное в выражениях наподобие f + i. Выражения, которые вообще не имеют смысла, например использование числа типа float в качестве индекса массива, запрещены. Выражения, в которых может произойти частичная потеря информации, например помещение числа длинного типа в переменную более короткого типа или вещественного числа в целую переменную, могут привести к выдаче предупреждения, но не запрещены.
97. Стандартный заголовочный файл <ctype.h> содержит объявления семейства функций, выполняющих независимые от символьного набора проверки и преобразования. Например, функция tolower(c) возвращает аналог символа c в нижнем регистре., если с — буква верхнего регистра, то системно-независимый аналог приведенный выше функции lower. Аналогично можно заменить следующую непереносимую между символьными наборами проверку: c >= ‘0’ && c <= ‘9’. Переносимый код будет выглядеть так: isdigit(c).
98. Стандарт языка не определяет, должны ли переменные типа char быть знаковыми или беззнаковыми. Может ли при преобразовании char в int получиться отрицательное число? Ответ зависит от конкретной системы, отражая различия в аппаратной и программной архитектуре. В некоторых системах символ char, крайний левый бит которого равен 1, преобразуется в отрицательное число (это называется «расширением знака»). В других тип char преобразуется в int путем добавления нулей на левом крою числа, и поэтому результат получается всегда положительным. Если необходимо поместить данные несимвольного характера в переменные типа char, то ля лучшей переносимости задайте модификатор signed или unsigned.
99. Заметьте, что числа типа float в выражениях не преобразуются в double автоматически; это — изменение оп сравнению с исходным определением языка. В основном все математические функции, объявленные в заголовочном файле <math.h>, используют двойную точность. Основная причина, по которой иногда используется тип float, заключается в том, чтобы сэкономить память в больших массивах, или, реже, сэкономить время в системах, имеющих особенно затратную вещественную арифметику с двойной точностью.
100. Правила преобразования становятся сложнее, если в выражениях участвуют аргументы типа unsigned. Проблема состоит в том, что сравнение между величинами со знаком и без знака является системно-зависимым, потому что зависит от размеров целочисленных переменных.
101. Преобразования происходят также в ходе присваивания; значение с справой стороны преобразуется в тип переменой, стоящей слева, и результат будет иметь именно этот тип. Длинные целые числа преобразуются в более короткие или величины типа char путем отбрасывания лишних старших битов. При преобразовании float в int отбрасывается дробная часть. При преобразовании double в float значение числа либо усекается, либо округляется — это зависит от конкретной реализации языка.
102. Обратите внимание, что приведение типа порождает новое значение нужного типа, никак не изменяя исходную переменную. Операция приведения имеет такой же высокий приоритет, как и другие одноместные операции.
103. Стандартная библиотека содержит переносимую между системами версию генератора псевдослучайных чисел, а также функцию для инициализации этого генератора.
104. В выражениях ++n переменная инкрементируется до того, как используется, а в выражении n++ — после того.
105. Поразрядная операция И (&) часто используется для обнуления некоторого набора битов. Поразрядная операция ИЛИ (|), напротив, используется для того, чтобы сделать отдельные биты единицами.
106. Необходимо отличать поразрядные операции & и | от логических операций && и ||, которые предполагают вычисление результата типа «истина-ложь» слева направо. Например, если x равно 1, а y равно 2, то x & y равно нулю, тогда как x && y равно единице.
107. Сдвиг вправо числа со знаком в некоторых системах приводит к заполнению этих битов значением знакового бита («арифметический сдвиг»), а в других — нулями («логический сдвиг»). Объявление аргумента x с модификатором unsigned гарантирует, что при сдвиге вправо освободившиеся биты будут заполнены нулями независимо от системы, в которой выполняется программа.
108. Одноместная операция ~ дает поразрядное дополнение целого числа до единицы, т.е. преобразует каждый единичный бит в нулевой и наоборот.
109. Большинство двуместных операций (т.е. операций наподобие + и -, у которых два аргумента — левый и правый)…
110. Помимо такого второстепенного преимущества, как краткость записи, операции с присваиванием имеют то преимущество, что они соответствуют способу человеческого мышления. Когда мы говорим «прибавить 2 к i», то имеем в виду «увеличить i на 2», а не «извлечь i, прибавить 2 и поместить результат обратно в i». Поэтому выражение i += 2 предпочтительнее, чем i = i + 2. Операции с присваиванием даже могут помочь компилятору генерировать более оптимальный код.
111. Эту и аналогичные конструкции можно записать другим способом, прибегнув к условному выражении с трехместной операцией: (n > 0) ? f : n — первое выражение этой условной конструкции не обязательно заключать в скобки, поскольку приоритет операции ?: очень низкий — чуть выше чем у присваивания. Однако скобки желательны, поскольку они подчеркивают условную часть выражения.
112. Условные выражения часто позволяют сделать код очень кратким — код может показаться запутанным, но он намного компактнее, чем соответствующая конструкция if-else.
113. Обратите внимание, что приоритет поразрядных операций &, ^ и | меньше, чем у операций сравнения == и !=. Поэтому выражения, в которых анализируется отдельные биты, необходимо брать в скобки, чтобы получить корректные результаты.
114. Как и в большинстве языков, в С в основном не указывается порядок вычисления операндов в операциях. (Исключениями являются &&, ||, ?: и ‘,’). Например, в следующем операторе первой может вызваться как функция f(), так и функция g(): x = f() + g(); Аналогичным образом, не определяется и порядок, в котором вычисляются аргументы функции при ее вызове. Поэтому следующий оператор может дать различные результаты при трансляции разными компиляторами. Вызовы функций, вложенные операторы присваивания, операции инкрементирования и декрементирования имеют «побочные эффекты» — в ходе вычисления выражений могут непредсказуемо модифицироваться некоторые переменные. В любом выражении, в котором возможны побочные эффекты, присутствует зависимость от порядка использования переменных в вычислениях.
115. Писать код, зависящий от порядка вычисления аргументов или операндов, — это плохой стиль в любом языке программирования. Разумеется, необходимо знать, каких вещей избегать, однако если вы вообще не знаете, как те или иные операции выполняются в разных системах, у вас не возникает иссушения воспользоваться свойствами конкретной реализации.
116. Выражение наподобие x = 0, i++ или printf(…) становится оператором, если после него поставить точку с запятой. В языке C точка с запятой является элементом оператора и его завершающей частью, а не разделителем операторов, как в языке Pascal. Фигурные скобки, { и }, служат для группировки объявлений и операторов в составные операторы, или блоки, синтаксически эквивалентные одному оператору.
117. Более краткая форма if (выражении != 0) выглядит так: if (выражение) — такая запись часто оказывается более естественной и понятной, хотя иногда может вызвать путаницу.
118. Здесь else соответствует внутреннему if, что и показано с помощью отступа. Отступ ясно показывает, чего хочет программист, но компилятор этого не понимает и в результате ассоциирует else с ближайшим вложением if. Такие ошибки найти нелегко, поэтому рекомендуется всегда использовать фигурные скобки для выделения вложенных операторов if.
119. Иногда последний оператор else оператор не нужен — для него не предусмотрены никакие операции. Тогда и завершающий фрагмент можно опустить или же воспользоваться им для обработки ошибок — перехвата «невозможной» ситуации.
120. Напишите версию функции с одной проверкой внутри цикла и сравните быстродействие (затраты времени).
121. За исключением нескольких меток для одной и той же операции, сквозного выполнения в switch следует по возможности избегать, а все случаи, когда оно все же применяется, тщательно комментировать.
122. С точки зрения хорошего стиля программирования рекомендуется ставить оператор break даже в конце последнего блока switch (в данном случае default), хотя в этом нет необходимости. Со временем, когда в конец оператор switch добавится еще один блок case, этот нехитрый прием подстраховки спасет вас от лишних неприятностей.
123. Любую из трех частей заголовка for можно опустить, хотя точки с запятыми должны остаться на своих местах.
124. Какой из двух циклов использовать, while или for, — это в значительной мере дело вкуса. Цикл for следует предпочесть тогда, когда есть простая инициализация и инкрементирование переменных цикла, поскольку все управляющие элементы цикла в этом случае удобно сгруппированы вместе в его заголовке. Перегружать инициализацию и инкрементирование в цикле for операциями, не относящимися к делу, — это плохой стиль. В заголовке цикла следует выполнять только операции по непосредственному управлению этим циклом.
125. Это стандартная конструкция (идиома) языка C для обработки n элементов массива.
126. Основная идея алгоритма, разработанного Д. Л. Шеллом (D. L. Shell) в 1959 году, заключается в том, что на первом этапе сортировки сравниваются далеко отстоящие друг от друга элементы, а не соседние, как в более простых методах. Это позволяет быстро ранжировать большие массы неупорядоченных элементов, чтобы на следующих этапах оставалось меньше работы. Интервал между сравниваемыми элементами постепенно уменьшается до единицы, и в этот момент сортировка сводится к обмену соседних элементов — методу пузырьков.
127. Ещё одним знаком операции языка С является запятая, которая чаще всего находит применение в операторе for. Пара выражений, разделенных запятой, вычисляется слева направо; тип и значение результата равны типу и значению правого операнда. Однако запятые, которые разделяют аргументы функций, переменные в объявления и т.п., не являются знаками операций и не гарантируют порядок вычисления слева направо. Операцию «запятая» следует использовать как можно реже и с осторожностью. Наиболее уместно ее применение в конструкциях, где объединяются в единое целое связанные друг с другом операции.
128. Опыт показывает, что цикл do-while используется значительно реже, чем while и for. Тем не менее время от времени он оказывается полезным.
129. Выход (break) выполняется из ближайшего (самого внутреннего) цикла или оператора switch. Оператор continue часто используется там, где следующая за ним часть цикла слишком длинная и сложная, так что выделение ее отступами в условном операторе ухудшает удобочитаемость.
130. Рекомендуется проверить, правильно ли ведет себя функция даже в тех случаях, когда строка пустая или содержит только символы пустого пространства.
131. В языке С имеется оператор goto — бездонный источник потенциальных неприятностей — и метки для перехода с его помощью. В техническом плане оператор goto никогда не бывает необходим, и на практике почти всегда легче обходиться без него. В этой книге он вообще не используется. Тем не менее есть несколько ситуаций, в которых и goto может найти свое место пол солнцем. Наиболее распространенная — это необходимость прекратить работу управляющей структуры с большим количеством вложений, например выйти сразу из двух и более вложенных циклов. Оператор break туту не применим непосредственно, поскольку он обеспечивает выход только из внутреннего, ближайшего цикла. Код, в котором есть оператор goto, всегда можно переписать без него, хотя, возможно, придется добавить дополнительную проверку или переменную. За немногими исключениями наподобие приведенного здесь примера код, основанный на переходах с помощью оператора goto, труднее понимать и дорабатывать, чем код без этих операторов. Хотя мы и не утверждаем этого категорически, все же оператор goto следует стараться употреблять как можно реже или вообще никогда.
132. Функции не только помогают разбить вычислительные задачи на набор маленьких, но и позволяют программистам пользоваться разработками предшественников, а не начинать все заново. Хорошо написанные функции скрывают подробности своей работы от тех частей программы, которым их знать не положено, таким образом проясняя задачу в целом и облегчая процесс внесения исправлений и дополнений. Обычно программы на С состоят из множества мелких функций, а не нескольких сравнительно больших. Программа может храниться в одном или нескольких файлах исходного кода, которые можно компилировать по отдельности, а затем собирать вместе, добавляя к ним предварительно скомпилированные функции из библиотек.
133. Более общей стала инициализация — теперь можно инициализировать автоматические массивы и структуры.
134. С маленькими частями удобнее иметь дело, чем с большим целым, поскольку в функциях можно скрыть несущественные детали реализации, при этом избежав риска нежелательного вмешательства одной части программы в другую. К тому же отдельные части могут даже оказаться полезными для других задач.
135. Поскольку массивы в С начинаются с нулевой позиции, индексы могут быть нулевыми или положительными, а отрицательное значение наподобие -1 удобно для сигнализации ошибки.
136. Такого рода функция, не выполняющая никаких операций, часто бывает полезна как заменитель («заглушка») в ходе разработки программы.
137. Если тип возвращаемого значения опущен, по умолчанию подразумевается int.
138. Всякая программа является всего лишь набором определений переменных и функций.
139. Если функция не возвращает значения, хотя по объявлению должна это делать, в передаваемых ею данных гарантированно будет содержаться «мусор» — случайные числа.
140. При использовании команды cc объектный код отличают от исходного благодаря применению разных расширений имен файлов — соответственно .o и .c.
141. Один из способов сделать это — объявить atof прямо в вызывающей функции.
142. Использовать этот стиль в новых программах — дурной тон. Если функция принимает аргументы, объявляйте их. Если же она не принимает никаких аргументов, пишите void.
143. При выполнении этой операции возможна частичная потеря информации, поэтому некоторые компиляторы в таких случаях выдают предупреждения. А вот при наличии явного приведения типов компилятору становится ясно, что операция выполняется намеренно, так что он не выдает предупреждений.
144. Прилагательное «внешний» здесь употребляется как противоположность слову «внутренний», которое относится к аргументам и переменным, определенным внутри функций.
145. В С нельзя определить функцию внутри другой функции.
146. По умолчанию внешние переменные и функции имеют то свойство, что все ссылки на них по одним и тем же именам — даже из функций, скомпилированных отдельно, — являются ссылками на один и тот же объект программы (Стандарт называет это свойство внешним связыванием — external linkage).
147. Если функциям необходимо передавать друг другу большое количество элементов данных, то внешние переменные удобнее и эффективнее, чем длинные списки аргументов, однако, как было указано в главе 1, здесь не все так однозначно, поскольку от этого падает структурированность программы и возникает слишком сильная перекрестная зависимость функций друг от друга.
148. Операции помещения в стек и извлечения из него тривиальны, но после добавления проверки и исправления ошибок эти операции становятся достаточно объемными, чтобы поместить их в отдельные функции, а не повторять длинные фрагменты несколько раз.
149. Рассмотрим эти вопросы на примере, распределив код программы-калькулятора по нескольким файлам. С практической точки зрения эта программа слишком мала, чтобы ее стоило так разбивать, на зато это хорошая иллюстрация важных приемов, применяемых в больших программах.
150. Областью действия (видимости) имени называется часть программы, в пределах которой можно использовать имя. Для автоматической переменной, объявляемой в начале функции, областью видимости является эта функция. Область действия внешней переменной или функции распространяется от точки, в которой она объявлена, до конца компилируемого файла.
151. Важно понимать различие между объявлением внешней переменной и ее определением. Объявление сообщает, что переменная обладает определенными свойствами (в основном типом), а определение выделяет место в памяти для ее хранения.
152. Поместим функции getch и ungetch в четвертый файл, gtech.c. Они будут отделены от остальных, потому что в настоящей, не учебной программе такие функции берутся из заранее скомпилированных библиотек.
153. С одной стороны, хочется разрешить доступ из каждого файла только к самой необходимой информации; с другой стороны, уследить за множеством заголовочных файлов довольно нелегко. Нужно искать компромисс: добиться одного можно, только жертвуя другим. Пока объем программы не достиг некоторой средней величины, бывает удобнее хранить все, что необходимо для связей двух частей программы, в одном заголовочном файле. В программах, намного больших по объему, требуется более разветвленная организация и больше заголовочных файлов.
154. Если объявление внешней переменной или функции содержит слово static, ее область действия ограничивается данным файлом исходного кода — от точки объявления до конца. Таким образом, внешние статические переменные — это механизм сокрытия имен. Чаще всего внешними статическими объявляются переменные, но такое объявление применимо и к функциям. Обычно имена функций являются глобальными и видимыми в любой части программы. Но если функцию объявить статической (static), ее имя будет невидимо за пределами файла, в котором она объявлена.
155. Объявление с ключевым словом register сообщает компилятору, что соответствующая переменная будет интенсивно использоваться программой. Идея заключается в том, чтобы поместить такие (регистровые) переменные в регистры процессора и добиться повышения быстродействия и уменьшения объема кода. Впрочем, компилятор имеет право игнорировать эту информацию. Объявление register применимо только к автоматическим переменным и к формальным параметрам функций. На практике существуют ограничения на употребление регистровых переменных, связанные со свойствами аппаратных устройств компьютера. В каждой функции только очень небольшое количество переменных (и только нескольких определенных типов) можно сделать регистровыми. Однако от лишних объявлений register не бывает никакого вреда, поскольку избыточные или неразрешенные объявления просто игнорируются.
156. Язык С не является блочно-структурным языком в том же смысле, как Pascal или другие языки, поскольку в нем функции нельзя определять внутри других функций. С другой стороны, внутри одной функции переменные можно определять в блочно-структурном стиле. Автоматическая переменная объявляется и инициализируется в блоке всякий раз заново при входе в этот блок.
157. Автоматические переменные, в том числе формальные параметры, также преобладают над внешними переменными и функциями с теми же именами, скрывая и блокируя действе их имен. Стремясь программировать в хорошем стиле, лучше избегать имен переменных, которые перекрывают действие переменных из более внешних блоков, — слишком велика опасность путаницы и непредвиденных ошибок.
158. При отсутствии явной инициализации внешние и статические переменные гарантированно инициализируется нулями, а автоматические и регистровые получают неопределенные начальные значения («мусор»). Инициализирующие выражения для внешних и статических переменных должны быть константными; инициализация выполняется один раз, фактически до начала выполнения программы. А для автоматических и регистровых переменных инициализация выполняется каждый раз при входе в соответствующий блок. Фактически инициализация автоматической переменной — это сокращенная запись для комбинации объявления и оператора присваивания. Если не указывать размер массива, компилятор вычислит его сам, сосчитав инициализирующие значения. Если указано меньше инициализирующих значений, чем заданные размер массива, то недостающие значения будут заменены нулями для внешних, статических и автоматических переменных. Если же их слишком много, возникнет ошибка.
159. Длина массива символов (строки) равна количеству символов плюс завершающий ‘\0’.
160. Наша версия быстрой сортировки — не самая быстрая из возможных, но зато одна из простейших.
161. Операция перемены элементов местами, swap, вынесена в отдельную функцию, поскольку она выполняется в qsort три раза.
162. Рекурсия может оказаться несовместимой с экономией памяти, поскольку нужно где-то хранить стек обрабатываемых чисел. Не дает она также и выигрыша в быстродействии. Но рекурсивный код обычно компактнее и его значительно легче писать и дорабатывать, чем его аналог без рекурсии.
163. Некоторые средства языка С реализованы с помощью препроцессора, работа которого по сути является первым этапом компиляции.
164. Если имя_файла указано в кавычках, поиск файла обычно начинается с того места, где находится исходный текст программы. Если его там нет или если имя файла заключено в угловые скобки, то поиск файла продолжается по правилам, зависящим от реализации языка. Строго говоря, это даже не обязательно должны быть файлы; способ обращения к заголовочным модулям зависит от системы и реализации языка.
165. Разумеется, при внесении изменений во включаемый файл все файлы, которые ссылаются на него, должны быть перекомпилированы.
166. Область действия имени, заданного в директиве #define, распространяется от точки его определения до конца файла исходного кода. Подстановка выполняется только для идентификаторов и не распространяется на такие же строки символов в кавычках. Например, если имени YES задано макроопределение, подстановка не выполняется для printf(“YES”) или YESMAN.
167. Правила использования вложенных знаков ## загадочны и малопонятны; подробности можно найти в приложении А.
168. Если выражение после #if не равно нулю, в программу включаются все последующие строки вплоть до директивы #endif, #elif или #else. Выражение defined(имя) в директиве #if равно единице, если имя определено с помощью #define, и нулю в противном случае.
169. Директивы #ifdef и #ifndef — это специальные формы условной директивы для проверки того, определено то или иное имя или нет. Первый пример директивы, приведенный в начале раздела, #if !defined(HDR) можно было бы записать так: #ifndef HDR.
170. Указатель — это переменная, содержащая адрес другой переменной. Указатели очень широко используются в С — частично потому, что иногда определенные операции можно выполнить только с их помощью, а частично потому, что их использование обычно позволяет записать код более компактно и эффективно, чем другими способами. Указатели и массивы тесно связаны между собой.
171. Об указателях говорят, что наряду с оператором goto — это верный способ запутать программу до невозможности. Это, несомненно, правда — если пользоваться ими бездумно.
172. Теперь стандартным объявлением нетипизированного указателя является void* (указатель на пустой тип void), а не char*.
173. Операция & применима только к объектам, хранящимся в оперативной памяти: переменным и элементами массивов. Ее нельзя применить к выражениям, константам и регистровым переменным (register).
174. Одноместная операция * называется операцией ссылки по указателю (indirection) или разыменования (dereferencing). Применяя ее к указателю, получаем объект, на который он указывает.
175. Форма этого объявления задумывалась как мнемоническая — специально для облегчения его понимания и использования. Объявление сообщает, выражение *ip имеет тип int. Синтаксис объявления переменной копирует синтаксис выражений, в которых эта переменная может появляться.
176. Следует также заметить, что любой указатель может указывать только на объекты одного конкретного типа данных, заданного при его объявлении. (Исключением является только «указатель на void», в котором может содержаться произвольный адрес без указания на тип данных; однако по указателям этого типа нельзя ссылаться и получать значения).
177. Поскольку в языке С аргументы передаются в функцию по значению, не существует прямого способа изменить локальную переменную вызывающей функции, действую внутри вызываемой функции.
178. Любую операцию, выполняемую с помощью индексации массива, можно проделать с применением указателей, причем код с применением указателей обычно работает быстрее, но для непосвященных выглядит более запутанно.
179. Смысл прибавления единицы к указателю (или, если обобщить, любой операции адресной арифметики) состоит в том, что pa+1 указывает на следующий объект, а pa+i — на i-й объект после a. p + n — эта конструкция обозначает адрес n-ого объекта после того, на который указывает p. Это всегда справедливо независимо от типа данных, на которые указывает p; число n масштабируется по размеру соответствующих объектов, который определяется объявлением p. Например, если тип int имеет длину четыре байта, n будет прибавляться с коэффициентом 4. Все манипуляции с указателями будут автоматически выполняться с учетом размера объектов.
180. По определению значение переменной или выражения типа «массив» является адресом нулевого элемента массива. Имя массива является синонимом для местоположения его первого элемента. Если в функцию передается имя массива, по сути туда поступает адрес первого элемента.
181. Язык С гарантирует, что 0 никогда не бывает адресом данных, поэтому возвращение нуля можно использовать как сигнал аварийного завершения — в данном случае нехватки места в буфере памяти. Часто вместо нуля используется символическая константа NULL, которая более четко показывает, что это не просто число, а специальное значение указателя. Любой указатель можно сравнивать с нулем, и это сравнение всегда будет иметь смысл. Но если указатели не указывают на один и тот же массив, то арифметические операции и сравнения с их участием не будут определены.
182. Количество символов в строке может быть настолько большим, что не поместится в переменную типа int. В заголовочном файле <stddef.h> определяется тип ptrdiff_t, достаточно большой для того, чтобы содержать разность любых двух указателей со знаком. Однако, чтобы соблюсти технику безопасности, лучше всего принять size_t в качестве типа возвращаемого strlen значения. Именно так сделано в стандартной библиотечной версии этой функции. Тип size_t — это целочисленный тип без знака; результаты этого типа дает операция sizeof.
183. Во внутреннем представлении строки она заканчивается нулевым символом ‘\0’, чтобы программа могла найти ее конец. Таким образом, фактическая длина строки в памяти на один символ длиннее, чем то, что написано в двойных кавычках.
184. while(*s++ = *t++); — на первый взгляд эта запись кажется невразумительной, но на самом деле она достаточно удобна, и эту программную идиому следует освоить, поскольку в программах на С она встречается часто.
185. Как обычно, лучше всего будет разделить программу на функции, которые соответствуют этому естественному делению задачи, а на функцию main возложить обязанности по управлению ими.
186. Нужно внести изменения в объявления, а сравнение выполнять функцией strcmp. Сам алгоритм остается в точности тем же, и это позволяет думать, что он будет работать так же хорошо, как и раньше.
187. Поскольку количество дней в месяцах отличается для високосного и обычного года, их лучше занести отдельно в строки двумерного массива, чем вычислять на ходу в программе, сколько дней в феврале того или иного года. Поскольку экономия памяти в данном случае не актуальна, так работать удобнее, чем вычислять номера месяцев по индексам.
188. Его элементы имеют тип char; этим подчеркивается, что переменные данного типа можно использоваться для хранения небольших целых чисел вместо символов.
189. В языке С двумерный массив — это фактически одномерный массив, каждый элемент которого в свою очередь является массивом.
190. Если двумерный массив необходимо передать в функцию, среди ее параметров должно быть количество столбцов. Количество строк необязательно — например, [][13]. Обобщая, можно сказать, что длину первого измерения (диапазона индексов) массива можно не указывать, а вот наличие остальных обязательно.
191. Это идеальный случай для применения внутреннего статического массива.
192. Начинающие программисты на С часто путаются в различиях между двумерными массивами и массивами указателей. int a[10][20]; int *b[10];, где a — полноправный двумерный массив; для него выделено 200 ячеек памяти размера int. А вот для переменной b ее определение всего лишь выделят память для 10 указателей без их инициализации. Важное преимущество массива указателей состоит в том, что строки массива могут иметь различную длину. Самое популярное применение массивов указателей — это хранение символьных строк различной длины.
193. В системных средах, поддерживающих язык С, существует способ передавать в программу аргументы или параметры командной строки при запуске программы на выполнение. При вызове функции main она получает два аргумента. Первый, который обычно называют argc (от argument count — счетчик аргументов), содержит количество аргументов командной строки, с которыми была запущена программа. Второй, обычно под именем argv (от argument vector — вектор аргументов), указывает на массив символьных строк, содержащих сами аргументы, — по одному в строке. Для манипулирования этими символьными строками, естественно, используется многоуровневая система указателей. По определению argv[0] содержит имя, под которым запускается программа, поэтому argc всегда не меньше 1. Если счетчик argc равен 1, то после имени программы нет никаких аргументов командной строки. В программах на С в системе Unix принято, чтобы необязательные аргументы или ключи начинались со знака «минус». Например, для поиска по обратному алгоритму можно ввести ключ –x (от except — кроме), а для вывода нумерации строк — ключ –n (от number — номер).
194. Напомним, что в тот раз образец был «зашит» прямо в программу, что явно неудобно с точки зрения ее универсальности. Кроме того, нужно разрешить ставить необаятельные аргументы в любом порядке; к тому же основная часть программы не должна зависеть от количества задаваемых аргументов. Программа должна вести себя устойчиво и рационально даже с самыми нелепыми значениями n и содержимым входного потока. Напишите ее так, чтобы она максимально экономно использовала память: строки должны храниться таким образом, как в программе сортировки из раздела 5.6, а не в двумерном массиве фиксированного размера. Обработка ошибок в аргументах здесь опущена, чтобы сосредоточиться на главном.
195. Только в редких случаях используются выражения с адресной арифметикой еще более сложные, чем приведенные выше; в таких случаях удобнее будет разбить вычисления на два-три этапа.
196. Алгоритм сортировки часто состоит из трех частей: процедуры сравнения, определяющей способ упорядочения любой пары объектов; процедуры обмена местами, изменяющей порядок следования такой пары; собственно процедуры сортировки, выполняющей сравнение и обмен, пока объекты не окажутся упорядоченными. Сама сортировка независима от вида операций сравнения и обмена, поэтому можно задать сортировку по различным критериям, передавая в алгоритм разные функции сравнения и обмена.
197. Любой указатель можно привести к типу void* и обратно без потери информации.
198. Язык С иногда обвиняют в чрезмерно сложном синтаксисе объявлений — в частности, тех, в которых участвуют указатели на функции. Синтаксис языка построен так, чтобы объявление и использование переменных были максимально согласованы. Для простых случаев это получается вполне удачно, а вот для сложных может возникать путаница, поскольку объявления не всегда читаются слева направо, да еще и бывают перегружены скобками. На практике редко встречаются очень сложные объявления, но тем не менее нужно понимать их, а по необходимости и писать. Один из удачных подходов — синтезировать объявления постепенно, с помощью оператора typedef.
199. Поскольку эти программы лишь демонстрационные, они не отличаются «дуракоустойчивостью» и работают лишь со значительными ограничениями. Также их запутывают лишние пробелы. Обработка ошибок налажена не слишком хорошо, поэтому и неправильные объявления также сбивают программу с толку. Доработайте dcl так, чтобы программа справлялась с обработкой ошибок во входных данных.
200. Структура — это совокупность нескольких переменных, часто различных типов, сгруппированных под единым именем для удобства обращения. (В некоторых языках — например, в Pascal — структуры называются записями.) С помощью структур удобно организовывать сложные данные — в частности, в больших программах, поскольку благодаря им группу связанных друг с другом переменных можно рассматривать и обрабатывать как единое целое, а не как разрозненный набор элементов.
201. После слова struct может стоять необязательный идентификатор, именуемый меткой структуры.
202. Переменные, перечисленные в объявлении структуры, называются ее членами, элементами или полями. В разных структурах можно использовать одни и те же имена элементов, хотя с точки зрения хорошего стиля так можно делать только тогда, когда речь идет о близкородственных объектах.
203. Структуру можно инициализировать, поставив после ее определения список значений-констант: struct point maxpt = { 320, 200 }; <…> Более строго было бы также заключить инициализаторы для каждой «строки» или структуры в фигурные скобки: { “auto”, 0 }, { “break”, 0 }, { “case”, 0 }, … Однако в этих внутренних скобках нет никакой необходимости.
204. Структуры нельзя сравнивать между собой.
205. Параметры-структуры передаются по значениям, как и любые другие параметры. Если в функцию необходимо передать большую структуру, это лучше сделать, передав указатель на нее, а не копию всех ее данных.
206. В языке С есть одноместная операция sizeof, выполняемая при компиляции, с помощью которой можно вычислить размер любого объекта. Строго говоря, операция sizeof дает целое значение без знака, тип которого называется size_t и определен в заголовочном файле <stddef.h>. А вот еще один способ, основанный на делении длины массива на длину его же первого элемента: #define NKEYS (sizeof keytab / sizeof keytab[0]) Здесь имеется то преимущество, что в записи не нужно ничего менть при изменении типа массива.
207. Складывать указатели нельзя, а вот вычитать можно.
208. Если p — указатель на структуру, то арифметические операции над p выполняются с учетом размера этой структуры, так что выражение p++ инкрементирует p настолько, чтобы получить следующий элемент массива структур.
209. Не следует предполагать, что длина структуры равна сумме длин ее элементов. Различные объекты по-разному выравниваются по машинным словам, поэтому внутри структуры могут быть неименованные «дыры». Например, если тип char имеет длину 1 байт, а int — 4 байта, то следующая структура может фактически занимать в памяти восемь байт, а не пять: struct { char c; int i; }; Надо отметить, что операция sizeof всегда дает правильное значение размера объекта.
210. Это исключительно дело вкуса; выберите одну форму и придерживайтесь ее.
211. Структуре не разрешается иметь в качестве компоненты экземпляр самой себя.
212. Мы опустили контроль ошибок в значениях, возвращаемых из strdup и talloc; вообще говоря, это неразумно, и здесь мы были не правы.
213. Как удовлетворить требование большинство большинства реальных систем к выравниванию объектов некоторых типов по границам машинных слов (например, целые числа часто выравниваются по четным адресам)? Требования к выравниванию обычно удовлетворить нетрудно за счет некоторых избыточных трат памяти. Для этого нужно сделать так, чтобы функция распределения памяти всегда возвращала указатель, удовлетворяющий всем возможным требованиям к выравниванию.
214. Функция malloc возвращает NULL, если она не может выделить участок памяти; в таком случае strdup передает это значение дальше, возлагая обработку ошибки на вызывающую функцию.
215. Алгоритм основан на поиске в хэш-таблице: поступающее имя конвертируется в небольшое неотрицательное число, которое используется как индекс в массиве указателей. Каждый элемент массива указывает на начало связанного списка блоков, описывающих имена, которые соответствуют данному хэш-коду. Если какому-либо хэш-коду не соответствуют никакие имена, возвращается NULL. <…> Это не самая лучшая хэш-функция из возможных, но зато она короткая и быстрая.
216. Цикл for в функции lookup — это стандартная идиоматическая конструкция для перебора элементов связанного списка: for (ptr = head; ptr != NULL; ptr = ptr->next) …
217. В языке С есть такое специальное средство для определения имен новых типов данных, как оператор typedef. Например, чтобы ввести синоним типа int под названием Length, следует записать: typedef int Length; Новые типы, определяемые с помощью typedef, начинаются с прописной буквы, чтобы можно было их легко различить. Фактически оператор typedef очень напоминает директиву #define с тем исключением, что, поскольку он анализируется компилятором, он может допускать такие текстовые подстановки, которые препроцессору не по силам. Для применения оператора typedef есть две основные побудительные причины, помимо стилевых и вкусовых предпочтений. Первая — это возможность параметризировать программу с целью улучшения переносимости. Если используемые в программе типы данных могут зависеть от системы и аппаратной конфигурации компьютера, их можно определить через typedef, а затем при переносе заменять только эти определения. Так, часто с помощью typedef определяются условные имена для целочисленных типов, чтобы уже в конкретной системе подставить short, int или long. В качестве примера можно привести такие типы из стандартной библиотеки, как size_t и ptrdiff_t. Вторая причина, побуждающая применять typedef, — это возможность улучшить удобочитаемость, сделать программу самодокументированной. Название типа Treeptr может оказаться более информативным, чем просто указатель на какую-то сложную структуру.
218. Объединение — это переменная, которая может содержать объекты различных типов и размеров (но не одновременно); при этом удовлетворение требований к размеру и выравниванию возлагается на компилятор. С помощью объединений можно работать с данными различных типов в пределах одного участка памяти, не привнося в программу элементы низкоуровневого, машинно-зависимого программирования. Объединения аналогичны записям с вариантами в языке Pascal. <…> Удобно, чтобы такие значения занимали одинаковый объем памяти и хранились в одном месте независимо от типа. Именно для этого и существуют объединения — переменные, способные хранить данные любого из нескольких типов: union u_tag { int ival; float fval; char *sval; } u; Переменная u будет иметь достаточную длину, чтобы содержать данные самого длинного из трех типов; конкретный размер зависит от системы и реализации. Извлекать можно данные только того типа, какие были помещены при последнем обращении к переменной. Следить и помнить, какие именно данные были помещены в объединение, — это забота программиста; если поместить значение одного типа, а извлечь его как значение другого, результат будет системно-зависимым и трудно предсказуемым. Фактически объединение является структурой, в которой все элементы имеют нулевой смещение от ее начала, сама она имеет достаточную длину, чтобы в нее поместился самый длинный элемент, и при этом выравнивание выполняется правильно для всех типов данных в объединении. Над объединениями разрешено выполнять те же операции, что и над структурами: присваивать или копировать как единое целое, брать адрес и обращаться к отдельным элементам. Объединение можно инициализировать только данными того типа, который имеет его первый элемент.
219. Если место в памяти — на вес золота и каждый байт на счету, приходится упаковывать несколько объектов в одно машинное слово. <…> Здесь числовые константы должны быть степенями двойки.
220. Некоторые операции встречаются так часто, что превратились в устойчивые конструкции (идиомы).
221. struct { unsigned int is_keyword: 1; unsigned int is_extern : 1; unsigned int is_static: 1; } flags; Здесь определяется переменная flags, содержащая три однобитовых поля. Число после двоеточия задает ширину поля в битах. Поля объявлены как unsigned int, чтобы гарантированно быть величинами без знака. Практически все, что связано с битовыми полями, является системно-зависимым. Для принудительного выравнивания по границе следующего слова можно использовать специальное значение длины поля, равное 0.
222. В некоторых системах поля выстраиваются слева направо, а в других — справа налево. Это означает, что хотя с помощью полей удобно работать с внутренними структурами данных, в случае обработки внешних структур следует тщательно подходить к вопросу о том, с какого конца начинать. Программы, зависимые от такого порядка, не переносимы между системами.
223. В этой главе рассматривается стандартная библиотека — набор функций для ввода и вывода данных, обработки строк, управления памятью, математических вычислений и других операций, необходимых в программах на С. В стандарте ANSI все библиотечные функции определены совершенно точно, так что они существуют в эквивалентной форме в любой системе, поддерживающей язык С. Программы, в которых взаимодействие с системой осуществляется исключительно с помощью средств из стандартной библиотеки, можно переносить из одной среды в другую безо всяких изменений.
224. Если систем работает не совсем таким образом, библиотека делает все необходимое, чтобы программе казалось именно так.
225. Стандартный поток вывода (standard input) обычно представлен клавиатурой. Стандартный поток вывода (standard output) по умолчанию соответствует экрану монитора.
226. Символическая константа EOF определена в <stdio.h>. Обычно ее значение равно -1, но в выражениях следует писать EOF, чтобы не зависеть от конкретного значения.
227. Если имя файла заключено в угловые скобки (<>), заголовочный файл разыскивается в определенном стандартом месте (например, в системах Unix это каталог /usr/include).
228. Как уже упоминалось, «функции» наподобие getchar и putchar из <stdio.h> и tolower из <ctype.h> часто являются макросами, благодаря чему удается избежать излишних затрат на вызов функций для каждого символа.
229. Независимо от того, как функция из файла <ctype.h> реализована в конкретной системе, программы с их использованием благодаря им защищены от особенностей того или иного символьного набора.
230. Функция sprintf выполняет те же преобразования, что и printf, но помещает результат вывода в символьную строку.
231. int printf(char *fmt, …) — здесь конструкция … означает, что количество и типы аргументов могут меняться. Такая конструкция может стоять только в конце списка аргументов.
232. Реализация этого заголовочного файла системно-зависима, однако интерфейс всегда единообразный.
233. В конце файла возвращается EOF; обратите внимание, что это не 0, который означает, что очередной введенный символ не соответствует текущей спецификации в строке формата.
234. Знак & с именем monthname употреблять не надо, поскольку имя массива само по себе является указателем.
235. Обычно эта ошибка не выявляется при компиляции.
236. В некоторых системах делается различие между текстовыми и двоичными файлами; для работы с двоичными файлами следует добавить в строку режима букву “b” (от “binary”).
237. Если произошла ошибка, функция fopen возвращает NULL. Если необходимо, произошедшую ошибку можно определит точнее; см. описание функций обработки ошибок в конце раздела 1 приложения Б.
238. Когда программа на С запускается на выполнение, операционная система автоматически открывает три файла и создает файловые указатели на них. Это стандартный поток ввода, стандартный поток вывода и стандартный поток ошибок. Соответствующие указатели называются stdin, stdout и stderr; они объявлены в файле <stdio.h>. Обычно stdin ассоциирован с клавиатурой, а stdout и stderr — с экраном монитора, но stdin и Stdout можно ассоциировать с файлами ил конвейерами (перенаправить), как описано в разделе 7.1.
239. Поскольку в большинстве операционных систем имеется ограничение на количество одновременно открытых программных файлов, полезно закрывать файлы и освобождать их указатели, как только файлы становятся ненужными. Есть и еще одна причины применить fclose к открытому файлу — в результате очищается и сбрасывается в файл буфер, в котором функция putc накапливает выходные данные. Функция fclose вызывается автоматически для каждого открытого файла во время нормального завершения программы. Функция exit вызывает fclose для закрытия каждого открытого файла, очищая тем самым буферы вывода.
240. Программа сигнализирует об ошибках двумя способами. Во-первых, диагностика ошибок посылается функцией fprintf в поток stderr и гарантированно появляется на экране, а не исчезает где-то в недрах файла или конвейера. В сообщения включается имя программы из argv[0], чтобы при использовании данной программы параллельно с другими можно было легко определить источник ошибки.
241. Принято, что возвращаемое значение 0 сигнализирует о нормальном завершении; различные ненулевые значения обычно обозначают аварийные ситуации.
242. Хотя ошибки при выводе очень редки, они все же случаются (например, при заполнении всего дискового пространства), поэтому программа профессионального коммерческого качества должна проверять и такие случаи. Мы пока не обращаем внимание на то, с какими кодами завершения заканчиваются наши небольшие иллюстративные программы, но в любой серьезной программе в таких случаях необходимо возвращать разумные и полезные значения.
243. Функция system(char*) выполняет команду, содержащуюся в символьной строе s, а затем возобновляет выполнение текущей программы. Допустимое содержимое строки s сильно зависит от конкретной операционной среды. Ниже приведен тривиальный пример из системы Unix: system(“date”);
244. Указатель, возвращаемый функциями malloc и calloc, выровнен в памяти надлежащим образом, но его еще нужно привести к нужному типу.
245. Такие функции, как isupper, можно реализовать с позиций экономии времени, а можно с позиций экономии места. Исследуйте обе возможности.
246. Операционная система Unix предоставляет свои служебные средства программам через системные вызовы (system calls), которые по сути являются встроенными функциями системы, вызываемыми из пользовательских программ.
247. Так как библиотека ANSI C в основном построена на идеях Unix, приведенный в этой главе код может помочь вам также в понимании работы библиотечных средств.
248. В любой конкретной системе функции стандартной библиотеки пишутся на основе средств, предоставляемых системой.
249. В операционной системе Unix весь ввод-вывод осуществляется путем чтения и записи файлов, поскольку все периферийные устройства, даже клавиатура и экран монитора, являются файлами единой файловой системы. Это означает, что любая передача данных между программой и периферийными устройствами управляется через единообразный интерфейс.
250. Нулевое количество означает, что достигнут конец файла, а -1 обозначает какую-либо ошибку. При записи возвращается количество записанных байт; если оно не равно количеству, посланному на запись, то произошла ошибка.
251. В ходе одного вызова можно записать или прочитать любое количество байт. Наиболее распространенные случая — это один символ за раз («небуферизованный ввод-вывод») либо количество наподобие 1024 или 4096, которое соответствует размеру физического блока данных на периферийном устройстве. Большими блоками передавать данные эффективнее, поскольку при этом выполняется меньше системных вызовов.
252. Для этого существуют два системных вызова — open и creat.
253. В файловой системе Unix с файлом ассоциируется девять битов служебной информации о допусках к нему. Они регулируют чтение, запись и запуск этого файла его владельцем, группой, к которой принадлежит владелец, и всеми остальными пользователями.
254. Функция unlink(char *name) удаляет заданное имя файла из файловой системы. Она соответствует стандартной библиотечной функции remove.
255. Ввод и вывод обычно выполняются последовательно: каждая операция read или write выполняется в той позиции файла, на который остановилась предыдущая операция. Однако по мере необходимости файл можно считывать или записывать в произвольном порядке. Системный вызов lseek позволяет передвигается по файлу, не считывая и не записывая никаких данных. С помощью функции lseek можно обращаться с файлами более-менее как с большими массивами, ценой некоторого снижения скорости доступа.
256. В случае ошибки она возвращает ненулевое число.
257. Напомним, что файлы в стандартной библиотеке описываются файловыми указателями, а не дескрипторами. Файловый указатель — это указатель на структуру, содержащую набор данных о файле.
258. В следующем фрагменте типичного файла <stdio.h> имена, предназначенные для использования только функциями самой библиотеки, начинаются со знака подчеркивания, чтобы уменьшить вероятность совпадения с идентификаторами прикладных программ. Это соглашение используется всеми стандартными библиотечными функциями.
259. Напоминаем, что слишком длинная директива #define продолжается на следующей строке с помощью обратной косой черты.
260. Символы возвращаются в формате unsigned, чтобы гарантировать их положительность.
261. Наша функция fopen не распознает режим “b” для двоичного доступа к файлу, поскольку система Unix не делает такого различия.
262. Перепишите функции fopen и _fillbuf с применением битовых полей вместо явных операций над битами. Сравните объем кода и быстродействие.
263. Возвращаемое ею значение типа int сообщает статус завершения, а не текущую позицию.
264. Примером может служить программа ls из системы Unix для вывода списка файлов в каталоге с сопутствующей информацией о них, такой как размеры, допуски и т.д. Аналогичные функции выполняет команда dir в системе MS-DOS.
265. Поскольку каталог в Unix представляет собой просто файл, программе ls нужно всего лишь прочитать этот файл и извлечь из него нужные имена. Но для получения другой информации — например, размера файла — необходимо пользоваться системными вызовами. В других системах даже для получения имен файлов в каталоге не обойтись без системного вызова; именно так обстоит дело в MS-DOS. Хотелось бы обеспечить доступ к этой информации сравнительно системно-независимым способом, пусть даже реализация окажется существенно основанной на средствах конкретной системы.
266. Начнем с краткого обзора устройства файловой системы Unix. Каталогом (directory) в ней называется файл, содержащий список имен файлов и информацию об их местоположении. Эта информация представляет собой ссылку на другую таблицу — список файловых индексов. Файловый индекс (inode) представляет собой структуру данных, содержащую всю информацию о файле, кроме его имени. Каждый пункт списка файлов в каталоге обычно состоит из имени файла и соответствующего номера индекса.
267. К сожалению, формат и точное содержание файла-каталога меняется от одной версии системы к другой. Поэтому разделим задачу на составные части и попытаемся изолировать непереносимые фрагменты. На внешнем уровне определяется структура Dirent и три функции, opendir, readdir и closedir, которые обеспечивают системно-независимый доступ к имени и номеру индекса в списке каталога.
268. Наличие скобок обязательно, поскольку приоритет операции & ниже, чем ==.
269. Каждый каталог всегда содержит пункты списка, соответствующие ему самому, с именем “.”, и родительскому каталогу с именем “..”. Их необходимо пропустить, иначе программа зациклится.
270. В системе, который мы обычно пользуемся, он совпадает с unsigned short, но информацию об этом нельзя внедрять в программу, поскольку в разных системах тип может отличаться. Поэтому лучше использовать typedef. Полный набор «системных» типов можно найти в файле <sys/types.h>.
271. Во-первых, многие программы не являются «системными программами»; они просто пользуются информацией, которую хранит и предоставляет операционная система. В таких программах очень важно, чтобы способ представления информации был целиком описан в стандартных заголовочных файлах, которые подключаются к программе; объявления и определения не должны включаться в текст программы непосредственно. Во-вторых, при известной сноровке можно разработать интерфейс к системно-зависимым объектам, который сам по себе будет относительно независим от системы. Функции стандартной библиотеки могут служить хорошим примером этого.
272. Эти функции иллюстрируют некоторые соображения, касающиеся написания системно-зависимого кода в сравнительно системно-независимой манере, а также демонстрируют реальное практическое применение структур, объединений и операторов typedef.
273. Когда поступает запрос на память, выполняется перебор списка свободных блоков, пока не обнаруживается достаточно большой блок. Этот алгоритм кратко называется «первый подходящий» — в отличие от «наилучшего подходящего», в котором отыскиваете наименьший из блоков, подходящих по размеру. Если блок имеет точно такой размер, как запрашивается, он отсоединяется от списка и предается пользователю. Если блок имеет больший размер, он делится на части, и пользователь получает столько, сколько просил, а остаток остается в списке. Если не удается найти достаточно большой блок, у операционной системы запрашивается очередной большой фрагмент памяти, который подключается к списку. При освобождении также выполняется поиск по списку свободных блоков: отыскивается место для вставки освобождаемого блока. Если освобождаемый блок вплотную граничит со свободным блоком с какой-либо из двух сторон, то он сливается с ним в единый блок большего размера, чтобы не слишком фрагментировать память. Определить, как граничат блоки, нетрудно, поскольку список свободных блоков организован по возрастанию адресов.
274. Различные системы устроены по-разному, но в каждой есть самый «требовательный» тип данных: если элемент данных этого типа можно поместить по определенному адресу, то любые другие элементы также можно поместить туда. В некоторых системах самый требовательный тип — double, тогда как в других достаточно int и long. <…> Полe Align в данном объединении никогда не используется; его наличие просто выравнивает каждый заголовок принудительно по границе самого требовательного типа.
275. Запрос памяти у системы — это сравнительно трудоемкая операция, и ее не стоит выполнять при каждом вызове malloc. Вот почему функция morecore запрашивает сразу как минимум NALLOC блоков; полученный большой блок потом будет «нарезаться» по мере необходимости. После установки поля размера morecore добавляет полученную память в общий фонд, вызывая функцию free.
276. Эта функция возвращает -1, если места в памяти недостаточно, хотя возвращать NULL в этом случае было бы удачнее.
277. Данная версия malloc переносима только между системами, в которых сравнение произвольных указателей имеет смысл и реализовано корректно.
278. Хотя распределение памяти — по самой своей сути всегда системно-зависимая операция, приведенный код иллюстрирует, как эту зависимость можно сделать управляемой и локализовать в очень малой части программы. Выравнивание регулируется с помощью typdef и объединений (при условии, что sbrk возвращает правильный указатель). Приведение типов, применяемое к указателям, сделано явным и даже приспособлено к неудачному системному интерфейсу. Хотя подробности реализации приведенных функций относятся в распределению памяти, общий подход можно применять и к решению других проблем.
279. Доработайте эти функции так, чтобы они более тщательно проверяли возможные ошибки.
280. Справочник является интерпретацией предложенного стандарта, но не самим стандартом, хотя нами были приложены все усилия, чтобы сделать его надежным пособием по языку.
281. Программа состоит из одной или нескольких единиц трансляции (translation units), Хранящихся в виде файлов. Она проходит несколько этапов трансляции, описанных в разделе А.12.
282. Существует шесть классов лексем: идентификаторы, ключевые слова, константы, строковые литералы, знаки операций и прочие разделители.
283. Идентификатор — это последовательность букв и цифр. Первым символом должна быть буква; знак подчеркивания (_) считается буквой. Буквы нижнего и верхнего регистра различаются. Идентификаторы могут иметь любую длину; для внутренних идентификаторов значимыми являются не менее 31 первого символа; в некоторых реализациях принято большее количество значимых символов.
284. Целочисленная константа, состоящая из последовательности цифр, воспринимается как восьмеричная, если она начинается с цифры 0 (цифры «нуль»), и как десятичная в противном случае.
285. Если константа — десятичная и без суффикса, то она будет иметь первый из следующих типов, которого достаточно для представления ее значения: int, long int, unsigned long int.
286. Символьная константа — это последовательность из одного или нескольких символов, заключенная в одинарные кавычки (например, ‘x’). Значение символьной константы, состоящей из одного символа, равно числовому коду символа в символьном наборе, принятом в системе на момент выполнения программы. Значение константы из нескольких символов зависит от реализации языка.
287. Символьные константы не могут содержать одинарную кавычку (‘) или символ конца строки. Чтобы изобразить их и некоторые другие символы, используются управляющие последовательности или специальные символы (escape sequences). Если после обратной косой черты не следует ни один из перечисленных выше символов, результат будет не определен.
288. В некоторых реализациях имеется расширенный набор символов, который не может быть представлен только типом char. Константа для такого набора записывается с буквой L впереди (например, L’x’) и называется расширенной символьной константой. Такая константа имеет целочисленный тип wchar_t, определенный в стандартном заголовочном файле <stddef.h>. Наборы символов, обычно используемые в США и Западной Европе, удобно кодировать типом char, тогда как типа wchar_t был введен главным образом для азиатских языков.
289. При отсутствии суффикса вещественный тип принимается double. Суффиксы вещественных констант введены в новом стандарте языка; первоначально они не существовали.
290. Идентификаторы, объявленные в составе перечислимого типа, являются идентификаторами типа int.
291. Строковой литерал, который также называется строковой константой, — это последовательность символов, заключенная в двойные кавычки (“…”). Такая строка имеет тип массив символов и класс памяти static (раздел А.4) и инициализируется заданными символами. Представляются ли одинаковые строковые литералы одним фактическим экземпляром строки в памяти ил несколькими, зависит от реализации языка. Результат работы программы, пытающейся изменить строковой литерал, не определен. Результат конкатенации обычных и расширенных строковых литералов друг с другом не определен.
292. Идентификаторы, или имена, обозначают различные компоненты программы: функции; метки структур и перечислений; элементы структур или объединений; имена новых типов в typedef; объекты. Объектом (называемым иногда переменной) является участок памяти, интерпретация которого в программе зависит от двух главных характеристик: ее класса памяти и типа. Класс памяти определяет время жизни памяти, ассоциированной с обозначаемым объектом, а тип определяет, какой смысл вкладывается в данные, находящиеся в объекте. С именем также ассоциируется своя область видимости или действия (т.е. тот участок программы, где это имя известно) и способ связывания, определяющий, обозначает ли это имя тот же самый объект или функцию в другой области действия.
293. Существует два класса памяти: автоматический и статический. Класс памяти объекта задается рядом ключевых слов в совокупности с контекстом объявления этого объекта. Автоматические объекты локальны в блоке (раздел А.9.3.) и при выходе из него уничтожаются. Объявления внутри блока создают автоматические объекты, если в них отсутствуют указания на класс памяти или указан модификатор auto. Объекты, объявленные с ключевым словом register, являются автоматическими и размещаются (по возможности) в быстро доступных регистрах процессора.
294. Статические объекты могут быть локальными в блоке или внешними по отношению ко всем блокам, но в обоих случаях их значения сохраняются после выхода из блока или функции до повторного входа. Внутри блока, в том числе и в теле функции, статические объекты объявляются с ключевым словом static. Объекты, объявляемые вне блоков на одном уровне с определениями функций, всегда являются статическими. С помощью ключевого слова static их можно сделать локальными в пределах единицы трансляции; в этом случае для них устанавливается внутреннее связывание. Они становятся глобальными для всей программы, если опустить явное указание класса памяти или использовать ключевое слово extern; в этом случае для них устанавливается внешнее связывание.
295. Размер объектов, объявляемых как символы (char), позволяет хранить любой символ из символьного набора, принятого в системе во время выполнения программы. Обычные объекты типа int имеют естественный размер, принятый в архитектуре конкретной системы, другие размеры предназначены для специальных целей. Более длинные целые типы имеют как минимум не меньшую длину в памяти, чем более короткие, однако в некоторых реализациях обычные целые типы могут быть эквивалентны коротким (short) или длинным (long). Тип long double введен в новом стандарте. В первой редакции синонимом для double был тип long float, который теперь изъят из обращения.
296. Нарду с базовыми типами существует практически бесконечный класс производных типов, конструируемых из базовых типов следующими способами: как массивы объектов определенного типа; как функции, возвращающие объекты определенного типа; как указатели на объекты определенного типа; как структуры, содержащие последовательности объектов различных типов; как объединения, способные содержать один объект из заданного множества нескольких объектов разных типов. В общем случае эти методы создания новых объектов могут применяться последовательно, в сочетании друг с другом.
297. Объявление объекта как volatile указывает на его особые свойства для выполняемой компилятором оптимизации.
298. Объект — это именованная область памяти; именующее выражение (lvalue) — это выражение, обозначающее объект или ссылающееся на него.
299. При преобразовании из вещественного типа в целочисленный дробная часть числа отбрасывается; если полученное при этом значение нельзя представить величиной заданного целочисленного типа, то результат не определен. В частности, не определен результат преобразования отрицательных чисел с плавающей точкой в целые числа без знака.
300. В эту часть языка внесены два изменения. Во-первых, арифметические операции с операндами типа float теперь могут выполняться с одинарной точностью, а не только с двойной; в первой редакции языка вся арифметика с плавающей точкой имела двойную точность.
301. Указатель можно привести к целочисленному типу, достаточно большому для его хранения; требуемый размер зависит от реализации. Указатель на один тип можно преобразовать в указатель на другой тип. Если исходный указатель не ссылается на объект, должным образом выровненный по границам слов памяти, в результате может возникнуть исключительная ситуация, связанная с адресацией. Если требования к выравниванию у нового типа менее строгие ил такие же, как у первоначального типа, то гарантируется, что преобразование указателя к другому типу и обратно не изменит его. Само понятие «выравнивания» зависит от реализации, однако в любой реализации объекты типа char предъявляют наименее строгие требования к выравниванию.
302. Любой указатель на объект можно привести к типу void* без потери информации. Если результат подвергнуть обратному преобразованию, получится исходный указатель.
303. В языке не определена обработка переполнения, деления на нуль и других исключительных ситуаций, возникающих при вычислении выражений. В большинстве существующих реализаций С переполнение игнорируется при вычислении целочисленных выражений со знаком и присваивании, но в целом такое поведение выполняющей системы стандартом не гарантируется. Обработка деления на нуль и всех исключительных ситуаций вещественной арифметики может отличаться в разных реализациях; иногда для этой цели существует нестандартная библиотечная функция.
304. Первичные выражения — это идентификаторы, константы, строки и выражения в скобках. Идентификатор является именующим выражением (lvalue), если он обозначает объект и имеет арифметический тип либо тип «структура», «объединение» или «указатель».
305. Термин аргумент используется для выражения, передаваемого при вызове функции; термин параметр — для обозначения получаемого ею объекта (или его идентификатора) в объявлении или определении функции. В том же смысле иногда используются термины «фактический аргумент (аргумент)» и «формальный аргумент (параметр)».
306. Имеются два способы (стиля) объявления функций. В новом стиле типы параметров задаются явно и являются частью типа функции; такое объявление еще называется прототипом функции. В старом стиле типы параметров не указываются.
307. Порядок вычисления аргументов не определяется стандартом; в разных компиляторах он может отличаться. Однако гарантируется, что аргументы и именующее обозначение функции вычисляется полностью (включая и побочные эффекты) до входа в нее.
308. Операция в качестве результата дает значение операнда. Одноместный плюс был добавлен для симметрии с одноместным минусом.
309. Данную операцию нельзя применять к операнду типа «функция», операнду неполного (заранее объявленного, но не полностью определенного) типа или битовому полю.
310. Двуместная операция / дает частное, а % — остаток от деления первого операнда на второй; если второй операнд равен 0, то результат не определен.
311. Указатель на объект в массиве можно складывать со значением любого целочисленного типа. Последнее преобразуется в адресное смещение посредством умножения его на размер объекта, на который ссылается указатель. Сумма является указателем того же типа, что и исходный указатель, но ссылается на другой объект того же массива, отстоящий от исходного на вычисленное смещение.
312. В отличие от &, операция && гарантирует вычисление выражения слева направо: вначале вычисляется первый операнд со всеми побочными эффектами; если он равен 0, то значение выражения будет равно 0. В противном случае вычисляется правый операнд, и если он равен 0, то значение выражения будет равно нулю, в противном случае — единица. Результат имеет тип int.
313. В отличие от |, операция || гарантирует порядок вычислений слева направо: вначале вычисляется первый операнд со всеми побочными эффектами; если он не равен 0, то значение выражения будет равно 1. В противном случае вычисляется правый операнд, и ели он не равен 0, то значение выражения будет равно единице, в противном случае — нулю. Результат имеет тип Int.
314. Два выражения, разделенные запятой, вычисляются слева направо, и значение левого выражения отбрасывается. Тип и значение результата совпадают с типом и значением правого операнда. Вычисление всех побочных эффектов левого операнда завершается перед началом вычисления правого операнда.
315. Объявление задает способ интерпретации идентификатора, но не обязательно резервирует память, ассоциируемую с этим идентификатором. Объявления, которые действительно резервируют память, называются определениями.
316. Объявление со спецификатором extern внутри функции заявляет, что для объекта где-то в другом месте программы выделена память. Спецификатор typedef не резервирует никакой памяти и назван спецификатором класса памяти только для соблюдения стандартного синтаксиса.
317. Любое из short, long, signed и unsigned может использоваться и без int, которое в таком случае подразумевается по умолчанию. Если в объявлении нет ни одного спецификатора типа, то подразумевается тип int.
318. Смысл модификатора volatile зависит исключительно от реализации; стандартной семантики у него нет. Модификатор const применяется для объявления объектов, размещаемых в памяти, открытой только для чтения, а также для расширения возможностей оптимизации. Назначение модификатора volatile — запретить оптимизацию, которая могла бы быть выполнена в противном случае. Например, в системах, где ресурсы устройств ввода-вывода отображены на адресное пространство оперативной памяти, указатель на регистр устройства можно объявить с ключевым словом volatile, чтобы запретить компилятору сокращать кажущиеся ему избыточными обращения через указатель.
319. Структура — это объект, состоящий из последовательности именованных элементов различных типов. Объединение — это объект, который в каждый момент времени содержит один из нескольких элементов различных типов. Элемент структуры также может состоять из заданного количества битов. Такой элемент называется битовым полем ил просто полем. Его длина отделяется от имени двоеточием.
320. Если спецификатор с меткой, но без списка фигурирует там, где его метка не объявлена, создается неполный тип. Объекты неполного структурного типа могут упоминаться в контексте, где не требуется знать их размер, — например, в объявлениях (но не определениях) для описания указателя или создания нового типа с помощью typedef, но никаким иным образом. Тип становится полным при появлении последующего спецификатора с этой меткой, содержащего также список объявлений. Можно объявлять указатели на неполные типы.
321. Это маловразумительное правило — новое в стандарте ANSI.
322. Структуры с битовыми полями могут служить системно-переносимым методом для попытки уменьшить размеры памяти под структуру (вероятно, ценой удлинения кода программы и времени обращения к полям) или непереносимым методом для описания распределения памяти на битовом уровне. Во втором случае необходимо понимать правила и соглашения конкретной реализации языка.
323. Элементы структуры, не являющиеся битовыми полями, выравниваются по границам адресуемых ячеек памяти в зависимости от своего типа; таким образом, в структуре могут быть безымянные дыры. Если указатель на структуру приводится к типу указателя на ее первый элемент, результат указывает не первый элемент.
324. Идентификаторы, входящие в список перечислимых, объявляются константами типа int и могут употребляться везде, где требуются константы. Если в этом списке нет ни одного элемента со знаком =, то значения констант начинаются с 0 и увеличиваются на 1 по мере чтения объявления слева направо. Перечислимый элемент со знаком = присваивает соответствующему идентификатору указанное значение; последующие идентификаторы продолжают увеличиваться от заданного значения. Имена перечислимых, используемые в одной области действия, должны отличаться друг от друга и от имен обычных переменных, но их значения не обязаны отличаться.
325. Модификаторы типа, стоящие после символа *, относятся к самому указателю, а не к объекту, на который он указывает. const int *pci — тип указателя pci — «указатель на const int».
326. int strcpy(char *dest, const char *source) — второй аргумент — указатель на неизменяемую строку символов. Имена параметров играют роль удобных комментариев.
327. Указанные синтаксические конструкции заимствованы из языка C++.
328. В инициализаторе статического объекта или массива все выражения должны быть константными (см. раздел А.7.19).
329. Статический объект, не инициализированный явно, инициализируется так, как если бы ему (или его элементам) присваивалась константа 0. Начальное значение автоматического объекта, не инициализируемого явным образом, не определено.
330. Инициализатор структуры — это либо выражение того же структурного типа, либо заключенный в фигурные скобки список инициализаторов ее элементов, заданных по порядку. Безымянные битовые поля игнорируются и не инициализируются. Если инициализаторов в списке меньше, чем элементов в структуре, то оставшиеся элементы инициализируются нулями. Инициализаторов не должно быть больше, чем элементов.
331. Инициализатор массива — это список инициализаторов его элементов, заключенный в фигурные скобки. Если размер массива не известен, то он считается равным числу инициализаторов, при этом его тип становится полным. Если размер массива фиксирован, то число инициализаторов не должно превышать числа его элементов; если инициализаторов меньше, чем элементов, то оставшиеся элементы инициализируются нулями.
332. Структуры или массивы обобщенно называются составными объектами (aggregates). Если составной объект содержит объекты составного типа, то правила инициализации применяются рекурсивно. Фигурные скобки в некоторых случаях инициализации можно опускать. Так, если инциализатор элемента составного объекта, который сам является составным объектом, начинается с левой фигурной скобки, то этот подобъект инициализируется последующим списком разделенных запятыми инициализаторов. Если, однако, инициализатор подобъекта не начинается с левой фигурной скобки, то чтобы его инициализировать, отсчитывается соответствующее число элементов из списка; следующие элементы инициализируются оставшимися инициализаторами объекта, частью которого является данный подобъект.
333. Объявления, в которых спецификатором класса памяти является typedef, не объявляют объектов — они определяют идентификаторы, представляющие собой имена типов. Эти идентификаторы называются типоопределяющими именами.
334. long подразумевает long int.
335. За исключением оговоренных случаев, операторы выполняются в том порядке, в каком они записаны. Операторы не имеют вычисляемых значений и выполняются, чтобы выполнить определенные действия.
336. Чтобы в местах, где по синтаксису полагается один оператор, можно было выполнить несколько, предусматривается возможность задания составного оператора (который также называется блоком). Тело определения функции — это также составной оператор.
337. Инициализация автоматических объектов выполняется при каждом входе в блок и продолжается по мере перебора описателей. При переходе по метке внутрь блока никакие инициализации не выполняются. Инициализация статических объектов выполняется только один раз перед запуском программы.
338. Операторы switch можно вкладывать друг в друга. В первой редакции языка требовалось, чтобы и управляющее выражение switch, и константы в блоках case имели тип int.
339. Операторы цикла служат для организации циклов — повторяющихся последовательностей операторов.
340. Материал, подготовленный в качестве входных данных для компилятора С, называется единицей трансляции. Такая единица состоит из последовательности внешних объявлений, каждое из которых представляет собой либо объявление, либо определение функции.
341. Функция моет возвращать значения арифметического типа, структуры, объединения, указатели и void, но не «функции» и не «массивы».
342. int max(int a, int b, int c) { … }, где int — спецификатор объявления; max(int a, int b, int c) — описатель функции; { … } — блок, задающий ее код.
343. Термин «внешний» здесь используется, чтобы подчеркнуть расположение объявлений вне функций; напрямую с ключевым словом extern («внешний») он не связан. Класс памяти для объекта с внешним объявлением либо вообще не указывается, либо указывается в виде extern или static. Внешнее объявление объекта считается определением, если оно имеет инициализатор. Внешнее объявление, в котором нет инициализатора и спецификатора Extern, называется предварительным определением. Если в единице трансляции появится определение объекта, то все его предварительные определения станут считаться просто лишними объявлениями. Если никакого определения для этого объекта в единице трансляции не будет найдено, все его предварительные определения будут считаться одним определением с инициализатором 0.
344. Каждый раз компилировать всю программу нет необходимости: ее исходный текст можно хранить в нескольких файлах, представляющих собой единицы трансляции, а ранее скомпилированные функции можно загружать из библиотек. Связи между функциями программы могут осуществляться через вызовы и обращение к внешним данным. Следовательно, существует два вида областей действия (видимости) объектов: во-первых, лексическая область действия идентификатора, т.е. область в тексте программы, где компилятором воспринимаются все его характеристики; во-вторых, область действия, ассоциируемая с объектам и функциями, имеющими внешнее связывание. Внешнее связывание определяет связи, устанавливаемые между идентификаторами из раздельно компилируемых единиц трансляции.
345. Препроцессор языка С выполняет макроподстановку, условную компиляцию и включение именованных файлов. Строки, начинающиеся со знака # (перед которым разрешены символы пустого пространства), задают препроцессору инструкции-директивы. Их синтаксис не зависит от остальной части языка; они могут фигурировать где угодно и оказывать влияние (независимо от области действия) вплоть до конца единицы трансляции.
346. Применение директивы #undef к не определенному ранее идентификатору не считается ошибкой.
347. Операции # и ## позволяют брать аргументы макроса в кавычки и выполнять конкатенацию.
348. Порядок поиска указанного файла по его имени в ресурсах системы зависит от реализации.
349. Константные выражения в директиве #if и последующих строках #elif вычисляются по порядку, пока не будет обнаружено выражение ненулевым значением. Более того, перед раскрытием макросов выражения следующего вида всегда заменяются константами 1L, если указанный идентификатор определен, и 0L, если не определен: defined идентификтаор или defined ( идентификатор ). Следующие управляющие директивы эквивалентны: #ifdef идентификатор #ifndef идентификатор / #if defined идентификатор #if !defined идентификатор
350. Управляющая строка следующего вида предписывает препроцессору выполнить некоторую операцию, зависящую от реализации: #pragma последовательность-лексемнеоб. Неопознанная директива этого вида просто игнорируется.
351. В этой грамматике не приводят определения таких терминальных (фундаментальных, базовых) символов, как…
352. Стандартная библиотека С не является частью собственно языка, однако всякая среда, поддерживающая язык С, обязана предоставить программисту объявления функций, типов и макросов, содержащиеся в этой библиотеке.
353. Внешние идентификаторы, начинающиеся со знака подчеркивания, зарезервированы для использования библиотекой, как и все прочие идентификаторы, которые начинаются с этого знака плюс буква в верхнем регистре или еще один знак подчеркивания.
354. Функции ввода-вывода, типы и макросы, определенные в файле <stdio.h>, составляют почти треть библиотеки.
355. Потоком (stream) называется источник или получатель данных, который можно ассоциироваться с диском или другим периферийным устройством. Библиотека поддерживает текстовые или двоичные потоки, хотя в некоторых системах (в частности, в Unix) они не различаются. Текстовый поток — это последовательность строк; каждая строка содержит нуль или больше символов и заканчивается символом ‘\n’. Операционная среда часто преобразует текстовые потоки в другие представления и обратно (например, символ ‘\n’ может представляться комбинацией возврата каретки и перевода строки). Двоичный поток — это последовательность необработанных байтов, представляющих некие внутренние данные. Основное свойство двоичного потока состоит в том, что если его записать, а затем прочитать в одной и той же системе, поток от этого не изменится. Поток подсоединяется к файлу или устройству путем открытия; соединение разрывается путем закрытия потока. Во всех случаях, где это не вызывает путаницы, выражения «поток» и «файловый указатель» будут употребляться как взаимозаменимые.
356. Применительно к потоку вывода функция fflush выполняет запись всех буферизованных, но еще не записанных данных; результат применения к потоку ввода не определен.
357. Функция tmpfile создает временный файл с режимом доступа “wb+”, который автоматически удаляется при его закрытии или нормальном завершении программы. Функция tmpnam создает имя, а не файл.
358. Многие функции библиотеки в случае обнаружения ошибки или достижения конца файла устанавливают индикаторы состояния, которые можно изменять и проверять. Кроме того, целочисленное выражение errno (объявленное в <errno.h>) может содержать номер ошибки, который дает дополнительную информацию о последней из случившихся ошибок. Функция perror(s) выводит s и сообщение об ошибке (зависящее от реализации языка), которое соответствует целому числу в errno.
359. Функции возвращают ненулевое значение (истина), если аргумент удовлетворяет соответствующему условию или принадлежит указанному классу символов, и нуль (ложь) в противном случае.
360. Функции семейства mem… предназначены для манипулирования объектами как массивами символов; они образуют интерфейс к быстродействующим системным функциям.
361. Углы для тригонометрических функций задаются в радианах.
362. Функция srand использует параметр seed как инициализирующее значение для новой последовательности псевдослучайных чисел. Вначале параметр seed равен 1.
363. Функция free освобождает область памяти, на которую указывает p; она не делает ничего, если p равен NULL.
364. Какие значения аргумента status передавать в среду — зависит от реализации, однако нуль общепринят как сигнал успешного завершения программы.
365. Функция getenv возвращает строку среды, ассоциированную с name, или NULL, если такой строки не существует. Детали зависят от реализации.
366. Если в момент включения файла <assert.h> имя NDEBUG является определенным, то макрос assert игнорируется.
367. Функция clock возвращает время, измеряемое процессором в тактах от начала выполнения программы, или -1, если оно не известно. Пересчет этого времени в секунды выполняется по формуле clock() / CLOCKS_PER_SEC.
368. А это изменение пришлось по душе всем: 8 и 9 больше не являются восьмеричными цифрами.
369. Записанные вплотную строковые литералы автоматически сцепляются в один.
370. Конструкция long float как синоним double удалена из языка, но для работы с вещественными числами очень большой точности можно использовать тип long double.
371. Стандарт вводит требования к минимальному диапазону числовых типов и регламентирует обязательное наличие заголовочных файлов (<limits.h> и <float.h>) с характеристиками конкретных реализаций.
372. Стандарт заимствовал из языка C++ понятие модификатора типа — например, const (п. А.8.2).
373. Строки теперь нельзя модифицировать, поэтому их можно хранить в памяти, доступной только для чтения.
374. Изменены правила «обычных арифметических преобразований» (приведений типов). Ранее при приведении целых типов всегда доминировал unsigned, а вещественных — double. Теперь правило таково: результат приводится к наименьшему из достаточно вместительных типов (п. А.6.5).
375. Стандарт вводит понятие (заимствованное из С++) объявления прототипа функции, в котором содержатся типы аргументов и есть возможность различать функции с переменным количеством аргументов.